writeonce/docs/guides/releasing.md
shoney.arickathil b11f964eec docs: release runbook — build, verify, publish on GitHub
- docs/guides/releasing.md: the steps from `just dist` to a working
  download button
- pins the constraint that matters: the asset filename and tag must
  match the URL /install links, or the button 404s
- includes verifying the tarball with the binaries INSIDE it, tagging
  the built commit, `gh release create` with both files, the web-UI
  path, and a curl check of the exact link the site uses
- notes dist/ is gitignored, the shoneyJ/shoneyj path-case difference,
  and what a version bump must touch in install/view.wo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 04:51:14 +02:00

5.1 KiB

Cutting a release — building the tarball and publishing it on GitHub

The /install page on writeonce.de links a GitHub release asset by an exact URL. Publishing is therefore not "upload a file somewhere": the asset filename has to match what the site links, or the download button 404s. This runbook keeps the two in step.

The URL the site links today:

https://github.com/shoneyj/writeonce/releases/download/v0.1.0/writeonce-0.1.0-linux-amd64.tar.gz

which decomposes as <repo>/releases/download/<tag>/<asset-name>. So the tag must be v0.1.0 and the asset must be named exactly writeonce-0.1.0-linux-amd64.tar.gz — which is what just dist already produces.

0. Before you start

gh must be authenticated. It is an interactive browser/device flow, so run it yourself:

gh auth login

In a Claude Code session, type it with a leading ! so the output lands in the conversation: ! gh auth login.

Check the tree is clean and the version is what you mean to ship — VERSION is the single source, and mkdist.sh refuses to build if woc version or wovm --version disagree with it:

git status --porcelain     # expect empty
cat VERSION                # e.g. 0.1.0

1. Build the artifact

just dist

That builds both release binaries, runs the version drift guard, and writes two files:

dist/writeonce-<ver>-linux-amd64.tar.gz
dist/writeonce-<ver>-linux-amd64.tar.gz.sha256

2. Verify it before anyone else can

Two checks, both cheap, both worth it. First the digest:

cd dist && sha256sum -c writeonce-0.1.0-linux-amd64.tar.gz.sha256 && cd ..

Then prove the tarball actually works, using the binaries INSIDE it — not the ones in your build tree:

tmp=$(mktemp -d)
tar -C "$tmp" -xzf dist/writeonce-0.1.0-linux-amd64.tar.gz
export PATH="$tmp/writeonce/bin:$PATH"
woc version && wovm --version

mkdir -p "$tmp/hello" && cd "$tmp/hello"
printf 'name    = "hello"\nversion = "0.1.0"\n\n[runtime]\nwo = ">= 0.1"\n' > wo.toml
printf 'fn main() -> Int {\n  print("hello, writeonce");\n  return 0;\n}\n' > main.wo
woc . && ./target/hello

If that prints hello, writeonce, the release is sound.

3. Tag the commit

The tag is what the download URL points at, so tag the exact commit the binaries were built from:

git tag -a v0.1.0 -m "writeonce 0.1.0"
git push origin v0.1.0

4. Publish and upload

One command creates the release and attaches both files:

gh release create v0.1.0 \
  dist/writeonce-0.1.0-linux-amd64.tar.gz \
  dist/writeonce-0.1.0-linux-amd64.tar.gz.sha256 \
  --title "writeonce 0.1.0" \
  --notes-file - <<'EOF'
Linux x86-64, glibc 2.38 or newer. Two binaries — `woc` and `wovm` —
depending only on the system C library.

    rm -rf /usr/local/writeonce
    tar -C /usr/local -xzf writeonce-0.1.0-linux-amd64.tar.gz
    export PATH=$PATH:/usr/local/writeonce/bin

Verify with the published `.sha256` before extracting.
EOF

Add --draft to stage it without publishing, or --prerelease to mark it as one. If the release already exists and you only need to attach (or replace) files:

gh release upload v0.1.0 dist/writeonce-0.1.0-linux-amd64.tar.gz --clobber

Without gh, the same thing through the web UI: the repo's Releases page → Draft a new release → choose tag v0.1.0 → drag both files into the attachment box → Publish release. Uploading by hand is where the filename usually drifts, so paste it rather than retyping it.

curl -sIL -o /dev/null -w '%{http_code} %{url_effective}\n' \
  https://github.com/shoneyj/writeonce/releases/download/v0.1.0/writeonce-0.1.0-linux-amd64.tar.gz

A 200 means /install's download button works for a stranger. Anything else means the tag, the asset name, or the repo path disagrees with the link in docs/examples/site/install/view.wo.

6. Refresh the site's mirror

/install offers this site as a mirror beside the GitHub link, served by the framework's StaticFiles out of $WO_DIST (default ./dist). Copy the same two files there so the mirror is not stale:

scp dist/writeonce-0.1.0-linux-amd64.tar.gz* user@host:/srv/writeonce/dist/

Both copies are the same bytes, and the published .sha256 covers either one — that is the point of shipping the digest beside the archive.

Notes

  • dist/ is gitignored (.gitignore:103). The tarball is a build artifact; the release is where it lives, not the repository.
  • Repo path case. The git remote is shoneyJ/writeonce while the site links shoneyj/writeonce. GitHub paths are case-insensitive and redirect, so both resolve — but keep them consistent when either changes.
  • Bumping the version means editing VERSION, rebuilding, and updating every place the site names the current release: docs/examples/site/install/view.wo (the download links, the tar command, and the .sha256 link). The version appears there as literal text, so grep for the old number before you publish.
  • One target today. mkdist.sh builds linux-amd64 only; a cross matrix is future work. Do not add architectures to the release notes that no build produces.