Skip to content

release: publish the tarball on a tag, and stop a tag meaning nothing - #9

Merged
sajonaro merged 7 commits into
mainfrom
release-tarballs-and-tags
Sep 5, 2026
Merged

release: publish the tarball on a tag, and stop a tag meaning nothing#9
sajonaro merged 7 commits into
mainfrom
release-tarballs-and-tags

Conversation

@sajonaro

@sajonaro sajonaro commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

A tag produced an image and nothing else. make release built a portable tarball on somebody's laptop, where it stayed — this repository has no GitHub releases at all, and the tarball is the artifact with the largest surface and the least coverage: a build profile nothing else uses (static), a stdlib copy by a path only it knows, an install.sh no suite runs.

The version is not in the tag

It is (version …) in dune-project, and every artifact reads it from there — the tarball's name, writ --version, the image tag, the opam package. The tag only fires the workflow. So tagging v0.2.0 without bumping dune-project publishes an image tagged 0.1.0, a tarball named 0.1.0 and a binary that reports 0.1.0, under a release called v0.2.0. That is not a hypothetical — it is the default outcome of tagging, because the bump is a separate act nothing forced.

scripts/check-release-tag.sh the tag's shape, its agreement with dune-project, and that it goes forward. Run by release.yml and image-publish.yml before either builds, and by hand before pushing a tag.
scripts/check-versions.sh the three files that keep a hand-written copy of the version because nothing substitutes into them: writ.opam, the Claude plugin's manifest, and the image tag that plugin pulls — the one whose staleness is invisible, since an old image runs perfectly and just answers as an older writ.
scripts/test-check-release-tag.sh the rule itself, against cases rather than against whatever this checkout's tags happen to be.

What now ships on a tag

release.yml checks the tag, builds a tarball per architecture, and attaches both plus their checksums to a GitHub release whose body opens with the tag's own annotation — the line someone deciding whether to upgrade actually reads — followed by the three install routes.

tarball-build.yml is that build, once, as a called workflow: the shape image-build.yml already established here, for the same reason. ci.yml runs it on every pull request and release.yml runs it on a tag, so the thing checked on the way in is the thing that ships. It builds on an x86_64 and an aarch64 runner natively, then unpacks the tarball, installs it into an empty prefix and uses it from a third directorywrit --version against dune-project, a model, a .rules file, and all three binaries including writ-lsp, which the image does not carry and nothing else checks. It also asserts the link came out static, which is the only place README's "no libc version floor" claim can fail: a dynamically linked binary works perfectly on the machine that built it.

And a CI workflow, which did not exist

Nothing in CI ran make test — the only workflows were the image ones and the CLA bot, so a pull request could turn every suite red behind a green docker build. A release is cut from a tag on main, and a tag is only as good as what main was in. ci.yml runs build, suites, ocamlformat, the version checks, and a check that writ.opam still matches the file it is generated from.

Not in this PR

No tag is cut and none is touched. dune-project still says 0.1.0; README now documents the three steps that make a release.

🤖 Generated with Claude Code

sajonaro and others added 7 commits September 5, 2026 23:42
A tag produced an image and nothing else. `make release` built a portable
tarball on somebody's laptop, where it stayed: this repository has no GitHub
releases at all, and the artifact with the largest surface — a build profile
nothing else uses, a stdlib copy by a path only it knows, an install.sh no
suite runs — was first exercised by whoever downloaded it.

THE VERSION IS NOT IN THE TAG. It is `(version …)` in dune-project, and every
artifact reads it from there; the tag only fires the workflow. So tagging
v0.2.0 without bumping dune-project publishes an image tagged 0.1.0, a tarball
named 0.1.0 and a binary that reports 0.1.0, under a release called v0.2.0 —
not a hypothetical, but the DEFAULT outcome of tagging, because the bump is a
separate act nothing forced.

  scripts/check-release-tag.sh   the tag's shape, its agreement with
                                 dune-project, and that it goes forward. Run by
                                 release.yml and image-publish.yml before either
                                 builds, and by hand before pushing a tag.
  scripts/check-versions.sh      the three files that keep a hand-written copy
                                 of the version because nothing substitutes into
                                 them: writ.opam, the Claude plugin's manifest,
                                 and the image tag that plugin PULLS — the one
                                 whose staleness is invisible, since an old
                                 image runs perfectly and answers as an older
                                 writ.
  scripts/test-check-release-tag.sh  the rule itself, against cases rather than
                                 against whatever this checkout's tags are.

tarball-build.yml is the build, once, as a called workflow — the shape
image-build.yml already established here, for the same reason: ci.yml runs it on
every pull request and release.yml runs it on a tag, and two copies of a recipe
is how the check stops shadowing the publish. It builds on an x86_64 and an
aarch64 runner natively, then UNPACKS the tarball, installs it into an empty
prefix and uses it from a third directory — `writ --version` against
dune-project, a model, a .rules file, and all three binaries including writ-lsp,
which the image does not carry and nothing else checks. It also asserts the link
came out static, which is the only place README's "no libc version floor" claim
can fail: a dynamically linked binary works perfectly on the machine that built
it.

ci.yml is new too. Nothing in CI ran `make test` — the only workflows were the
image ones and the CLA bot, so a pull request could turn every suite red behind
a green docker build, and a release is cut from a tag on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`make lint` had never run in CI — there was no CI — so four files drifted from
ocamlformat and the only way to find out was to run it by hand. This is `dune
build @fmt --auto-promote` and nothing else: line wrapping, and one
(libraries …) list broken across lines. No behaviour changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`writ --version` prints the version and then three lines of licence, so the
check compared four lines against one and failed on both architectures with an
error message that quoted the whole banner. The tarball itself was fine — it
built, installed, and ran.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…0.2.0

Found by asking why the repository carries a tag `0.1.0` without its `v`. Its
annotation says it exists so that `dune subst` yields a clean version in an
opam build — and that turned out to describe a real mechanism the project had
since stopped benefiting from, because `git describe` picks `v0.1.0` (68
commits away) over `0.1.0` (133), and has done since v0.1.0 was created.

The mechanism, staged and measured rather than reasoned about:

  a clone at tag v9.9.9, `dune subst`, build
    dune-project   (version v9.9.9)     <- subst writes the tag VERBATIM
    writ.opam      version: "9.9.9"     <- opam strips the v for itself
    writ --version writ v9.9.9
    the tarball    writ 9.9.9

writ.opam runs `["dune" "subst"] {dev}`, and dev mode is every `opam pin` —
the install route README lists FIRST. So one release answered "which writ is
this" two different ways depending on how it was installed, and inside the pin
the binary disagreed with the package metadata it was installed from.

The generated version module now strips a leading `v` before a digit. Same
clone, same tag, after: `writ 9.9.9`. A dev pin still reports
0.1.0-68-gb054efa — not a release version, and not pretending to be one.

ci.yml stages that situation on every pull request, because no ordinary build
runs `dune subst` at all: nothing else in this repository would ever notice
the strip breaking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first RELEASED version, and it is 0.2.0 rather than 0.1.0 because 0.1.0 is
already spoken for: `v0.1.0` tags a commit from before most of this existed,
and ghcr.io/writ-lang/writ:0.1.0 has been published from it. Re-cutting that
number would move a published image tag under anyone who had pinned it —
including this repository's own Claude plugin, which pins it deliberately, in
plugins/writ/bin/writ-mcp. 0.2.0 costs nothing and is unambiguous.

The bump is one line in dune-project and three copies that nothing substitutes
into — writ.opam (regenerated), the plugin manifest, and the image tag the
plugin pulls. `scripts/check-versions.sh` is what makes that a checked list
rather than a remembered one; it is why this commit is not the usual
half-finished bump.

Note the plugin's pin points at ghcr.io/writ-lang/writ:0.2.0, which does not
exist until v0.2.0 is tagged and image-publish.yml has run. That window is
inherent to a deliberate pin: the alternative is a plugin whose pin lags its
own release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
setup-ocaml creates a switch local to the workspace, and opam resolves a local
switch from the current directory — so the moment the step cd'd into a clone
under $RUNNER_TEMP, `opam exec` failed with "No switch is currently set".
Name the switch while it can still be found, and set it in the clone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sajonaro
sajonaro merged commit 24fd9b0 into main Sep 5, 2026
6 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant