From 6eb2b24cd4d8a34f9feb46680a9e6a95f1b726dc Mon Sep 17 00:00:00 2001 From: jferrl Date: Fri, 18 Sep 2026 11:55:50 +0200 Subject: [PATCH] docs: add the v1.8.0 changelog and backfill two missing releases v1.8.0 covers everything since v1.7.0: the typed RateLimitError, the three token-minting and throttle-classification fixes, the webhook empty-secret fix, and the coverage and dependency work. Two of those alter behaviour a caller can depend on, so they sit under Changed with the consequence stated rather than among the fixes: a 403 permission failure no longer matches errors.Is(err, ErrRateLimited), and WithApplicationTokenExpiration now rejects values at or below 90s, which the README's own example used to use. The webhook fix gets its own Security heading for the same reason. v1.6.0 and v1.7.0 were tagged and published as GitHub releases but never written here, so the file jumped from February to now and implied nothing shipped in between. Both are backfilled from their release notes, v1.6.0 converted into the headings this file uses. Adds a note on the module path. The /v2 line is permanently retracted and unreachable: go list -m -versions reports no versions for it, and go get resolves none of the three. The note records that the retract block lives only in the v2.0.2 tag, since v2/ is no longer on main, so deleting that tag would silently restore v2.0.0 and v2.0.1. --- CHANGELOG.md | 205 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 172 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4680b3..45d3f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,51 +5,190 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +> **Module path.** `github.com/jferrl/go-githubauth` (v1.x) is the supported module. The +> `/v2` path was published by accident and is **permanently retracted**: v2.0.0, v2.0.1 +> and v2.0.2 are all covered by the `retract` block in `v2/go.mod` at the `v2.0.2` tag, +> including v2.0.2 itself, so `go get` resolves none of them and `go list -m -versions` +> reports no versions for that path. There will be no v2. +> +> That `v2.0.2` tag is the only thing carrying the retraction — the `v2/` directory no +> longer exists on `main` — so the tag must not be deleted or the retraction is lost. -### Notes +## [v1.8.0] - 2026-09-18 -#### GitHub stateless installation tokens: no action required +A correctness release. Two changes alter behaviour callers may depend on; both +are listed under Changed and are worth reading before upgrading. -GitHub is replacing the short opaque installation token with a `ghs_`-prefixed JWT of -about 520 characters -([announcement](https://github.blog/changelog/2026-05-15-github-app-installation-tokens-per-request-override-header/)). -This library is unaffected. It copies the token string from the API response into -`oauth2.Token.AccessToken` and never parses, measures or validates it. Expiry is read -from the response's `expires_at` field, so proactive refresh is unchanged. Both formats -are covered by tests. +### Added -GitHub Enterprise Server is out of scope. Enterprise Cloud and Data Residency endpoints, -reached with `WithBaseURL`, are in scope. Actions `GITHUB_TOKEN` is included in the -rollout. +- `RateLimitError{StatusCode, RetryAfter, Message}`, the concrete error returned when + GitHub throttles a request. It carries the wait the client computed from GitHub's own + headers, so a caller running its own backoff no longer has to re-parse a response it + never sees. Extract it with `errors.As`; it unwraps to `ErrRateLimited`, so + `errors.Is` and the rendered error string are unchanged (#65) -To force a format while validating your own code, set the temporary -`X-GitHub-Stateless-S2S-Token` header (`enabled` or `disabled`) through a custom -transport: +### Changed -```go -type statelessRT struct{ base http.RoundTripper } +- **A 403 is no longer treated as rate limiting just because it carries rate-limit + headers.** GitHub attaches `X-RateLimit-*` to essentially every authenticated + response, so a terminal `Resource not accessible by integration` was slept on for up + to 60s, retried, and returned wrapped in `ErrRateLimited`. A 403 now counts as + throttled only when it carries `Retry-After`, reports an exhausted budget, or says so + in its message. Callers branching on `errors.Is(err, ErrRateLimited)` will no longer + match a permission failure (#63) +- **`WithApplicationTokenExpiration` rejects values at or below 90 seconds**, the 60s + clock-drift backdate plus `DefaultExpirySkew`, falling back to the 10 minute default. + Below that bound the cache can never hold the token and every call re-signs. The + README previously documented `1 * time.Minute`, which is affected (#63) +- `WithHTTPClient` reuses the application JWT across requests, matching the default + transport, instead of re-signing on every installation-token request (#62) + +### Security + +- **An empty webhook secret is refused instead of used as an HMAC key.** HMAC accepts a + zero-length key, so a deployment that called `Middleware(nil)` or read a missing + secret from the environment verified every delivery against a key anyone can + reproduce, and forged payloads reached the downstream handler as authentic. `Verify` + now returns the new `ErrMissingSecret`, so a misconfigured deployment fails closed + (#61) -func (s statelessRT) RoundTrip(r *http.Request) (*http.Response, error) { - r = r.Clone(r.Context()) - r.Header.Set("X-GitHub-Stateless-S2S-Token", "enabled") - return s.base.RoundTrip(r) -} +### Fixed -src := githubauth.NewInstallationTokenSource(installationID, appSource, - githubauth.WithHTTPClient(&http.Client{Transport: statelessRT{base: http.DefaultTransport}}), -) -``` +- Named identifier types are accepted. The `Identifier` constraint is `~int64 | ~string`, + but the implementation type-switched on the exact dynamic type, so `type AppID int64` + was rejected with `unsupported identifier type` (#63) +- A short application token expiration no longer mints an already-dead JWT. Issuance is + backdated 60s for clock drift and only the upper bound was clamped, so a 30 second + expiration produced a token that had expired 30 seconds earlier (#63) +- A non-positive installation ID fails as a configuration error before any network call, + rather than as a 404 from GitHub (#62) +- The wait for a response header is bounded. Dial, TLS handshake and idle connections + were capped, but a server that accepted a connection and then stalled hung `Token()` + indefinitely, holding the token cache mutex against every concurrent caller (#61) +- The error body of a failed response is capped at 64 KiB (#63) +- Three comments claimed a default application JWT is usable for 9m30s; the backdating + makes it 8m30s (#63) +- `ErrRateLimited` and `WithRetryOnThrottle` godoc now describe the 403 contract they + actually implement (#63) +- Examples check the error from `resp.Body.Close` (#54) + +### Documentation + +- Added godoc examples, package documentation, `llms.txt`, and a comparison with + `ghinstallation` (#54) +- README reduced from 587 to 148 lines (#54) +- **GitHub stateless installation tokens: no action required.** GitHub is replacing the + short opaque installation token with a `ghs_`-prefixed JWT of about 520 characters + ([announcement](https://github.blog/changelog/2026-05-15-github-app-installation-tokens-per-request-override-header/)). + This library copies the token string into `oauth2.Token.AccessToken` and never parses, + measures or validates it, and expiry is read from `expires_at`, so both formats work + unchanged. GitHub Enterprise Server is out of scope; Enterprise Cloud and Data + Residency endpoints are in scope, as is the Actions `GITHUB_TOKEN` + +### Tests + +- Both installation token formats, stateless and classic opaque, are pinned for verbatim + passthrough +- Every reachable branch is covered, and a test that reached `example.com` over the + network on each CI run was replaced with ones that exercise what their names claim + (#66) + +### Internal + +- Dropped two branches no input can reach, and replaced a relative-reference endpoint + resolution with `url.URL.JoinPath`, which cannot fail and preserves an Enterprise + `/api/v3/` prefix by construction (#66) + +### Dependencies + +- Moved to the Go 1.26 toolchain +- Bumped `golang.org/x/oauth2` from 0.36.0 to 0.37.0 (#59) +- Bumped `github/codeql-action` from 4 to 4.38.0 (#53, #55, #56, #57, #58, #60) +- Bumped `actions/setup-go` from 6 to 7 (#52) + +**Full Changelog**: + +## [v1.7.0] - 2026-06-30 + +GitHub Enterprise Cloud support and a more foolproof installation-token configuration. + +### Added -GitHub ignores any other value, so confirm what you received by counting the dots after -`ghs_`: two means stateless, none means the classic form. Remove the header once you are -done. It will stop being respected at a future deprecation point. +- **Custom base URL**: `WithBaseURL` sets the API base URL verbatim, normalizing only a + trailing slash, mirroring how `go-github` targets a custom endpoint. Unlike + `WithEnterpriseURL` it does not append `/api/v3/`, which enables GitHub Enterprise + Cloud with data residency (`https://api.SUBDOMAIN.ghe.com/`) and pointing the client + at an `httptest` server in tests (#50) + +### Changed + +- **Order-independent options**: `WithBaseURL`, `WithEnterpriseURL`, `WithHTTPClient` and + `WithRetryOnThrottle` can be combined in any order. `WithHTTPClient` previously rebuilt + the client and silently discarded a base URL or retry setting applied before it +- **Fail-loud configuration**: an invalid base URL, or a nil HTTP client, is reported by + the first call to `Token()` instead of silently falling back to the public GitHub API + +### Fixed + +- `WithHTTPClient` operates on a shallow copy, so the caller's `*http.Client`, which may + be shared elsewhere, keeps its original transport +- Passing nil to `WithHTTPClient` yields a clear error instead of panicking + +### Maintenance + +- Removed the deprecated, no-op `net.Dialer.DualStack` field from the pooled HTTP client +- Renamed the unexported `githubClient.client` field to `httpClient` ### Tests -- Added `Test_createInstallationToken_TokenFormats`, covering verbatim passthrough of both - the stateless (520-character, two-dot) and classic opaque token formats, and confirming - expiry is sourced from `expires_at` +- Coverage for `WithBaseURL` (GHEC and `httptest` URLs), option order-independence, + fail-loud misconfiguration, and that the caller's HTTP client is not mutated + +### Dependencies + +- Bumped `actions/cache` from 5 to 6 (#49) +- Bumped `actions/checkout` from 6 to 7 (#48) +- Bumped `codecov/codecov-action` from 6 to 7 (#46) + +**Full Changelog**: + +## [v1.6.0] - 2026-04-20 + +### Added + +- **External key store support**: `NewApplicationTokenSourceFromSigner` accepts any + `crypto.Signer` with an RSA public key, so the App private key never enters process + memory. Works with AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault Transit, PKCS#11 + HSMs and ssh-agent. Construction verifies the signer's public key is `*rsa.PublicKey`, + since GitHub requires RS256 +- **Proactive token refresh**: `ReuseTokenSourceWithSkew` refreshes a cached token when + `time.Until(exp) <= skew` rather than waiting for expiry to pass, closing the window + where a request starts shortly before expiry and reaches GitHub already expired. Tune + with `WithExpirySkew` and `WithInstallationExpirySkew` +- **Automatic retry on throttling**: installation token fetches retry once on 429, or on + 403 carrying `Retry-After` / `X-RateLimit-Reset`. The sleep honors context cancellation + and is capped at 60s, and a terminal throttle wraps `ErrRateLimited` for `errors.Is`. + Opt out with `WithRetryOnThrottle(false)` +- **`webhook` subpackage**: constant-time HMAC-SHA256 verification of GitHub deliveries. + `Verify` with sentinel errors (`ErrMissingSignature`, `ErrInvalidSignatureFormat`, + `ErrSignatureMismatch`), and `Middleware` with body restoration, a 25 MiB default cap + and 401/413 short-circuits, configurable via `WithMaxPayloadSize` and + `WithErrorHandler` + +### Changed + +- **Minimum Go version is 1.25**, transitively required by `golang.org/x/oauth2` v0.36.0. + The README previously claimed 1.21 +- **Token sources refresh 30s before expiry by default.** Pass `WithExpirySkew(0)` or + `WithInstallationExpirySkew(0)` to restore the previous behaviour + +### Dependencies + +- Bumped `golang.org/x/oauth2` from 0.34.0 to 0.36.0 +- Bumped `codecov/codecov-action` from 5 to 6 +- Bumped `styfle/cancel-workflow-action` from 0.13.0 to 0.13.1 + +**Full Changelog**: ## [v1.5.1] - 2026-02-09