Skip to content

ci(release): index each tag on pkg.go.dev - #66

Merged
Behnam-RK merged 6 commits into
mainfrom
ci/index-releases-on-pkg-go-dev
Sep 10, 2026
Merged

ci(release): index each tag on pkg.go.dev#66
Behnam-RK merged 6 commits into
mainfrom
ci/index-releases-on-pkg-go-dev

Conversation

@Behnam-RK

@Behnam-RK Behnam-RK commented Sep 10, 2026

Copy link
Copy Markdown
Owner

What

publish gets 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:

curl -fsS "https://proxy.golang.org/github.com/behnam-rk/dezhban/@v/$TAG.info"
curl -fsS "https://pkg.go.dev/github.com/behnam-rk/dezhban@$TAG"

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

  • Warn-only, never fatal. By the time it runs, the tag is pushed and the release is created. A cache warm-up that timed out is not a failed release, and a red run would say it was. On exhaustion it emits ::warning and exits 0.
  • Placed last on purpose. The tag push and gh release create are the steps that must not be disturbed, and the documented "re-run publish after a failure that left a tag" path has to keep working unchanged. Warming twice is harmless, so a re-run costs nothing.
  • Module path from go list -m, not a hardcoded string. Go is already set up in that job for go run ./tools/relsign. Proxy URLs escape every uppercase letter as !<lower>; the module path is all-lowercase github.com/behnam-rk/dezhban while the repo is Behnam-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.
  • No KIND gate. 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 a main package. There is no importable library surface. This is recorded in releasing.md so it does not get chased as a bug later.

Docs

docs/contribute/releasing.md gets a ## pkg.go.dev section (the by-hand commands for a tag this step missed, the warn-only rationale, the license note), and the pipeline diagram's publish line is updated to stay true.

No CHANGELOG entry

Release tooling with no user-facing change. proxy.golang.org already serves all 14 existing tags (v0.1.0v0.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 (:107 SC2086, :470 SC2035). Nothing from the new step.
  • YAML parse confirms the step landed last in publish and no job boundary moved.
  • Ran the step body standalone against all three paths: the uppercase guard warns and exits 0; v99.0.0 exhausts all 6 attempts and exits 0 without tripping set -e; v0.13.0 succeeds against both endpoints.
  • go test ./internal/help passes — contribute/releasing.md is 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: true skips the whole publish job, 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 (requestReviewsByLogin does not resolve the bot and suggestedActors has no reviewer filter) — worth requesting from the PR page for an independent hosted read.

Round Findings Headline Introduced by
self-read 2 mod="$(go list -m)" unguarded under set -eu, in a step whose comment promises it never fails the original step
1 7 a doc claim that pkg.go.dev "never renders internal packages under any license" — it does, on a direct URL the original step
2 6 the retry loop could not fix the one cause its comment gave for it, and the warning handed over a command that would keep failing 3 of 6 were in text round 1 added
3 1 the doc promised the pkg.go.dev page carries an install line; it renders none the original step
4 1 round 3's fix quoted the license-restriction line on the module page, which does not contain it round 3

Stopped 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:

  • Round 1 folded three module-path guards into one case and 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.
  • Round 1's new immutability paragraph asserted the stranded-tag recovery was safe because the step runs after the failure that strands a tag. That does not follow — the tag was public for as long as it took to notice. Round 2 made it a precondition with a command to check it.
  • Round 1's same paragraph got the re-cut consequence wrong for most users: on the default GOPROXY they are served the cached original silently, which is worse than the checksum mismatch a direct fetcher sees.

Found by testing the fixes rather than by either reviewer: run outside a module, go list -m does not fail — it prints command-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 stubbing go: go list failing, command-line-arguments, an uppercase path, a go.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=bash clean; actionlint reports only the two pre-existing findings at :107 and :470. task check green throughout.

Still unrehearsable: dry_run: true skips the whole publish job, so the step is first exercised by the next real release.

Behnam-RK and others added 6 commits September 10, 2026 07:46
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>
@Behnam-RK
Behnam-RK merged commit c4886ab into main Sep 10, 2026
5 checks passed
@Behnam-RK
Behnam-RK deleted the ci/index-releases-on-pkg-go-dev branch September 10, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant