-
Notifications
You must be signed in to change notification settings - Fork 535
Refactor shared value formatting and unify GitHub error/SHA classification paths #53018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3bb758b
d07bf7c
3ef7e0d
a0856f2
49a8e93
aaec1b1
7935c8f
6f6c0ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # ADR-53018: Centralize GitHub Error Classifiers and Value Formatting | ||
|
|
||
| **Date**: 2026-08-16 | ||
| **Status**: Draft | ||
| **Deciders**: Unknown | ||
|
|
||
| --- | ||
|
|
||
| ### Context | ||
|
|
||
| The codebase had duplicate implementations of two cross-cutting concerns scattered across multiple packages. `IsAuthError` and `IsRateLimitError` were defined in `pkg/gitutil` but called by `pkg/cli` and `pkg/parser` — packages that have no semantic dependency on git operations. Placing error-classification logic in a git utility package created an inappropriate coupling: callers that only needed to classify GitHub API responses had to import git infrastructure. Independently, `marshalEnvValue` in `pkg/workflow` contained inlined JSON/reflect normalization that duplicated the same logic already present in `importinpututil.FormatResolvedValue`, creating a split-brain risk where the two serialization paths could diverge silently. Additionally, full-SHA validation was written inline as `len(x)==40 && gitutil.IsHexString(x)` at seven separate call sites rather than using the already-exported `gitutil.IsValidFullSHA` predicate. | ||
|
|
||
| ### Decision | ||
|
|
||
| We will move `IsAuthError` and `IsRateLimitError` out of `pkg/gitutil` and into `pkg/errorutil` as the canonical shared API for GitHub error classification. We will update all callers across `pkg/cli` and `pkg/parser` to import from `errorutil`. We will replace `marshalEnvValue`'s inlined JSON/reflect normalization with a delegation to `importinpututil.FormatResolvedValue`, keeping only a `fmt.Sprint` scalar fallback and a `nil`→`""` guard. We will replace all inline `len(x)==40 && IsHexString(x)` predicates with `gitutil.IsValidFullSHA`. | ||
|
|
||
| ### Alternatives Considered | ||
|
|
||
| #### Alternative 1: Keep classifiers in `gitutil`, add re-export shims in `errorutil` | ||
|
|
||
| Re-export `gitutil.IsAuthError` and `gitutil.IsRateLimitError` from `errorutil` without moving the implementation. Callers can import from either package. This avoids touching the implementation and keeps `gitutil` as the authority, but it creates two public APIs for the same function, does not fix the semantic mismatch (error classification is not a git concern), and leaves the underlying coupling intact. It was rejected because it trades a clean break for ongoing confusion about which package owns the behavior. | ||
|
|
||
| #### Alternative 2: Inline error-classification logic at each call site | ||
|
|
||
| Remove shared classifiers entirely and duplicate the substring checks wherever they are needed. This eliminates the package-dependency question but defeats the goal of a single source of truth, making future changes to classification phrases error-prone and requiring updates across many files. It was rejected because the problem that motivated `gitutil.IsAuthError` in the first place — avoiding scattered inline checks — would recur immediately. | ||
|
|
||
| ### Consequences | ||
|
|
||
| #### Positive | ||
| - `pkg/gitutil` scope is now narrowly defined as git repository operations and SHA/ref validation, eliminating an inappropriate coupling to GitHub API error semantics. | ||
| - `pkg/errorutil` becomes the single authoritative location for GitHub error classification, so future phrase changes need to be made in exactly one place. | ||
| - `marshalEnvValue` and `importinpututil.FormatResolvedValue` are guaranteed to produce identical serialization for arrays and maps, eliminating the risk of silent divergence between the two code paths. | ||
| - Inline SHA predicates are replaced by a named, tested, regex-backed function, reducing the chance of off-by-one errors (e.g., accepting mixed-case or 64-character SHAs). | ||
|
|
||
| #### Negative | ||
| - The change touches 21 files across `pkg/cli`, `pkg/parser`, `pkg/workflow`, `pkg/gitutil`, and `pkg/errorutil`, making it a wide-surface refactor that carries merge-conflict risk for any concurrent branches importing `gitutil.IsAuthError`. | ||
| - Removing `IsRateLimitError` and `IsAuthError` from `pkg/gitutil`'s public API is a breaking change for any external consumers that imported those symbols directly (though this appears to be an internal-only codebase). | ||
|
|
||
| #### Neutral | ||
| - `isPermissionErrorStr` in `pkg/cli/audit.go` now delegates to `errorutil.IsAuthError` and augments with audit-specific markers (`exit status 4`, `permission`, `gh auth login`, workflow guidance) rather than maintaining its own canonical union — this preserves audit-command-specific behavior without duplicating shared logic. | ||
| - Tests for the moved functions are migrated from `pkg/gitutil` to `pkg/errorutil`, and spec tests are updated to reflect the new package ownership. | ||
|
|
||
| --- | ||
|
|
||
| *ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,37 @@ func IsGoneError(err error) bool { | |
| return matched | ||
| } | ||
|
|
||
| // IsRateLimitError reports whether output indicates a GitHub API rate-limit error. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/improve-codebase-architecture] This redundancy isn't harmful, but it means the first documented phrase has no independent coverage. Consider either removing the redundant literal or documenting that 💡 Why this mattersIf someone later adds a string-specific exclusion or a more restrictive matcher, the documented phrases should be independently testable. A deduplication comment in // "rate limit exceeded" is a suffix of "api rate limit exceeded" — one pattern covers both.@copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The redundant API-specific phrase remains covered by the canonical |
||
| // The check is case-insensitive and matches known API phrases. | ||
| func IsRateLimitError(output string) bool { | ||
|
|
||
| matched := containsSubstring(output, | ||
| "rate limit exceeded", | ||
| "secondary rate limit", | ||
| ) | ||
| if matched { | ||
| errorutilLog.Printf("Classified output as rate-limit related (len=%d)", len(output)) | ||
| } | ||
| return matched | ||
| } | ||
|
|
||
| // IsAuthError reports whether output indicates an authentication or | ||
| // authorization issue from the GitHub API or gh CLI. | ||
| func IsAuthError(output string) bool { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 Why this blocks mergeThe old audit helper had extra command-specific heuristics, but the shared classifier is now used in parser and CLI fallback logic to decide whether to retry with That changes control flow, not just messaging: callers will take fallback branches intended only for missing/invalid credentials, and the final error becomes misleading when the fallback also fails. Please tighten the classifier so it only matches credential-specific markers, or require stronger context than a generic |
||
| matched := containsSubstring(output, | ||
| "gh_token", | ||
| "github_token", | ||
| "authentication", | ||
| "not logged into", | ||
| "unauthorized", | ||
| "permission denied", | ||
| "saml enforcement", | ||
| ) | ||
| if matched { | ||
| errorutilLog.Printf("Classified output as auth-related (len=%d)", len(output)) | ||
| } | ||
| return matched | ||
| } | ||
|
|
||
| // containsErrorSubstring reports whether err contains any of the provided | ||
| // substrings after lowercasing the full error message for case-insensitive | ||
| // matching. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd]
isPermissionErrorStrinaudit.gonow lowercases the input before matching, but the existingerrorutil.IsAuthError(which it delegates to first) also lowercases internally. This is correct and harmless, but the audit-specific branch that follows usesstrings.ToLower(s)on a different variable (lower). One subtle gap: the original"GitHub CLI authentication"marker is no longer present — it was removed and not mapped to any matching pattern inerrorutil.IsAuthError("authentication"would match it, but"GitHub CLI authentication"is a distinct phrase). A targeted test for this removed literal would confirm it's still covered.💡 Suggested test
@copilot please address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added coverage for the legacy GitHub CLI authentication marker in aaec1b1.