Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,96 @@ jobs:

echo "### Published [$TAG](https://github.com/$GITHUB_REPOSITORY/releases/tag/$TAG)" >> "$GITHUB_STEP_SUMMARY"

# 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.
#
# 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

# Warn-only means `set -e` must not get a chance to kill the step
# either, so every command below is guarded or explicitly ignored.
# 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:]]*)
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 !<lower>, 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"

# 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
if curl -fsS --max-time 30 -o /dev/null "$info"; then
ok=yes
break
fi
echo "proxy did not serve $TAG (attempt $attempt/$attempts)"
attempt=$((attempt + 1))
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. 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

# 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

# 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.
dry-run-summary:
Expand Down
62 changes: 60 additions & 2 deletions docs/contribute/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 sign SHA256SUMS, tag the tested commit, 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
Expand All @@ -38,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
Expand Down Expand Up @@ -164,6 +167,61 @@ 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.

**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.

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.

**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 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

There is no Apple Developer certificate in this project ($99/yr, and this is
Expand Down