release: publish the tarball on a tag, and stop a tag meaning nothing - #9
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A tag produced an image and nothing else.
make releasebuilt 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, aninstall.shno suite runs.The version is not in the tag
It is
(version …)indune-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 taggingv0.2.0without bumpingdune-projectpublishes an image tagged0.1.0, a tarball named0.1.0and a binary that reports0.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.shdune-project, and that it goes forward. Run byrelease.ymlandimage-publish.ymlbefore either builds, and by hand before pushing a tag.scripts/check-versions.shwrit.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.shWhat now ships on a tag
release.ymlchecks 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.ymlis that build, once, as a called workflow: the shapeimage-build.ymlalready established here, for the same reason.ci.ymlruns it on every pull request andrelease.ymlruns it on a tag, so the thing checked on the way in is the thing that ships. It builds on anx86_64and anaarch64runner natively, then unpacks the tarball, installs it into an empty prefix and uses it from a third directory —writ --versionagainstdune-project, a model, a.rulesfile, and all three binaries includingwrit-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.ymlruns build, suites,ocamlformat, the version checks, and a check thatwrit.opamstill matches the file it is generated from.Not in this PR
No tag is cut and none is touched.
dune-projectstill says0.1.0; README now documents the three steps that make a release.🤖 Generated with Claude Code