diff --git a/.beans/ATFS-78ea--changeset-driven-versioning-changeset-directory-kn.md b/.beans/ATFS-78ea--changeset-driven-versioning-changeset-directory-kn.md new file mode 100644 index 0000000..c9742cd --- /dev/null +++ b/.beans/ATFS-78ea--changeset-driven-versioning-changeset-directory-kn.md @@ -0,0 +1,58 @@ +--- +# ATFS-78ea +title: 'Changeset-driven versioning: .changeset directory + knope, PR check for missing changesets' +status: completed +type: feature +priority: normal +created_at: 2026-08-24T09:03:06Z +updated_at: 2026-08-24T09:07:36Z +--- + +atfs's release version and `CHANGELOG.md` are currently picked by hand as +part of the ATFS-bq8m tag ritual. Track them instead: a `.changeset/*.md` +file per pull request, in the same format the Node.js `changesets` tool +uses, read by [knope](https://knope.tech) to compute the next version and +changelog entry. + +Fits cleanly because atfs already has no version file to bump — it resolves +its own version purely from the nearest git tag (ATFS-72r8's `git:v*.*.*` +adoption) — so knope's single unnamed `[package]` just derives its current +version from the last `vX.Y.Z` tag and knows to produce the next one in the +same plain format. Knope's only forge-aware feature (creating a release on +GitHub/Gitea) is irrelevant here: Tangled isn't either, `knope.toml` +configures no forge, and `knope release` just tags the commit locally — +exactly what the existing manual `git tag && push` ritual already does. + +Two deliberate scope decisions (see the questions this bean answered): +release cutting stays manual and local (`knope release`, run by a +maintainer) rather than a Tangled-workflow or PR-bot flow — Tangled has no +cron and no bot-account precedent here, and this only replaces the +"what's the next version" guesswork in ATFS-bq8m's ritual, not the ritual +itself. And the missing-changeset check ships now, in CI, rather than as +convention-only. + +- [x] `.changeset/README.md` — the convention, bump-type format, and what's + exempt +- [x] `knope.toml` — single unnamed package, no versioned_files, no forge, + conventional commits disabled (changesets only) +- [x] `.tangled/workflows/changeset-check.yml` — fails a PR that touches + release-affecting files with no new `.changeset/*.md` +- [x] CLAUDE.md — the release-ritual bullet and a sharp-edges note + +## Summary of Changes + +Added the `.changeset` directory and its README describing the convention +(bump type + description per PR, `atfs` as the sole package name, and the +exempt paths — `.beans/`, `.tangled/`, `web/`, docs). Added `knope.toml` +configuring `atfs` as an unnamed single package with no versioned files +(version comes purely from the last release tag, matching ATFS-72r8), +`ignore_conventional_commits = true` under `[changes]` so only changesets +drive version bumps, and deliberately no `[github]`/`[gitea]` forge block +since Tangled isn't a forge knope knows — `knope release` just tags the +commit locally, feeding straight into the existing ATFS-bq8m tag-push +ritual. Added `.tangled/workflows/changeset-check.yml`, a pull-request check +that unshallows and fetches `main` (the same guarded pattern image.yml uses +for tag history), diffs the PR against it, and fails if any non-exempt file +changed with no new `.changeset/*.md` added. Documented both in CLAUDE.md: +a clause on the existing release-ritual bullet, and a new sharp-edges bullet +on knope's forge limitation and the fetch-main trick. diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..d2173f1 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,55 @@ +# Changesets + +This directory tracks what's changed since the last release, one file per +change, so that the next release's version number and changelog entry are +computed rather than picked by hand. It's read by [knope](https://knope.tech) +(`knope.toml` at the repo root configures it) and the format is the same one +the Node.js [changesets](https://github.com/changesets/changesets) tool uses. + +## Adding one + +Every pull request that touches release-affecting code (see the CI check +below for exactly what that means) needs a new file here. Easiest way: + +```sh +knope document-change +``` + +which prompts for a bump type and description and writes the file for you. +By hand, it's a markdown file with a frontmatter block naming the package and +bump type, then the changelog entry: + +```markdown +--- +atfs: minor +--- + +`atfs init` gained interactive prompts and can write the server record. +``` + +`atfs` is the only package here (a single Go module, no per-package version +files — see `knope.toml`), so every changeset names it. Bump type is one of +`major`, `minor`, `patch`. The body is exactly what will appear in +`CHANGELOG.md`. + +## What doesn't need one + +Changes confined to `.beans/`, `.tangled/`, `web/` (the site deploys +continuously on every push to main, independent of release tags), or `*.md` +docs don't affect what a release contains, so they're exempt — see +`.tangled/workflows/changeset-check.yml` for the exact rule the CI check +enforces. Anything else — `cmd/`, `internal/`, `lexicons/`, `Dockerfile`, +`hack/`, `go.mod` — needs one, even a one-line `patch` changeset for a purely +internal fix with no user-visible effect. + +## Cutting a release + +A maintainer runs `knope release` locally when it's time to cut one — it +reads every file in this directory plus any Conventional Commits, computes +the next version, writes the `CHANGELOG.md` entry, deletes the consumed +changeset files, commits, and tags. From there it's the existing release +ritual (bean ATFS-bq8m): push the tag, and `.tangled/workflows/*.yml` take it +from there exactly as they do for a hand-picked tag. Knope never talks to +Tangled itself — it only knows GitHub and Gitea as forges, so `knope.toml` +configures none, and `knope release` just tags the commit locally, same as +`git tag vX.Y.Z && git push --tags` always has. diff --git a/.tangled/workflows/changeset-check.yml b/.tangled/workflows/changeset-check.yml new file mode 100644 index 0000000..42500dc --- /dev/null +++ b/.tangled/workflows/changeset-check.yml @@ -0,0 +1,59 @@ +# Fails a pull request that changes release-affecting files without adding +# a .changeset/*.md file — see that directory's README for the convention +# this enforces and knope.toml for how a release actually consumes them. +# Pure git/shell: the check itself never needs knope installed, only a +# maintainer cutting a release does (locally, per the README). +when: + - event: ["pull_request"] + branch: ["main"] + +engine: "nixery" + +dependencies: + nixpkgs: + - git + +steps: + # Spindle's clone is a fixed --depth=1 fetch of the trigger commit alone + # (tangled-core's spindle/models/clone.go) — no other branches, so `main` + # itself has to be fetched before it can be diffed against. Same + # unshallow-then-fetch shape as image.yml's tag resolution, guarded the + # same way since --unshallow errors on an already-complete clone. + - name: "changeset required" + command: | + set -euo pipefail + + if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then + git fetch --unshallow origin + fi + git fetch origin main:refs/remotes/origin/main + + base="$(git merge-base refs/remotes/origin/main HEAD)" + changed="$(git diff --name-only "$base" HEAD)" + + # These paths are exempt because none of them affect what a release + # contains: .beans/ and .tangled/ are process/CI, web/ deploys + # continuously on every push to main independent of release tags + # (CLAUDE.md's "Repo & deployment"), and docs carry no version. + needs_changeset=false + while IFS= read -r f; do + [ -z "$f" ] && continue + case "$f" in + .beans/*|.tangled/*|web/*|*.md|.changeset/*) ;; + *) needs_changeset=true ;; + esac + done <<< "$changed" + + if [ "$needs_changeset" = false ]; then + echo "no release-affecting files changed — no changeset required" + exit 0 + fi + + added="$(git diff --name-only --diff-filter=A "$base" HEAD -- '.changeset/*.md' | grep -v '/README\.md$' || true)" + if [ -z "$added" ]; then + echo "This PR changes release-affecting files but adds no .changeset/*.md file." + echo "Run 'knope document-change' locally (or add one by hand, per .changeset/README.md) describing the change and its version-bump type." + exit 1 + fi + + echo "changeset present: $added" diff --git a/CLAUDE.md b/CLAUDE.md index 9ed7b3d..1e70ae2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -517,7 +517,18 @@ by an ingress, downloads never are. needs tag history — gosd-build.toml's `git:v*.*.*` app version among them — has to fetch it itself: `git fetch --unshallow --tags origin` (guarded, since `--unshallow` errors on an already-complete clone), - which image.yml now does before `gosd build`. + which image.yml now does before `gosd build`. `changeset-check.yml` hits + the same wall diffing a PR against `main` — no branch refs means `main` + itself isn't there to diff against — so it unshallows the same way and + then fetches `main` explicitly by refspec before taking `merge-base`. +- **Knope knows GitHub and Gitea as forges; Tangled isn't either.** That's + fine here — `knope.toml` configures no forge at all, so `knope release` + never tries to talk to one, it just tags the commit locally, which is all + the existing tag-push release ritual (ATFS-bq8m) needs. Leaving the forge + config out also stops knope auto-detecting one from the git remote, which + would otherwise try (and fail) against Tangled. See `.changeset/README.md` + and `knope.toml`'s own comments for the rest of the versioning setup + (ATFS-78ea). - **Nix expands a bare `?ref=` to `refs/heads/`**, so a flake ref pinned to a *tag* must spell it out: `?ref=refs/tags/vX.Y.Z`, never `?ref=vX.Y.Z`, which hunts for a branch of that name and dies with `couldn't find remote @@ -584,7 +595,17 @@ by an ingress, downloads never are. import, like atbackup's `backup.ist`). - Hosted on **Tangled**; CI lives in `.tangled/workflows/*.yml`. Releases are cut by pushing a `v*` tag (see bean ATFS-bq8m for the full ritual and - the runner constraints it discovered). + the runner constraints it discovered). **What version to tag and what + `CHANGELOG.md` says is computed, not picked by hand**: every PR that + touches release-affecting code adds a `.changeset/*.md` file (convention + and exemptions in that directory's own README; + `.tangled/workflows/changeset-check.yml` enforces it), and a maintainer + runs `knope release` locally when it's time — it reads the pending + changesets, writes the changelog entry, commits, and tags, feeding + straight into the ritual above. `knope.toml` deliberately configures no + forge: knope only speaks GitHub and Gitea, not Tangled, so it just tags + the commit locally, exactly as the manual `git tag && push` ritual always + has (ATFS-78ea). - **The CLI's distribution channel is `flake.nix`** — the daemon ships as a container and an SD-card image, but `atfs` itself isn't in either (the Dockerfile builds `atfsd` alone) and `go install` can't reach it behind the diff --git a/knope.toml b/knope.toml new file mode 100644 index 0000000..ed3e1f3 --- /dev/null +++ b/knope.toml @@ -0,0 +1,24 @@ +# Drives release versioning from .changeset/*.md files exclusively — see +# that directory's README for the convention. Conventional Commits are +# disabled as a version-bump source: without this, an ordinary "fix:"/"feat:" +# commit message would also compute a bump, defeating the changeset-only +# tracking .tangled/workflows/changeset-check.yml enforces on every PR. +[changes] +ignore_conventional_commits = true + +# A single Go module, no per-package version file — atfs already resolves +# its own version purely from the nearest git tag at build time (gosd's +# `--app-version git:v*.*.*`, CLAUDE.md's "adopt gosd's ... app version" +# note), so this package has no versioned_files: its current version is +# just the last `vX.Y.Z` release tag, and `knope release` tags the next one +# the same way — a single unnamed [package] gets the plain `vX.Y.Z` tag +# format, matching every tag this repo already has. +[package] +changelog = "CHANGELOG.md" + +# Deliberately no [github]/[gitea] block: atfs is hosted on Tangled, which +# knope doesn't speak (it only knows GitHub and Gitea as forges). Leaving +# this file's forge config empty also stops knope auto-detecting one from +# the git remote — with no forge configured, `knope release` just tags the +# commit locally, which is all it needs to do: pushing that tag is already +# the release trigger `.tangled/workflows/*.yml` act on (bean ATFS-bq8m).