From 463f897e5ff47941e9072073ae4ef70bf1d10a42 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 25 Aug 2026 04:58:40 +0200 Subject: [PATCH] =?UTF-8?q?ci:=20release=20workflow=20=E2=80=94=20no=20gh?= =?UTF-8?q?=20auth=20login,=20tag-triggered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .github/workflows/release.yml: builds, verifies and publishes on a `v*` tag. `permissions: contents: write` on the injected GITHUB_TOKEN replaces `gh auth login`; no PAT, nothing to rotate - runs-on ubuntu-22.04 DELIBERATELY: the build host's glibc caps which symbol versions the binaries import, and that cap is the floor every user needs. 22.04 (2.35) includes Ubuntu 22.04 / Debian 12 / RHEL 9; 24.04 (2.39) would exclude them - guards that fail instead of publishing: tag vs VERSION, produced asset name vs the filename /install links, sha256, and a smoke test that builds a hello project with the binaries INSIDE the tarball - reports the shipped glibc floor so the claim on /install is checkable from a build log - releasing.md: pipeline route up front, manual route kept; GH_TOKEN recipe for non-GitHub CI Not run — this repo has no CI history and Actions cannot execute locally. Every guard's shell was dry-run here against the real dist. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 119 ++++++++++++++++++++++++++++++++++ docs/guides/releasing.md | 57 +++++++++++++++- 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7a18305 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,119 @@ +# NOTE: this workflow has never run. Authored 2026-08-25 and not +# executable locally — the first real tag push is its first test. +# Expect to adjust the toolchain step if the pinned OCaml/dune version +# is not available on the runner image. + +name: release + +# Fires only on a version tag, so nothing is published by an ordinary +# push. `workflow_dispatch` is the manual escape hatch for a re-run. +on: + push: + tags: + - 'v*' + workflow_dispatch: + +# The ONE line that replaces `gh auth login`: it widens the automatic +# GITHUB_TOKEN so this job may write releases. No PAT, no secret to +# rotate, and the token dies with the job. +permissions: + contents: write + +jobs: + release: + # DELIBERATE, not a default. The release binaries link glibc + # dynamically, so the build host's glibc caps which symbol versions + # they can import — and that cap becomes the minimum glibc every + # user needs. Built on 24.04 (glibc 2.39) the floor is 2.39; + # built here on 22.04 (2.35) it is 2.35, which is the difference + # between excluding and including Ubuntu 22.04, Debian 12 and + # RHEL 9. Raise this image only with a reason, and update the + # supported-systems list in docs/examples/site/install/view.wo in + # the same change. + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + # The compiler is OCaml stdlib only — no opam packages — so this + # step exists purely to get a compiler and dune onto the runner. + - uses: ocaml/setup-ocaml@v3 + with: + ocaml-compiler: '4.14' + + # The tag is the release's identity; VERSION is what the binaries + # report. If they disagree the download URL would name a version + # nobody can install. mkdist.sh already guards VERSION against the + # binaries; this guards the tag against VERSION. + - name: Tag must match VERSION + run: | + tag="${GITHUB_REF_NAME#v}" + ver="$(cat VERSION)" + [ "$tag" = "$ver" ] || { + echo "tag $GITHUB_REF_NAME does not match VERSION $ver" >&2 + exit 1 + } + + - name: Build the tarball + run: ./scripts/mkdist.sh + + # The site links one exact filename. If mkdist ever changes its + # naming, the download button 404s for every visitor — so fail + # here instead. + - name: Asset name must match what the site links + run: | + ver="$(cat VERSION)" + asset="writeonce-${ver}-linux-amd64.tar.gz" + test -f "dist/$asset" + grep -q "$asset" docs/examples/site/install/view.wo || { + echo "$asset is not the filename /install links" >&2 + exit 1 + } + + - name: Verify the digest + run: cd dist && sha256sum -c "writeonce-$(cat ../VERSION)-linux-amd64.tar.gz.sha256" + + # Prove the ARTEFACT works, using the binaries inside it rather + # than the ones just built in the tree. This is what catches a + # tarball that packaged the wrong thing. + - name: Smoke-test the extracted toolchain + run: | + ver="$(cat VERSION)" + tmp="$(mktemp -d)" + tar -C "$tmp" -xzf "dist/writeonce-${ver}-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 . + out="$(./target/hello)" + [ "$out" = "hello, writeonce" ] || { echo "got: $out" >&2; exit 1; } + + # Record the real glibc floor of what is about to ship, so the + # claim on /install can be checked against a build log rather + # than trusted. + - name: Report the glibc floor + run: | + ver="$(cat VERSION)" + tmp="$(mktemp -d)" + tar -C "$tmp" -xzf "dist/writeonce-${ver}-linux-amd64.tar.gz" + for b in "$tmp"/writeonce/bin/*; do + printf '%s needs %s\n' "$(basename "$b")" \ + "$(objdump -T "$b" | grep -oE 'GLIBC_[0-9.]+' | sort -uV | tail -1)" + done + + # gh is preinstalled on GitHub runners and reads GH_TOKEN from the + # environment, so there is no `gh auth login` anywhere in this file. + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + run: | + ver="$(cat VERSION)" + gh release create "$GITHUB_REF_NAME" \ + "dist/writeonce-${ver}-linux-amd64.tar.gz" \ + "dist/writeonce-${ver}-linux-amd64.tar.gz.sha256" \ + --title "writeonce ${ver}" \ + --generate-notes diff --git a/docs/guides/releasing.md b/docs/guides/releasing.md index d747f20..d57adce 100644 --- a/docs/guides/releasing.md +++ b/docs/guides/releasing.md @@ -16,7 +16,62 @@ 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. Authenticate `gh` (once per machine) +## Two routes + +**Automated (preferred).** `.github/workflows/release.yml` builds, +verifies and publishes on a `v*` tag push. It needs no `gh auth login` +and no secret: GitHub injects a per-job `GITHUB_TOKEN`, and the single +line `permissions: contents: write` is what lets that token create a +release. The token expires when the job ends, so there is nothing to +rotate or leak. Skip to *Releasing from the pipeline* below. + +**Manual.** Everything from §0 onward — the path for a first release, or +when the pipeline is broken and you need to ship anyway. + +## Releasing from the pipeline + +``` +git tag -a v0.1.0 -m "writeonce 0.1.0" +git push origin v0.1.0 +``` + +That is the whole release. The workflow then, in order: checks the tag +matches `VERSION`, builds via `scripts/mkdist.sh`, asserts the produced +filename is the one `/install` links, verifies the `.sha256`, extracts +the tarball and builds a hello project **with the binaries inside it**, +prints the glibc floor of what is about to ship, and publishes both +files with `gh release create`. + +Two things about that file are deliberate: + +- **`runs-on: ubuntu-22.04`, not `ubuntu-latest`.** The binaries link + glibc dynamically, so the build host's glibc caps the symbol versions + they can import — and that cap becomes the minimum glibc every user + needs. On 24.04 (glibc 2.39) the floor is 2.39; on 22.04 (2.35) it is + 2.35. That is the difference between excluding and including Ubuntu + 22.04, Debian 12 and RHEL 9. Changing the image changes who can run + the release, so change `install/view.wo`'s supported-systems list in + the same commit. +- **It fails rather than publishes** when the tag, `VERSION` and the + asset name disagree, because those three are what the download URL on + `/install` is built from. + +### Other CI (GitLab, Jenkins, Buildkite) + +No `gh auth login` there either — `gh` reads a token from the +environment: + +``` +GH_TOKEN=$MY_SECRET gh release create v0.1.0 dist/*.tar.gz dist/*.sha256 +``` + +The secret is a PAT with `repo` scope (classic) or **Contents: read and +write** (fine-grained). Outside GitHub it is a long-lived credential you +own and must rotate — which is exactly the cost `GITHUB_TOKEN` avoids, +and the reason to prefer Actions for this one job even if the rest of +your CI lives elsewhere. + +## 0. Authenticate `gh` (manual route only) `gh` keeps its own credential, separate from git's. SSH keys let you `git push`; they do **not** let `gh` call the API, so a machine that