ci: release workflow — no gh auth login, tag-triggered
- .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) <noreply@anthropic.com>
This commit is contained in:
parent
6c3e8af7ea
commit
cfe81cf8d0
2 changed files with 175 additions and 1 deletions
119
.github/workflows/release.yml
vendored
Normal file
119
.github/workflows/release.yml
vendored
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue