ci(release): index each tag on pkg.go.dev - #66
Merged
Conversation
pkg.go.dev is not a publish target. It is a read-through cache over proxy.golang.org, and a version lands there only once somebody asks the proxy for it — which, left alone, is whenever the first person happens to fetch the module. That is a coincidence, not a release step. Add a final step to `publish` that asks for the tag it just pushed: proxy.golang.org's `@v/<tag>.info` (this is what actually indexes the version), then pkg.go.dev's page (so it builds now instead of on the next index poll). The module path comes from `go list -m` rather than a hardcoded string, and the step bails with a warning if that path ever gains an uppercase letter, since proxy URLs escape those as `!<lower>` and the failure mode is a silent 404. It warns and never fails. By the time it runs the tag is pushed and the release is created, so a cache warm-up that timed out is not a failed release, and a red run would say it was. It is not gated on KIND either: pkg.go.dev files prereleases separately and never shows an rc as latest. Documented in releasing.md, including the by-hand commands for a tag this step missed, and the fact that pkg.go.dev deliberately renders no documentation for dezhban — the license is not on its allow-list, and every package here is internal/ or a main package regardless. No CHANGELOG entry: this is release tooling with no user-facing change. proxy.golang.org already serves all 14 existing tags; this only makes the timing deterministic for future ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The step's own comment promises it warns and never fails, and then opens with `set -eu` followed by an unguarded `mod="$(go list -m)"`. A `go list` that exits non-zero — a broken toolchain, a checkout without go.mod — takes the whole step down with it, and by that point the tag is pushed and the release is created. The one outcome the step was written to avoid is the one it produced: a red run over a shipped release. Guard the capture, and warn-and-skip on an empty module path. Proven by stubbing `go` to exit 1: the old line exits 1, the guarded one exits 0 after emitting the warning. Also drop the duplicated retry bound. `for attempt in 1 2 3 4 5 6` paired with `if [ "$attempt" -lt 6 ]` is the same number in two places, so a change to one silently sleeps a round too few or too many. One `attempts` variable now drives both, and the exhaustion warning quotes it rather than claiming "~2m", which was only true when every attempt failed fast. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 1 on the pkg.go.dev step. releasing.md claimed pkg.go.dev "never renders" internal packages under any license. It does render them on a direct URL; what is actually true is that nothing here is importable from outside the module, which is the claim the workflow comment was already making correctly. A reader who believed the stronger version would think the no-docs outcome was guaranteed by package layout rather than by the license, and would not notice when that stopped being true. The step's retry comment gave "the proxy negative-caches a miss" as a reason to retry. It is the opposite: a cached miss is exactly what retrying cannot clear, and it is the reason the budget stays small rather than the reason there is one. Anyone trusting that comment would tune the attempt count up, which cannot help either. The pipeline diagram had publish tagging before signing. It signs at :476 and tags at :502. The line was already being rewritten by this branch, so this is the cheap moment. Fold the module-path checks into one case that also rejects a path which is not a single word. A go.work — none today — makes `go list -m` print one module per line; the old code built a URL out of that, spent six attempts failing on it, and then emitted a ::warning containing the newline, which GitHub truncates, so the step lost its own diagnostic. Proven against the previous commit: it warns "did not serve github.com/behnam-rk/dezhban" and drops everything after the first line. `$mod` now goes to the log and stays out of the annotation. `[[:upper:]]` replaces `[A-Z]`, whose bracket range is collation-dependent. Trim the step's header comment to what a reader of release.yml needs — warn only, and why it is last — and point at releasing.md for the rest instead of restating three of its paragraphs beside a "see releasing.md" line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 2 on the pkg.go.dev step. The retry loop and the warning it ends with disagreed with the negative-cache fact stated between them. A miss is negative-cached, so the request that loses the propagation race is the one that installs the cached 404 and the five attempts after it only re-read it — the loop could not fix the one cause the comment gave for having it. Worse, the warning then promised "it will be indexed on first fetch" and handed over the identical request that had just failed six times, so an operator following it would keep hitting the cache and conclude the module was broken. Three attempts now, ten seconds apart, for what retrying can actually fix (a transient network error or a 5xx out of the runner), and the warning names the wait instead: the cache expires on its own, up to about 30 minutes, and the release itself is fine. releasing.md claimed the stranded-tag recovery was safe from all of this because the step runs after the failure that strands a tag. The step never fetching that version does not mean nobody did — the tag was public for as long as it took to notice, and one `go get …@latest` or a dependency bot in that window is enough. Made it a precondition with the command to check it, and linked the recovery paragraph to it. The re-cut consequence was also wrong for most users. On the default `GOPROXY=proxy.golang.org` they are served the cached original and silently get the old tree, which is worse than the `checksum mismatch` a direct fetcher sees, because the re-cut looks like it worked and shipped nothing. Both are stated now, and permanence is attributed to sum.golang.org rather than the proxy. Round 1 folded three module-path guards into one branch and lost the specific message with them; "no usable module path" is false of the uppercase case, where the path is fine and this step is the limitation. Each rejection carries its own reason again. Restored the stderr that round 0 threw away with `2>/dev/null` — in a step whose only failure mode is silence, the toolchain error is the line worth keeping. Added the branch that testing the above turned up: run outside a module, `go list -m` does not fail, it prints "command-line-arguments", which is a single lowercase word and cleared every check. A proxy path needs a host in its first element. The step summary said "Indexed on pkg.go.dev" after a curl whose status it explicitly ignores. It reports what it proved — indexed on proxy.golang.org — and links the page rather than asserting it is up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 3, verified against the live pages. The section promised the page carries "the import path, the version list and the install line". There is no install line — pkg.go.dev renders no `go get` or `go install` element on module or package pages, and the only copyable thing in the header is the import path. A maintainer who opened the page from the job summary looking for it would conclude the indexing step half-worked, which is the one thing this section exists to pre-empt. Say what is there instead: the import path, the version list, a `Directories` listing of cmd/dezhban and the internal/ tree, `License: UNKNOWN`, and the exact line that stands in for the docs — "Documentation not displayed due to license restrictions". Also name the subject of "warns, it never fails". Two paragraphs had been inserted between it and the step it refers to, leaving the pronoun pointing at the re-cut warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round 4. Round 3's own fix attributed the wrong page: the module page at pkg.go.dev/github.com/behnam-rk/dezhban@v0.13.0 contains no "Documentation not displayed due to license restrictions" text anywhere, and its internal/ rows ship collapsed behind "Show Internal Directories". That sentence is what tells a reader no-docs is the expected state, so getting the page wrong leaves them unable to tell a license block from a version pkg.go.dev has not ingested yet — the ambiguity the step's own summary line exists to flag. The module page carries the import path, the version list, License: UNKNOWN and the Directories listing, and has no doc area at all, there being no package at the module root. The license line lives one level down, on a package page such as cmd/dezhban. Say that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
What
publishgets one new final step,Index the tag on pkg.go.dev.pkg.go.dev is not a publish target — it is a read-through cache over
proxy.golang.org, and a version lands there only once somebody asks the proxy for it. Left alone, that is whenever the first person happens to fetch the module: a coincidence, not a release step. The new step asks, for the tag it just pushed:The first is what actually indexes the version; the second just makes pkg.go.dev build the page now rather than on its next index poll.
Design notes
::warningand exits 0.gh release createare the steps that must not be disturbed, and the documented "re-runpublishafter a failure that left a tag" path has to keep working unchanged. Warming twice is harmless, so a re-run costs nothing.go list -m, not a hardcoded string. Go is already set up in that job forgo run ./tools/relsign. Proxy URLs escape every uppercase letter as!<lower>; the module path is all-lowercasegithub.com/behnam-rk/dezhbanwhile the repo isBehnam-RK/dezhban, so a rename that introduced a capital would 404 in silence. The step detects that and bails with a warning rather than guess.KINDgate. rc tags are indexed too — pkg.go.dev files prereleases separately and never shows an rc as the latest version.pkg.go.dev will show no documentation, and that is expected
It renders docs only for licenses on its allow-list, and ours — Hippocratic License 3.0 (Core) with the Dezhban named-entity restriction — is a deliberately modified, non-OSI license its detector will not match. The page still carries the import path, the version list and the install line, with a not-legally-redistributable note where the docs would be.
Nothing is lost in practice: every package here is under
internal/, which pkg.go.dev never renders under any license, or is amainpackage. There is no importable library surface. This is recorded inreleasing.mdso it does not get chased as a bug later.Docs
docs/contribute/releasing.mdgets a## pkg.go.devsection (the by-hand commands for a tag this step missed, the warn-only rationale, the license note), and the pipeline diagram'spublishline is updated to stay true.No CHANGELOG entry
Release tooling with no user-facing change.
proxy.golang.orgalready serves all 14 existing tags (v0.1.0…v0.13.0); this only makes the timing deterministic for future ones. Flagging the omission rather than leaving it to be noticed.Verification
actionlint .github/workflows/release.yml— only the 2 pre-existing findings (:107SC2086,:470SC2035). Nothing from the new step.publishand no job boundary moved.v99.0.0exhausts all 6 attempts and exits 0 without trippingset -e;v0.13.0succeeds against both endpoints.go test ./internal/helppasses —contribute/releasing.mdis not a bundled help page, so the renderer's markdown subset does not apply.Untested end-to-end, and there is no way to rehearse it:
dry_run: trueskips the wholepublishjob, so this step is first exercised by the next real release. Watch its output on that run.🤖 Generated with Claude Code
Review loop
Two review rounds by a read-only reviewer, plus a self-read before round 1. Copilot could not be requested from the CLI on this repo (
requestReviewsByLogindoes not resolve the bot andsuggestedActorshas no reviewer filter) — worth requesting from the PR page for an independent hosted read.mod="$(go list -m)"unguarded underset -eu, in a step whose comment promises it never failsStopped after round 4 on the second stop condition: its only finding was in round 3's own fix, so loop-introduced findings (1) outnumbered pre-existing ones (0). Rounds 3 and 4 both verified the diff against the live pkg.go.dev and proxy.golang.org responses rather than from memory.
Loop-introduced regressions, named rather than absorbed:
caseand lost their distinct diagnostics with them. "No usable module path" is false of the uppercase branch, where the path is fine and this step is the limitation. Round 2 gave each rejection its own reason back.GOPROXYthey are served the cached original silently, which is worse than thechecksum mismatcha direct fetcher sees.Found by testing the fixes rather than by either reviewer: run outside a module,
go list -mdoes not fail — it printscommand-line-arguments, a single lowercase word that cleared every guard. A proxy path needs a host in its first element.Verification of the final step, by extracting the
run:block and stubbinggo:go listfailing,command-line-arguments, an uppercase path, ago.work-style multi-line path, three-attempt exhaustion, and the happy path — all exit 0, each with the right diagnostic, and the valid path emits no warning.shellcheck --shell=bashclean;actionlintreports only the two pre-existing findings at:107and:470.task checkgreen throughout.Still unrehearsable:
dry_run: trueskips the wholepublishjob, so the step is first exercised by the next real release.