From d57286c92fd9b13cd9f51c16d2d09c96e8c95ca9 Mon Sep 17 00:00:00 2001 From: Behnam RK Date: Thu, 10 Sep 2026 07:46:45 +0330 Subject: [PATCH 1/6] ci(release): index each tag on pkg.go.dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/.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 `!` 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) --- .github/workflows/release.yml | 60 +++++++++++++++++++++++++++++++++++ docs/contribute/releasing.md | 36 ++++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78fa6b7..d83098d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -562,6 +562,66 @@ jobs: echo "### Published [$TAG](https://github.com/$GITHUB_REPOSITORY/releases/tag/$TAG)" >> "$GITHUB_STEP_SUMMARY" + # 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: not a release step so much as a coincidence. Asking + # for it here makes the listing deterministic. + # + # Warn-only, never fatal. By this point the tag is pushed and the release + # is created, so a failure here would paint a shipped release red over a + # cache warm-up — and the proxy still picks the tag up on first fetch. + # + # NOTE: pkg.go.dev lists the module but renders NO documentation, because + # LICENSE (Hippocratic 3.0 Core plus the Dezhban named-entity restriction) + # is not on its allow-list. That is expected, not a regression: every + # package here is internal/ or a main package, so there is no importable + # surface to lose. See docs/contribute/releasing.md. + - name: Index the tag on pkg.go.dev + env: + TAG: ${{ needs.prepare.outputs.tag }} + run: | + set -eu + mod="$(go list -m)" + + # Proxy URLs escape every uppercase letter as `!`. This module + # path is all lowercase (github.com/behnam-rk/dezhban) even though the + # GitHub repo is Behnam-RK/dezhban, so no escaping is needed — but a + # rename that introduced a capital would 404 in silence. Say so rather + # than guess. + case "$mod" in + *[A-Z]*) + echo "::warning title=pkg.go.dev::module path $mod has uppercase; proxy URLs need !-escaping — skipping" + exit 0 + ;; + esac + + info="https://proxy.golang.org/$mod/@v/$TAG.info" + + # Retry: the tag needs a moment to reach GitHub's git servers, and the + # proxy negative-caches a miss briefly. + ok="" + for attempt in 1 2 3 4 5 6; do + if curl -fsS --max-time 30 -o /dev/null "$info"; then + ok=yes + break + fi + echo "proxy has not served $TAG yet (attempt $attempt/6)" + if [ "$attempt" -lt 6 ]; then sleep 20; fi + done + + if [ -z "$ok" ]; then + echo "::warning title=pkg.go.dev::proxy.golang.org did not serve $mod@$TAG within ~2m; it will be indexed on first fetch, or force it with: curl -fsS $info" + exit 0 + fi + + # Ask for the page so pkg.go.dev builds it now instead of on its next + # index poll. The proxy fetch above is what actually publishes the + # version, so this one's status does not matter. + curl -fsS --max-time 30 -o /dev/null "https://pkg.go.dev/$mod@$TAG" || true + + echo "### Indexed on [pkg.go.dev](https://pkg.go.dev/$mod@$TAG)" >> "$GITHUB_STEP_SUMMARY" + # Make a dry run say so loudly, rather than looking like a release that silently # did nothing. dry-run-summary: diff --git a/docs/contribute/releasing.md b/docs/contribute/releasing.md index 2668f80..7a3dde2 100644 --- a/docs/contribute/releasing.md +++ b/docs/contribute/releasing.md @@ -22,7 +22,8 @@ prepare resolve the version; require it's ALREADY rolled; require CI green | -- writes nothing, anywhere -- build cross-compile all 5 CLI targets + 4 tarballs + 4 .deb/.rpm; | build the macOS .pkg; install it on a runner and uninstall it again -publish tag the tested commit, sign SHA256SUMS, publish the release +publish tag the tested commit, sign SHA256SUMS, publish the release, + index the tag on pkg.go.dev (warn-only — see below) ``` Nothing touches the repository until every artifact has been built and the @@ -164,6 +165,39 @@ Each release carries: - `SHA256SUMS` — covering everything above - `SHA256SUMS.sig` — an **ed25519** signature over `SHA256SUMS` (see below) +## pkg.go.dev + +The tag is what publishes the module. 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. + +So `publish`'s last step asks, for the tag it just pushed: + +```sh +curl -fsS "https://proxy.golang.org/github.com/behnam-rk/dezhban/@v/v0.13.0.info" +curl -fsS "https://pkg.go.dev/github.com/behnam-rk/dezhban@v0.13.0" +``` + +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. Run those two +by hand for any tag that was cut before this step existed, or that it missed. + +It **warns, it never fails**. 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 painting the run red would say it was. It also runs for rc tags: pkg.go.dev +files prereleases separately and never shows an rc as the latest version. + +**pkg.go.dev shows no documentation for dezhban, 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 that 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. + ## Unsigned artifacts, signed checksums There is no Apple Developer certificate in this project ($99/yr, and this is From d6eb627c4ca44aa38bd5b4d65fc20da558afc3b5 Mon Sep 17 00:00:00 2001 From: Behnam RK Date: Thu, 10 Sep 2026 08:23:06 +0330 Subject: [PATCH 2/6] fix(release): keep the pkg.go.dev step from failing a shipped release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/release.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d83098d..2be0e51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -582,7 +582,15 @@ jobs: TAG: ${{ needs.prepare.outputs.tag }} run: | set -eu - mod="$(go list -m)" + + # Never fatal, per the note above — which means `set -e` must not get + # a chance to kill the step either. Every command below is either + # guarded or explicitly ignored. + mod="$(go list -m 2>/dev/null || true)" + if [ -z "$mod" ]; then + echo "::warning title=pkg.go.dev::could not read the module path (go list -m); skipping" + exit 0 + fi # Proxy URLs escape every uppercase letter as `!`. This module # path is all lowercase (github.com/behnam-rk/dezhban) even though the @@ -600,18 +608,21 @@ jobs: # Retry: the tag needs a moment to reach GitHub's git servers, and the # proxy negative-caches a miss briefly. + attempts=6 + attempt=1 ok="" - for attempt in 1 2 3 4 5 6; do + while [ "$attempt" -le "$attempts" ]; do if curl -fsS --max-time 30 -o /dev/null "$info"; then ok=yes break fi - echo "proxy has not served $TAG yet (attempt $attempt/6)" - if [ "$attempt" -lt 6 ]; then sleep 20; fi + echo "proxy has not served $TAG yet (attempt $attempt/$attempts)" + attempt=$((attempt + 1)) + if [ "$attempt" -le "$attempts" ]; then sleep 20; fi done if [ -z "$ok" ]; then - echo "::warning title=pkg.go.dev::proxy.golang.org did not serve $mod@$TAG within ~2m; it will be indexed on first fetch, or force it with: curl -fsS $info" + echo "::warning title=pkg.go.dev::proxy.golang.org did not serve $mod@$TAG after $attempts attempts; it will be indexed on first fetch, or force it with: curl -fsS $info" exit 0 fi From 512b6074f9598156e819ba1ec2f7ef604a432c21 Mon Sep 17 00:00:00 2001 From: Behnam RK Date: Thu, 10 Sep 2026 08:32:52 +0330 Subject: [PATCH 3/6] fix(release): correct three false claims and widen the module-path guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/release.yml | 58 +++++++++++++++++------------------ docs/contribute/releasing.md | 23 +++++++++----- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2be0e51..cfa695d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -562,52 +562,50 @@ jobs: echo "### Published [$TAG](https://github.com/$GITHUB_REPOSITORY/releases/tag/$TAG)" >> "$GITHUB_STEP_SUMMARY" - # 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: not a release step so much as a coincidence. Asking - # for it here makes the listing deterministic. + # Ask proxy.golang.org for the tag just pushed, so the version is indexed + # deterministically rather than whenever the first person happens to fetch + # the module. Why that is worth a step, what pkg.go.dev does and does not + # show for this project, and how to do it by hand, all live in one place: + # docs/contribute/releasing.md. Do not restate any of it here. # - # Warn-only, never fatal. By this point the tag is pushed and the release - # is created, so a failure here would paint a shipped release red over a - # cache warm-up — and the proxy still picks the tag up on first fetch. - # - # NOTE: pkg.go.dev lists the module but renders NO documentation, because - # LICENSE (Hippocratic 3.0 Core plus the Dezhban named-entity restriction) - # is not on its allow-list. That is expected, not a regression: every - # package here is internal/ or a main package, so there is no importable - # surface to lose. See docs/contribute/releasing.md. + # Last in the job, and warn-only, for one reason: by this point the tag is + # pushed and the release is created, so nothing here may fail the run or + # disturb the documented path that re-runs `publish` after a failure which + # already left a tag behind. - name: Index the tag on pkg.go.dev env: TAG: ${{ needs.prepare.outputs.tag }} run: | set -eu - # Never fatal, per the note above — which means `set -e` must not get - # a chance to kill the step either. Every command below is either - # guarded or explicitly ignored. + # Warn-only means `set -e` must not get a chance to kill the step + # either, so every command below is guarded or explicitly ignored. mod="$(go list -m 2>/dev/null || true)" - if [ -z "$mod" ]; then - echo "::warning title=pkg.go.dev::could not read the module path (go list -m); skipping" - exit 0 - fi - # Proxy URLs escape every uppercase letter as `!`. This module - # path is all lowercase (github.com/behnam-rk/dezhban) even though the - # GitHub repo is Behnam-RK/dezhban, so no escaping is needed — but a - # rename that introduced a capital would 404 in silence. Say so rather - # than guess. + # One case for every module path this step cannot turn into a URL: + # "" go list failed, or printed nothing + # not one word a go.work lists every workspace module, one per line + # uppercase proxy URLs escape capitals as `!`, which this + # step does not implement. The path is all-lowercase + # github.com/behnam-rk/dezhban even though the repo is + # Behnam-RK/dezhban, so only a rename could produce one + # — and the failure mode is a silent 404, worth naming. + # `$mod` stays out of the annotation: a newline in it would truncate + # the annotation and lose the diagnostic. It goes to the log instead. case "$mod" in - *[A-Z]*) - echo "::warning title=pkg.go.dev::module path $mod has uppercase; proxy URLs need !-escaping — skipping" + "" | *[![:graph:]]* | *[[:upper:]]*) + echo "go list -m gave: $mod" + echo "::warning title=pkg.go.dev::no usable module path from go list -m; skipping" exit 0 ;; esac info="https://proxy.golang.org/$mod/@v/$TAG.info" - # Retry: the tag needs a moment to reach GitHub's git servers, and the - # proxy negative-caches a miss briefly. + # Retry because the tag needs a moment to reach GitHub's git servers. + # Keep the budget small: the proxy negative-caches a miss, so sitting + # here longer does not clear one. When it does time out, the warning + # below hands over the command to force it later. attempts=6 attempt=1 ok="" diff --git a/docs/contribute/releasing.md b/docs/contribute/releasing.md index 7a3dde2..d9f5338 100644 --- a/docs/contribute/releasing.md +++ b/docs/contribute/releasing.md @@ -22,7 +22,7 @@ prepare resolve the version; require it's ALREADY rolled; require CI green | -- writes nothing, anywhere -- build cross-compile all 5 CLI targets + 4 tarballs + 4 .deb/.rpm; | build the macOS .pkg; install it on a runner and uninstall it again -publish tag the tested commit, sign SHA256SUMS, publish the release, +publish sign SHA256SUMS, tag the tested commit, publish the release, index the tag on pkg.go.dev (warn-only — see below) ``` @@ -183,20 +183,27 @@ 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. Run those two by hand for any tag that was cut before this step existed, or that it missed. +**A version that has been indexed can never be re-cut.** The proxy and +`sum.golang.org` record its content hash permanently, so re-tagging `vX.Y.Z` at +a different commit gives every user a `checksum mismatch` security error on that +version forever. This was always true of anyone who fetched a tag; the step only +makes it certain, and immediate. It does not endanger the stranded-tag recovery +above — that happens when `publish` fails *before* this step, so the version was +never indexed — but never re-use a version number that reached a release. Bump. + It **warns, it never fails**. 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 painting the run red would say it was. It also runs for rc tags: pkg.go.dev files prereleases separately and never shows an rc as the latest version. **pkg.go.dev shows no documentation for dezhban, 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 that its detector will not match. The page still -carries the import path, the version list and the install line, with a +renders docs only for licenses on its allow-list, and this project's is not on +it (see the LICENSE header for what it is and why). 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. +practice: every package here is under `internal/` or is a `main` package, so +nothing is importable from outside the module — there is no library surface for +those docs to have described. ## Unsigned artifacts, signed checksums From a05680201a22336d4e217e2bd6f067584c5a0e8a Mon Sep 17 00:00:00 2001 From: Behnam RK Date: Thu, 10 Sep 2026 08:41:57 +0330 Subject: [PATCH 4/6] fix(release): stop the step promising remedies that do not work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .github/workflows/release.yml | 71 +++++++++++++++++++++++------------ docs/contribute/releasing.md | 29 ++++++++++---- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cfa695d..689822f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -580,33 +580,51 @@ jobs: # Warn-only means `set -e` must not get a chance to kill the step # either, so every command below is guarded or explicitly ignored. - mod="$(go list -m 2>/dev/null || true)" - - # One case for every module path this step cannot turn into a URL: - # "" go list failed, or printed nothing - # not one word a go.work lists every workspace module, one per line - # uppercase proxy URLs escape capitals as `!`, which this - # step does not implement. The path is all-lowercase - # github.com/behnam-rk/dezhban even though the repo is - # Behnam-RK/dezhban, so only a rename could produce one - # — and the failure mode is a silent 404, worth naming. - # `$mod` stays out of the annotation: a newline in it would truncate - # the annotation and lose the diagnostic. It goes to the log instead. + # stderr is deliberately kept: in a step whose only failure mode is + # silence, a toolchain error is the one line worth having. + mod="$(go list -m || true)" + + # Every module path this step cannot turn into a URL, each with its + # own reason — "unusable" is false of the uppercase one, where the + # path is fine and this step is the limitation. case "$mod" in - "" | *[![:graph:]]* | *[[:upper:]]*) - echo "go list -m gave: $mod" - echo "::warning title=pkg.go.dev::no usable module path from go list -m; skipping" - exit 0 + "" | *[![:graph:]]*) + reason="go list -m gave no single-word module path" + ;; + *[[:upper:]]*) + # The path is all-lowercase github.com/behnam-rk/dezhban even + # though the repo is Behnam-RK/dezhban, so only a rename could + # produce a capital — and a silent 404 is worth naming. + reason="module path has capitals; proxy URLs escape those as !, which this step does not implement" + ;; + *.*/*) + # A proxy path needs a host in its first element. Without this, + # `go list -m` run outside a module prints the literal + # "command-line-arguments", which is a single lowercase word and + # clears every check above. + reason="" + ;; + *) + reason="not a proxy-fetchable module path (its first element names no host)" ;; esac + if [ -n "$reason" ]; then + # $mod stays out of the annotation — a newline in it truncates the + # annotation and loses the diagnostic. It goes to the log instead. + echo "go list -m gave: $mod" + echo "::warning title=pkg.go.dev::$reason — skipping" + exit 0 + fi info="https://proxy.golang.org/$mod/@v/$TAG.info" - # Retry because the tag needs a moment to reach GitHub's git servers. - # Keep the budget small: the proxy negative-caches a miss, so sitting - # here longer does not clear one. When it does time out, the warning - # below hands over the command to force it later. - attempts=6 + # Three attempts, and no more. They cover a transient network error + # or a 5xx on the way out of the runner — NOT a tag the proxy has yet + # to see. A miss is negative-cached, so the request that loses the + # propagation race is the one that installs the cached 404, and every + # later attempt in this job only re-reads it. Sitting here longer buys + # nothing, which is why the warning below points at a wait instead. + attempts=3 attempt=1 ok="" while [ "$attempt" -le "$attempts" ]; do @@ -614,13 +632,13 @@ jobs: ok=yes break fi - echo "proxy has not served $TAG yet (attempt $attempt/$attempts)" + echo "proxy did not serve $TAG (attempt $attempt/$attempts)" attempt=$((attempt + 1)) - if [ "$attempt" -le "$attempts" ]; then sleep 20; fi + if [ "$attempt" -le "$attempts" ]; then sleep 10; fi done if [ -z "$ok" ]; then - echo "::warning title=pkg.go.dev::proxy.golang.org did not serve $mod@$TAG after $attempts attempts; it will be indexed on first fetch, or force it with: curl -fsS $info" + echo "::warning title=pkg.go.dev::proxy.golang.org did not serve $mod@$TAG. Most likely it has negative-cached the miss, which expires on its own (up to ~30 minutes); after that, curl -fsS $info indexes the version. The release itself is fine." exit 0 fi @@ -629,7 +647,10 @@ jobs: # version, so this one's status does not matter. curl -fsS --max-time 30 -o /dev/null "https://pkg.go.dev/$mod@$TAG" || true - echo "### Indexed on [pkg.go.dev](https://pkg.go.dev/$mod@$TAG)" >> "$GITHUB_STEP_SUMMARY" + # Only the proxy fetch was checked. pkg.go.dev often 404s or says + # "check back later" for a version it has not ingested yet, so do not + # claim the page is up — just link it. + echo "### Indexed on proxy.golang.org — [pkg.go.dev](https://pkg.go.dev/$mod@$TAG) follows shortly" >> "$GITHUB_STEP_SUMMARY" # Make a dry run say so loudly, rather than looking like a release that silently # did nothing. diff --git a/docs/contribute/releasing.md b/docs/contribute/releasing.md index d9f5338..b00bcb7 100644 --- a/docs/contribute/releasing.md +++ b/docs/contribute/releasing.md @@ -39,7 +39,9 @@ the tag step reuses a tag that already points at the pinned commit, so the retry picks up where it stopped, and it still refuses a tag pointing anywhere else. A *fresh* dispatch is the wrong tool there — `resolve` sees the tag and stops, by design, so if you want a clean run instead of a re-run, delete the -stranded tag first (`git push --delete origin vX.Y.Z`). +stranded tag first (`git push --delete origin vX.Y.Z`) — after checking that the +proxy has not already served that version, which would make it unusable +([pkg.go.dev](#pkggodev)). `publish` also re-checks that `main` still points at the commit `prepare` pinned. If something merged mid-release, it stops rather than tag a tree that was never @@ -183,13 +185,24 @@ 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. Run those two by hand for any tag that was cut before this step existed, or that it missed. -**A version that has been indexed can never be re-cut.** The proxy and -`sum.golang.org` record its content hash permanently, so re-tagging `vX.Y.Z` at -a different commit gives every user a `checksum mismatch` security error on that -version forever. This was always true of anyone who fetched a tag; the step only -makes it certain, and immediate. It does not endanger the stranded-tag recovery -above — that happens when `publish` fails *before* this step, so the version was -never indexed — but never re-use a version number that reached a release. Bump. +**A version the proxy has served can never be re-cut.** `sum.golang.org` records +its content hash permanently, and re-tagging `vX.Y.Z` at a different commit then +splits your users in two. On the default `GOPROXY=proxy.golang.org` they are +served the *cached original* and silently get the old tree — the worse half, because +the re-cut looks like it worked and shipped nothing. Fetching direct, they get +`checksum mismatch` / `SECURITY ERROR`. + +This step makes that certain and immediate for every release, but it was never +safe. A stranded tag that sat on origin for twenty minutes could have been +fetched by anyone in that window — one `go get …@latest` or a dependency bot is +enough. So the "delete the tag and re-dispatch" recovery above has a +precondition: ask first whether the version is already out. + +```sh +curl -fsS "https://proxy.golang.org/github.com/behnam-rk/dezhban/@v/v0.13.0.info" +``` + +Anything but a 404 means that version number is spent. Bump instead. It **warns, it never fails**. 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, From c1fe8f88972916825e92f476d6cc5f1b586d1801 Mon Sep 17 00:00:00 2001 From: Behnam RK Date: Thu, 10 Sep 2026 09:13:45 +0330 Subject: [PATCH 5/6] docs(release): describe the pkg.go.dev page as it actually renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/contribute/releasing.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/contribute/releasing.md b/docs/contribute/releasing.md index b00bcb7..d4a8a02 100644 --- a/docs/contribute/releasing.md +++ b/docs/contribute/releasing.md @@ -204,7 +204,7 @@ curl -fsS "https://proxy.golang.org/github.com/behnam-rk/dezhban/@v/v0.13.0.info Anything but a 404 means that version number is spent. Bump instead. -It **warns, it never fails**. By the time it runs, the tag is pushed and the +The step **warns, it never fails**. 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 painting the run red would say it was. It also runs for rc tags: pkg.go.dev files prereleases separately and never shows an rc as the latest version. @@ -212,9 +212,10 @@ files prereleases separately and never shows an rc as the latest version. **pkg.go.dev shows no documentation for dezhban, and that is expected.** It renders docs only for licenses on its allow-list, and this project's is not on it (see the LICENSE header for what it is and why). 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/` or is a `main` package, so +import path, the version list, and a `Directories` listing of `cmd/dezhban` and +the `internal/` tree; where the docs would be it says *Documentation not +displayed due to license restrictions*, and the module reads `License: +UNKNOWN`. Nothing is lost in practice: every package here is under `internal/` or is a `main` package, so nothing is importable from outside the module — there is no library surface for those docs to have described. From 2473ab739761de05bb715bc92036f3b86a953cb4 Mon Sep 17 00:00:00 2001 From: Behnam RK Date: Thu, 10 Sep 2026 09:20:03 +0330 Subject: [PATCH 6/6] docs(release): put the license line on the page that actually shows it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/contribute/releasing.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/contribute/releasing.md b/docs/contribute/releasing.md index d4a8a02..07b1278 100644 --- a/docs/contribute/releasing.md +++ b/docs/contribute/releasing.md @@ -211,13 +211,16 @@ files prereleases separately and never shows an rc as the latest version. **pkg.go.dev shows no documentation for dezhban, and that is expected.** It renders docs only for licenses on its allow-list, and this project's is not on -it (see the LICENSE header for what it is and why). The page still carries the -import path, the version list, and a `Directories` listing of `cmd/dezhban` and -the `internal/` tree; where the docs would be it says *Documentation not -displayed due to license restrictions*, and the module reads `License: -UNKNOWN`. Nothing is lost in practice: every package here is under `internal/` or is a `main` package, so -nothing is importable from outside the module — there is no library surface for -those docs to have described. +it (see the LICENSE header for what it is and why). The module page still +carries the import path, the version list, `License: UNKNOWN`, and a +`Directories` listing — `cmd/dezhban`, the three `tools/`, and, behind *Show +Internal Directories*, the `internal/` tree. It has no doc area at all, there +being no package at the module root. Open one of those directories, say +`cmd/dezhban`, and where its docs would be the page says *Documentation not +displayed due to license restrictions*. Nothing is lost in practice: every +package here is under `internal/` or is a `main` package, so nothing is +importable from outside the module — there is no library surface for those docs +to have described. ## Unsigned artifacts, signed checksums