Repository navigation
build: harden CI, release and dependencies; require Go 1.27.2 - #112
Merged
Merged
Conversation
The documentation called the rewrite the "v2 stack" and announced a "first v2 release", but every module path lacks a `/v2` suffix: under Go's semantic import versioning the go command ignores a `v2.x.y` tag on such a path, so a "v2" release could not be consumed. No tag exists yet (neither on GitHub nor on proxy.golang.org), so the first release of every module is `v1.0.0` and the paths stay as they are. Reword the occurrences accordingly: CLAUDE.md, MIGRATION.md (the breaking-change heading becomes "before the first release"), LIMITATIONS.md, CHANGELOG.md (header; the "Authorization v2" bullet named the stack, not an API version), docs/migration-from-v0.md (v0 to v1) and the oauth2 example. The release-tag guard that enforces v1 comes with the release checklist. Audit: CI-02.
TestResponseSizeLimit/gzip_bomb_counted_after_decompression checked that 8 MiB of zeros compress to fewer than 16 KiB before serving them. Go 1.27 changed the compress/flate encoder output and the payload now compresses to 16,438 bytes, so the test fails on every Go 1.27 toolchain (CI hid it by running go1.26.5). The bound was arbitrary. What the case relies on is that the bytes on the wire stay under Policy.MaxResponseBytes (64 KiB here), so only the decompressed size can trip the cap. Assert exactly that, once the policy is built; the ErrResponseTooLarge assertion is unchanged. Mutation check: making outbound count the compressed wire bytes (no transparent decompression, limiter before gzip.NewReader) makes the test fail with "Expected error ... response body too large ... but got nil". Audit: DEP-01.
The core module declared `tool github.com/vektra/mockery/v2`, which put mockery and ~45 tool modules (cobra, viper, zerolog, x/tools, ...) into the module graph of every consumer: one importing only the core resolved 50 modules. The tool was also unusable: `.mockery.yaml` uses mockery v3 syntax. And `make lint` piped an unpinned install script (`curl .../golangci-lint/HEAD/install.sh | sh`). Pin each tool in its own module, outside go.work, so that no tool dependency reaches a library go.mod and each tool builds with exactly the dependency set its upstream tests: - tools/golangci-lint: v2.14.0 (Go 1.27 support; built with x/tools v0.50.0, upstream's); - tools/govulncheck: golang.org/x/vuln v1.9.0; - tools/mockery: v3.8.1 (reads Go 1.27.2 export data). `make tools` installs them into build/bin, checksum-verified by each module's go.sum. The core now resolves 22 modules for a consumer. mockery is kept and used, and its output is never committed: `make generate` writes in-package mock_<interface>_test.go files (testify template), which exist only in test binaries, so no library build sees them and every module builds with GOWORK=off without them. test, lint, vuln, bench and tidy generate first; stale generated files are removed. Dependabot and `go mod tidy` never generate mocks, so every module that gets mocks keeps a mockery_test.go importing testify/mock: go.mod and go.sum stay identical with or without them (objx becomes an indirect requirement; checked with GOWORK=off build, vet and tidy -diff on all 25 modules, with 58 mocks generated and with none). The oidc/provider import policy allows testify/mock in tests. Add `make vuln` (govulncheck -test on every module, GOWORK=off) and make `make tidy` sync, tidy every module and tool, then sync again. Audit: DEP-03, CI-01.
Every module declared go 1.26.0 or 1.26.5 and CI tested with go1.26.5,
which carries standard-library advisories reachable from the library's
call paths (HTTP/2 server crashes and request smuggling in net/http,
crypto/tls post-handshake and ECH issues, encoding/asn1 recursion,
net/url, encoding/xml...). Nothing in the build enforced a patched
toolchain, and the mixed go lines made `GOWORK=off go build` fail in the
modules at 1.26.0 ("updates to go.mod needed").
Set `go 1.27.2` in every go.mod (the tool modules included) and in
go.work. Since Go 1.21 the go line is a minimum, so no consumer can build
the library with an unpatched toolchain (GOTOOLCHAIN=auto switches to
go1.27.2, verified by the checksum database; a GOTOOLCHAIN=local
toolchain stops with a clear message), and CI follows go.work. govulncheck
reports no reachable advisory on go1.27.2 in any of the 25 modules; the
only remaining item is GO-2026-5932 (x/crypto/openpgp is unmaintained, no
fix), at module level, never imported.
go mod tidy merges the duplicate require blocks of password and
internal/interop/saml (Go 1.27 behaviour). make sync is a no-op; every
module builds, vets and is tidy with GOWORK=off, with and without the
generated mocks. MIGRATION.md documents the floor and the Go 1.27
behaviour it brings.
Audit: DEP-01, CI-02.
Nothing checked that a module builds outside the workspace, and the
workspace hid two release problems:
- the mixed go lines made some modules unbuildable with GOWORK=off;
- every sub-module requires the in-repo modules at the placeholder
v0.0.0-00010101000000-000000000000, made to work by `replace`
directives. Consumers ignore those, so a tag cut from this tree could
not be resolved.
A GOWORK=off build alone would not catch the second problem, since the
module's own replace directives still apply.
`make check-modules` (scripts/check-modules.sh), for CI and every release,
fails unless:
- go.work and every go.mod carry one go line and no toolchain line;
- go.work lists every module (Dependabot's go.work mode covers exactly
those);
- `go work sync` changes nothing;
- every module and tool module is tidy with GOWORK=off without the
generated mocks (what go mod tidy and Dependabot see);
- every module builds, vets and is still tidy with them.
Seven mutants (go line drift, toolchain line, module missing from go.work,
version drift, missing mock anchor, untidy module and tool module) each
make it fail.
`make release-check TAG=...` (scripts/release-check.sh, also run by the
release workflow) refuses:
- a tag that is not a canonical v1.MINOR.PATCH[-PRERELEASE] (the module
paths carry no /vN, and the go command skips build metadata);
- the examples, internal and tool modules;
- a module path mismatch, a placeholder requirement, and a commit off
origin/master.
It then builds and vets a copy of the module without its in-repo replace
directives, outside the workspace: what a consumer resolves. A dry run
against a local module proxy refuses a module using a core symbol its
required core version lacks ("undefined: security.ReleaseCheckProbe"),
although its GOWORK=off build through replace passes.
docs/releasing.md is the checklist: v1.0.0 first, dependency order, one
release commit setting real requirements, the toolchain patch policy, and
the repository settings the maintainer sets once. The git-flow `release`
target (no develop branch exists) is removed.
Audit: CI-02.
…lease
The workflows had several supply-chain weaknesses:
- they ran with the repository's default write-scoped token;
- third-party actions were referenced by mutable tags or a branch
(coverallsapp/github-action@master, handed GITHUB_TOKEN explicitly);
- release.yml gave `contents: write` to the whole job, tests and a
third-party release action included, kept the checkout credentials and
interpolated `github.ref_name` into a script. Its `*/v*` filter missed
every module nested two levels deep (oidc/client, saml/sp,
oauth2/store/sql...);
- nothing scanned for vulnerabilities.
Changes:
- workflow-level `permissions: contents: read`, every action pinned to
a full commit SHA with its version in a comment (looked up from the
releases: checkout v7.0.1, setup-go v7.0.0, upload-artifact v7.0.2,
download-artifact v8.0.2, coverallsapp/github-action v2.3.8),
`persist-credentials: false` everywhere;
- go.yml builds the pinned tools, generates the mocks, then runs
check-modules, build, test and lint. Coveralls uploads from a separate,
non-blocking job without Go caches, with a pinned reporter;
- govulncheck.yml runs `make vuln` on every change and weekly;
- release.yml triggers on `v[0-9]*` and `**/v[0-9]*` and is split:
- `verify`, read-only: release-check, then the whole gate, with no
cache read;
- `publish`: `contents: write`, environment `release`, no checkout, no
third-party action, `gh release create --verify-tag`, notes since the
module's previous tag, never "latest" for a sub-module.
The tag only reaches scripts through `env`;
- Dependabot: the `/` gomod entry already covers the 25 workspace modules
(go.work mode: #103, #104, #105, #108 and #110 bumped dependencies only
sub-modules require); added the tool modules (allowed by name, since
tool requirements are indirect), groups for OpenTelemetry and actions,
and a 7-day cooldown.
actionlint: 0 findings. zizmor 1.30.1, online, default and pedantic
personas: 0 findings (master: 18 findings + 5 suppressed). Repository
settings stay manual (docs/releasing.md).
Audit: CI-01, DEP-01, DEP-02.
release-check only dropped the in-repo replace directives, and only in
their unversioned form, before building the "consumer view" of a module:
- a third-party replace, e.g. a local fork providing a symbol, or a
patched golang.org/x/net, still applied. The check passed while
consumers, who ignore every replace and exclude directive of a
dependency, failed or got the vulnerable version;
- a versioned in-repo replace (`replace …/security v1.0.0 => ../`) was
left in place. It was refused only because `../` stops resolving from
the temporary copy, with a misleading message.
Read `go mod edit -json` in the copy, drop each in-repo replace by its
exact Old.Path[@Old.Version], then refuse any remaining replace and any
exclude: a release must not need them. In a dry run:
- the reviewer's probe, a third-party replace providing a symbol, is now
refused ("keeps replace directives its consumers ignore");
- a versioned in-repo replace is dropped, so a missing core symbol is
reported as "undefined: security.ReleaseCheckProbe";
- an exclude is refused.
Sort the previous tag with versionsort.suffix=- so that v1.0.0-rc.1
precedes v1.0.0 (release notes start from the right tag), and give the
anchor hint in check-modules' last step too.
Audit: CI-02.
Coverage: the Coveralls action ran a reporter binary downloaded at run time from a mutable release, verified only against a checksum file of the same release. That ran on master pushes, on a hosted runner where any code can read ACTIONS_RUNTIME_TOKEN and seed caches later runs restore. Upload with goveralls instead, pinned in tools/goveralls (v0.0.12, built from its own go.sum and the checksum database, like the other tools), from the build job with the read-only token as COVERALLS_TOKEN, as goveralls documents for GitHub Actions; a Coveralls outage does not fail the build. The artifact actions and the coverage job go away: the workflows now use only actions/checkout and actions/setup-go. Concurrency: one group per ref meant that a push to master cancelled the run still waiting behind another, so a master commit could end up with no build, test or govulncheck result. Group pull requests by ref (cancelling superseded runs) and everything else by commit. Release gate: scanning every module with -test let one unfixable advisory in a test-only dependency block every release. The release now scans the released module's own code outside the workspace; the full scan stays in the govulncheck workflow, which must be green on the release commit. Dependabot allows goveralls in /tools/*. actionlint: 0 findings; zizmor (online; regular, pedantic and auditor personas): 0 findings. Audit: CI-01, DEP-01.
Code-review fixes to the documentation of the build and release changes:
- CLAUDE.md:
- the per-step gate is `make test lint check-modules` (and `make vuln`
before a pull request);
- commit what `make tidy` / `go work sync` change, and how to resolve a
go.mod conflict;
- run `make generate` once before running `go test` directly;
- the tests of a published module need `make generate` in a repository
checkout.
- docs/releasing.md:
- release-check also refuses replace and exclude directives;
- the release gate scans the released module;
- a Dependabot section. Security updates run in a sub-module directory
without go.work, so `make tidy` must be pushed onto those PRs before
merging. Every entry is checked after a configuration change (the
gomod jobs download the toolchain), with a manual fallback;
- the tests of published modules.
- LIMITATIONS.md: the same limit for published modules' tests.
- docs/security-considerations.md: checksum verification is disabled by
`GOSUMDB=off` or by a matching `GOPRIVATE`/`GONOSUMDB` pattern. The
page named a variable that does not exist and a flag removed from the
go command.
- MIGRATION.md, CHANGELOG.md:
- the token is read-only by default, and only the release publish job
gets `contents: write`;
- coverage goes through goveralls;
- a consumer of the core resolves 23 modules instead of 50, both counts
including the consumer;
- two Markdown lists were split by a blank line.
- .mockery.yaml: why unexported interfaces get mocks too.
Audit: CI-01, CI-02, DEP-03.
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the following findings of the internal security audit of 2026-10-12 (commit c5c2ac3): DEP-01, DEP-02, DEP-03, CI-01, CI-02.
It also completes two maintainer decisions: Go 1.27.2 everywhere (with a govulncheck job), and Dependabot PRs #101–#110 merged with Dependabot extended. The first release of every module is
v1.0.0.DEP-01 — unpatched toolchain, no vulnerability scanning
Problem.
go.work), and the modules declaredgo 1.26.0/1.26.5.net/http,crypto/tls(post-handshake KeyUpdate, ECH),encoding/asn1recursion,net/url,encoding/xml.Fix.
go.mod(tool modules included) andgo.workdeclarego 1.27.2. Since Go 1.21 that line is a hard minimum for consumers:GOTOOLCHAIN=autoswitches to go1.27.2, which I verified: go1.27.1 with an empty module cache printedgo: downloading go1.27.2;GOTOOLCHAIN=localtoolchain stops with a clear message.go.worknames.govulncheckworkflow (v1.9.0, pinned) scans every module outside the workspace, tests included, on every change and weekly. The release workflow scans the released module itself.golang.org/x/crypto/openpgpis unmaintained, no fix). No module importsopenpgp.Breaking: consumers need Go ≥ 1.27.2. See the new MIGRATION.md section "Toolchain: every module requires Go 1.27.2", which also lists the Go 1.27 behaviour the floor brings:
encoding/jsonon the v2 implementation, with different error texts;Close;SSL_CERT_*honoured on darwin and windows.Side effect of Go 1.27 found here.
outbound'sTestResponseSizeLimit/gzip_bomb_counted_after_decompressionfails on every Go 1.27 toolchain: the newcompress/flateencoder makes the payload 16,438 B, over an arbitrary 16 KiB precondition. CI hid it by running go1.26.5. The precondition is now the invariant the case relies on: compressed size <MaxResponseBytes.DEP-02 —
golang.org/x/netv0.58.0 ingrpcAlready fixed on master by the Dependabot merge #109: its go.work update raised
x/netto v0.60.0 ingrpcandexamples. Verified: govulncheck reportsgrpcandexamplesclean.The root cause was that Dependabot version updates skip indirect requirements. It is now covered by the govulncheck job, plus the maintainer step "enable Dependabot alerts and security updates" (below).
DEP-03 — mockery in every consumer's module graph
Problem.
tool github.com/vektra/mockery/v2, so a consumer importing only the core resolved 50 modules (mockery, cobra, viper, zerolog, x/tools…)..mockery.yamlis mockery v3 syntax.Fix.
tools/, outsidego.work, so no tool dependency reaches a librarygo.mod:tools/golangci-lintv2.14.0: Go 1.27 support, built with upstream's own x/tools v0.50.0;tools/govulncheckv1.9.0;tools/goverallsv0.0.12: coverage upload;tools/mockeryv3.8.1: the release that reads Go 1.27.2 export data.make toolsinstalls them, checksum-verified by eachgo.sum.go list -m all.Mocks (maintainer decision: kept, used, generated by CI, never committed).
make generatewrites in-packagemock_<interface>_test.gofiles (testify template,EXPECT()API; 58 today).GOWORK=offwithout them.test,lint,vuln,tidyand CI generate first; stale generated files are deleted beforehand.stretchr/objx, while Dependabot andgo mod tidynever generate mocks. So each of the 16 modules that get mocks keeps a committedmockery_test.goimportingtestify/mock, which makesgo.mod/go.sumidentical with and without the mocks.check-modulesverifies both states.mocks/packages were rejected: Dependabot runs a strictgo mod tidyin every workspace module, and a committed import of a not-generated package would break it.docs/releasing.mdand LIMITATIONS.md: a published module's tests compile only from a repository checkout aftermake generate, because a module zip has no mocks. The library builds are unaffected.CI-01 — CI/release supply chain
Problem. The workflows:
coverallsapp/github-action@masterwas even handedGITHUB_TOKEN);contents: writeto the whole release job, tests and a third-party action included;github.ref_nameinto a script.Moreover:
*/v*filter never matched modules nested two levels deep (oidc/client,saml/sp,oauth2/store/sql…);make lintpipedcurl …/HEAD/install.sh | sh.Fix.
permissions: contents: read); only the releasepublishjob getscontents: write.persist-credentials: falseeverywhere.concurrency: pull requests are grouped by ref, with superseded runs cancelled. Every other run is grouped by commit, so a master commit always keeps its CI result.go.yml: tools →make generate→check-modules→ build → test → lint →make coveralls.tools/goveralls, withCOVERALLS_TOKENset to the read-only workflow token, as goveralls documents for GitHub Actions.continue-on-error, so a Coveralls outage never fails the build.govulncheck.yml: every module, tests included; on every change, plus a weekly run.release.yml: tagsv[0-9]*and**/v[0-9]*, split in two jobs:verify, read-only, no cache read:scripts/release-check.sh,check-modules, build, test, lint, and govulncheck on the released module's own code;publish:contents: write, environmentrelease, no checkout, no third-party action. It runsgh release create --verify-tagwith notes since the module's previous tag,--prereleasefor-rc, and--latest=falsefor sub-modules.The tag reaches scripts only through
env.directory: "/"is kept. Dependabot's go.work mode (dependabot-core #14909) already covers all 25 workspace modules, contrary to the audit's "1 of 25": build(deps): bump github.com/chromedp/chromedp from 0.16.0 to 0.20.1 #103, build(deps): bump connectrpc.com/connect from 1.20.0 to 1.21.0 #104, build(deps): bump modernc.org/sqlite from 1.57.0 to 1.60.1 #105, build(deps): bump google.golang.org/grpc from 1.83.2 to 1.84.0 #108 and build(deps): bump github.com/redis/go-redis/v9 from 9.22.0 to 9.23.0 #110 bumped dependencies that only sub-modules require. Listing every directory would duplicate every PR./tools/*(the tools are allowed by name, since tool requirements are indirect) and the actions entry, with OpenTelemetry/actions groups and a 7-day cooldown.CI-02 — module build hygiene and real versions at release
Problem.
golines made modules unbuildable withGOWORK=off.v0.0.0-00010101000000-000000000000behindreplace. Consumers ignorereplace, so a tag cut from this tree could not be resolved, and aGOWORK=offbuild does not notice: the module's ownreplacestill applies.Fix.
make check-modules(CI and release) fails unless:go.workand everygo.modshare onegoline, with notoolchainline;go.worklists every module (Dependabot coverage);go work syncchanges nothing;GOWORK=offwithout the generated mocks;make release-check TAG=…(scripts/release-check.sh, also inverify) accepts only a canonicalv1.MINOR.PATCH[-PRERELEASE]tag of a released module (no build metadata, no v0/v2+). It refuses:origin/master;replaceother than the in-repo ones, and anyexclude: consumers ignore both.It then builds and vets a copy of the module without its in-repo
replacedirectives, dropped by their exactpath@version, outside the workspace. That is what a consumer resolves.docs/releasing.md: v1.0.0 first; release order; one release commit setting real requirements; tags pushed in dependency order; consumer check; toolchain patch policy; unfixable-advisory policy; Dependabot; tests of published modules; repository settings.The "v2 stack / first v2 release" wording becomes v1 everywhere (docs-only commit): the module paths carry no
/vN, so the go command would ignore a v2 tag.The dead git-flow
releaseMakefile target is removed (nodevelopbranch exists).Dependabot PRs #101–#110
All merged, each on a Dependabot-rebased head with green CI:
7084908227bc1a4f7b95518828b4b7651731e70d05c3990cf9ab63c9958ab7aTests
"16438" is not less than "16384".outboundcounting wire bytes (no transparent decompression, limiter beforegzip.NewReader) → fails withExpected error with "outbound: response body too large" in chain but got nil.check-modules: 7 mutants, all killed: go-line drift,toolchainline, module missing fromgo.work, version drift (sync), missing mock anchor, untidy module, untidy tool module.release-checkdry run. It ran in a throwaway clone (never pushed) against a localfile://module proxy holding fake in-repo versions.+build, v0 and v2 tags;examples,internal,toolsand unknown directories;oidc/client,saml/sp/store/sql);…@v1.0.1 … 404);replaceproviding a symbol:keeps replace directives its consumers ignore: go.opentelemetry.io/otel => ./zz_otelfork, while itsGOWORK=offbuild passes;exclude.v1.0.0,v1.0.0-rc.1,oauth2/v1.0.0,oidc/client/v1.0.0;saml/sp/store/sql/v1.0.0, oncesaml/spis published;replace, dropped by itspath@version.undefined: security.ReleaseCheckProbe, with an unversioned or a versioned in-repo replace, whileGOWORK=off go buildthroughreplacepasses.$GITHUB_OUTPUTis right:versionsort.suffix=-(v1.0.0→v1.0.0-rc.1);ghfor the core, a nested module and an-rc.service_name=github, run ID, git head and branch, 173 repository-relative source files, 10,891 of 11,213 lines covered.oidc/provider's import-policy test allowstestify/mockin tests only, so a non-test mock would still be caught.Checks (Go 1.27.2)
make syncmake tools/make generatemake check-modulesmake build/make testmake lint(v2.14.0)make vulnscripts/*.sh, publish step)Coverage of the touched packages:
outboundsecurity(core)oidc/providerNo library code changed.
Follow-ups
After merge, run "Check for updates" on each Dependabot entry (Insights → Dependency graph → Dependabot) and read the logs:
gomodjob downloads go1.27.2 on Dependabot's go1.27.0 image, so look for a toolchain download error;/tools/*: check its indirect allow-list.The fallbacks are in
docs/releasing.md("Dependabot").Once Dependabot security updates are enabled: their PRs run in a sub-module directory without
go.work, so pushmake tidyonto them before merging.check-modulesfails otherwise (documented indocs/releasing.md).Optionally exercise
release.ymlend to end on a throwaway fork withv1.0.0-rc.0,oauth2/v1.0.0-rc.0,oidc/client/v1.0.0-rc.0andexamples/v1.0.0(the last must fail). A fork's tags never reach the real module path.12 test files predating this PR are not gofmt-clean (
oauth2/server_test.go,basic/authenticator_test.go…). Fix them after the audit-fix campaign, to avoid conflicts.Notes for the maintainer
Repository settings to change by hand (not done by this PR):
allow_auto_mergeis on;master: pull request + required status checkBuild & test (workspace). Keepgovulnchecknon-required: an advisory without a fix must not block every merge. The scheduled run and the release gate cover it;v*and**/v*: creation, update and deletion restricted to maintainers. A*does not match/, hence**/v*.release(created on first use): add required reviewers to approve each publication before the write token is minted.Coverage upload: goveralls is built from its pinned module (
tools/goveralls), verified by its go.sum and the checksum database like the other tools, and authenticates with the read-only workflow token. No binary is downloaded at run time. A Coveralls outage does not fail the build.Convention change for open PRs:
make test lint check-modules, plusmake vulnbefore a PR.make sync/make tidyresults must now be committed: CI fails ifgo work syncortidywould change a file.make generatecomes first; run it once before callinggo testdirectly.stdversioningo test; printf%wwith a pointer-to-error operand) and lint v2.14.0 (gosec 2.29 re-enables G407) apply.make tidy→make check-modules→ commit.