Skip to content

Error Handling

Every way a labelsync run can fail has a sentinel error in internal/labelsync/errors.go. Sentinels are always wrapped with %w, and are surfaced as a stable error_kind string in JSON output.

The wrapping rule

A call site with context to add never returns a sentinel bare, and never renders one with %v or into a freshly constructed error — either breaks both errors.Is matching and KindOf:

return fmt.Errorf("%w: %s", labelsync.ErrInvalidColor, raw)

Callers match with errors.Is; the JSON output layer calls KindOf to render error_kind:

func KindOf(err error) string // stable kind string, or "" when no known sentinel is wrapped

The kind strings are a public contract. They may be added to, never renamed.

Sentinels

SentinelKind stringRaised when
ErrConfigNotFoundconfig_not_foundNo config at --config, in the working directory, or under the XDG config dir
ErrAmbiguousConfigFileambiguous_config_fileBoth labels.yml and labels.yaml exist in one directory
ErrConfigExistsconfig_existsinit was asked to scaffold over an existing config file, without --force
ErrUnsupportedConfigVersionunsupported_config_versionversion is missing, or names a schema this binary does not understand
ErrEmptyConfigempty_configThe config parses but declares no labels
ErrDuplicateLabelNameduplicate_label_nameTwo labels share a name (compared case-insensitively, as GitHub does)
ErrDuplicateLabelColorduplicate_label_colorTwo labels share a colour — uniqueness is global, not per repository
ErrInvalidColorinvalid_colorA colour is not a 6-digit hex value, with or without a leading #
ErrInvalidLabelNameinvalid_label_nameA label name is empty, emoji only, or over the 50 code points GitHub accepts
ErrDescriptionTooLongdescription_too_longA description exceeds 100 code points
ErrUnknownGroupunknown_groupA label, or defaults.groups, references an undefined group
ErrAmbiguousGroupSourceambiguous_group_sourceA group sets more than one of org, user, repos, include_groups
ErrCyclicGroupcyclic_groupinclude_groups forms a cycle
ErrInvalidRepoRefinvalid_repo_refA repository reference is not in owner/repo form
ErrInvalidRenameinvalid_renameA rename is malformed, chained, or targets a name no label declares
ErrNoTokenno_tokenThe token resolution chain found no GitHub credential
ErrInteractiveRequiredinteractive_requiredAn operation needs a prompt but stdin is not a TTY
ErrRepoInaccessiblerepo_inaccessibleA repository is missing, archived, or outside the token’s scopes
ErrMaxWaitExceededmax_wait_exceededA rate-limit backoff would sleep past the --max-wait ceiling
ErrBudgetExhaustedbudget_exhaustedAn apply needs more requests than the rate-limit budget has left

All config validation runs at load, before any network call, and fails fast. ErrRepoInaccessible is the exception to fail-fast: it is reported per repository and the run continues, with the process exit code reflecting that repositories were skipped.

Adding a sentinel

Three edits, in the same change:

  1. The Err* variable and a KindOf case in internal/labelsync/errors.go.
  2. A row in the table above.
  3. An entry in the allSentinels table in internal/labelsync/errors_test.go.

The test derives its expected set by parsing the package source for exported Err* variables, so a sentinel that is declared but not tabled — or tabled after being removed — fails the build rather than silently escaping KindOf and rendering an empty error_kind.


The sentinel table and the reasoning behind the error_kind contract are also in the design record: design.md § Error handling.