From 79e77fadc657f8f46fb942b7dc22c01df89a6ea9 Mon Sep 17 00:00:00 2001 From: Niklas Ye Date: Thu, 24 Sep 2026 08:38:02 +0200 Subject: [PATCH] Let the release skill drive this repo, and let make drive the pipeline The release skill only knew repos that deploy an image through a wrapper chart. terdut-tui publishes binaries to a Gitea release and nothing else, so its first two releases were cut by hand. It now has a .release.conf saying KIND=binary, which the skill treats as gate, tag, wait for the pipeline, then check what was published. The gate had to exist as make targets for that: fmt, lint and test, the same three the other repos have. ci.yaml and release.yaml now call them instead of carrying their own copy of gofmt, vet and the tests, so a green gate locally and a green pipeline are the same code and cannot drift. The gofmt handling moved over as written, including the comment on why both of its failure modes need catching; both fail the target, checked with a misformatted file and an unparseable one. The binaries job calls make dist too. DIST_TARGETS is now the one place that says what a release contains, and dist-assets prints the names dist builds so the skill can verify the published release against a list instead of a count. The names are unchanged, and they are the self-updater's contract with every installed binary: internal/updater matches terdut-tui--- exactly. CLAUDE.md gains a Release section, including that the annotated tag's message is what appears on the release page. Not run in the pipeline yet: make is in the golang image, as terdut-server's CI relies on, but this repo's workflows only exercise it on the push that carries this commit, and make dist only on the next tag. A failure in the release workflow's test job stops the publish rather than shipping something unchecked. --- .gitea/workflows/ci.yaml | 36 +++------------------- .gitea/workflows/release.yaml | 53 ++++++-------------------------- .gitignore | 1 + .release.conf | 9 ++++++ CLAUDE.md | 19 ++++++++++++ Makefile | 58 +++++++++++++++++++++++++++++++++-- 6 files changed, 99 insertions(+), 77 deletions(-) create mode 100644 .release.conf diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 7643857..d342c82 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -56,34 +56,8 @@ jobs: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . fi - # This exists because `go vet` does not look at import order: the move to - # git.ryuvia.com rewrote every import path without re-sorting, the new path sorts - # before github.com/..., and both repos sat unformatted through a green CI run and - # a release before anyone noticed. - # - # Both of gofmt's failure modes need handling, and they are not alike. A file that - # is merely misformatted is listed on stdout with exit 0 -- so the failure has to - # be raised by hand. A file that does not parse is the opposite: nothing on stdout - # and exit 2, which a naive `[ -n "$unformatted" ]` reads as success. The first - # draft of this step had exactly that hole. - - name: Format - run: | - if ! unformatted=$(gofmt -l .); then - echo "::error::gofmt could not parse the tree" - gofmt -l . # re-run unredirected so the parse errors reach the log - exit 1 - fi - if [ -n "$unformatted" ]; then - echo "::error::not gofmt'd:" - echo "$unformatted" - gofmt -d . - exit 1 - fi - - - name: Vet - run: go vet ./... - - # Covers the API client against a stub server, the Update state machine, and View - # rendering -- all three are pure enough to test without a terminal. - - name: Test - run: go test ./... + # The Makefile is the single definition of the gate -- gofmt with both of its failure + # modes handled, go vet, and the tests -- so this is exactly what a developer and the + # release skill run. See the comments on the targets for why each is shaped as it is. + - name: Format, vet and test + run: make fmt lint test diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index cf6ac11..76829eb 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -8,9 +8,9 @@ name: Release # the job that publishes them. Nothing is handed between jobs. # # The asset names matter beyond being tidy: internal/updater looks for exactly -# terdut-tui--- in the latest release and reports every available name -# when it cannot find one. Renaming the pattern here breaks self-update for every -# installed binary. +# terdut-tui--- in the latest release. The pattern is defined once, by +# `make dist` (and `make dist-assets`, which the release skill checks the published release +# against) -- see DIST_TARGETS in the Makefile before touching it. on: push: tags: @@ -41,35 +41,9 @@ jobs: REF_NAME: ${{ github.ref_name }} run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . - # This exists because `go vet` does not look at import order: the move to - # git.ryuvia.com rewrote every import path without re-sorting, the new path sorts - # before github.com/..., and both repos sat unformatted through a green CI run and - # a release before anyone noticed. - # - # Both of gofmt's failure modes need handling, and they are not alike. A file that - # is merely misformatted is listed on stdout with exit 0 -- so the failure has to - # be raised by hand. A file that does not parse is the opposite: nothing on stdout - # and exit 2, which a naive `[ -n "$unformatted" ]` reads as success. The first - # draft of this step had exactly that hole. - - name: Format - run: | - if ! unformatted=$(gofmt -l .); then - echo "::error::gofmt could not parse the tree" - gofmt -l . # re-run unredirected so the parse errors reach the log - exit 1 - fi - if [ -n "$unformatted" ]; then - echo "::error::not gofmt'd:" - echo "$unformatted" - gofmt -d . - exit 1 - fi - - - name: Vet - run: go vet ./... - - - name: Test - run: go test ./... + # Same target CI and the release skill run; a tag that fails it publishes nothing. + - name: Format, vet and test + run: make fmt lint test binaries: needs: test @@ -86,21 +60,12 @@ jobs: REF_NAME: ${{ github.ref_name }} run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + # The Makefile owns the target list and the asset names -- see DIST_TARGETS there for + # why the naming pattern cannot change. - name: Build every target env: REF_NAME: ${{ github.ref_name }} - run: | - set -eu - mkdir -p dist - for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do - GOOS="${target%/*}" - GOARCH="${target#*/}" - out="dist/terdut-tui-${REF_NAME}-${GOOS}-${GOARCH}" - echo "building $out" - GOOS="$GOOS" GOARCH="$GOARCH" go build \ - -ldflags "-X main.version=${REF_NAME}" \ - -o "$out" . - done + run: make dist VERSION="$REF_NAME" # Creating the release is made idempotent rather than assumed-new: a re-run of a # failed release must not die on the release that already exists. Assets are diff --git a/.gitignore b/.gitignore index c3f5f32..d821ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ terdut-tui .graymatter/ +dist/ diff --git a/.release.conf b/.release.conf new file mode 100644 index 0000000..94bc898 --- /dev/null +++ b/.release.conf @@ -0,0 +1,9 @@ +# Read by the `release` skill (~/.claude/skills/release). +# +# terdut-tui publishes binaries to a Gitea release and nothing else: no image, no Helm +# chart, no wrapper in Ryuvia/charts. KIND=binary tells the skill to gate, tag, wait for +# release.yaml and verify the published assets, and to skip the chart steps. +KIND=binary + +# English, like the rest of the terdut projects. +PROSE_LANG=en diff --git a/CLAUDE.md b/CLAUDE.md index 117607f..04f9158 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,25 @@ All user actions target incidents. Two server behaviours the UI has to respect: manual resolve is **terminal** (hence the confirmation prompt), and snooze is the non-destructive "not now" alternative. +## Release + +Say **"Release"** (or "Release X.Y.Z") and the `release` skill runs it. This repo is +`KIND=binary` in `.release.conf`: it publishes binaries to a Gitea release and has no image, +chart or wrapper-chart PR. The run is gate, commit, push, tag, wait for `release.yaml`, then +`verify-release`. Preconditions and the plan, without side effects: + +```sh +~/.claude/skills/release/scripts/release-preflight # state + suggested version +~/.claude/skills/release/scripts/release-preflight vX.Y.Z # validate that release +``` + +- `make fmt lint test` is the gate, and it **is** what `ci.yaml` and `release.yaml` run. +- `make dist VERSION=vX.Y.Z` builds the four binaries; `make dist-assets` lists their names. + The pattern `terdut-tui---` is the self-updater's contract with every + installed binary, so changing it breaks self-update. +- **The annotated tag's message becomes the release notes** (`release.yaml` copies it, minus + its subject line). Write it for a reader of the release page. Never move a published tag. + ## Tech stack - Go 1.25+ diff --git a/Makefile b/Makefile index 46cfd8a..dda44b4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ VERSION := $(shell git describe --tags --always --dirty) -.PHONY: build install test +.PHONY: build install test lint fmt dist dist-assets release-vars build: go build -ldflags "-X main.version=$(VERSION)" -o terdut-tui . @@ -8,5 +8,59 @@ build: install: go install -ldflags "-X main.version=$(VERSION)" . -test: +# ci.yaml and release.yaml run `make fmt lint test`, so a green gate here and a green +# pipeline are the same code rather than two descriptions of it. It is also what the +# release skill runs before tagging. + +# Covers the API client against a stub server, the sign-in flow, the Update state +# machine, and View rendering -- all pure enough to test without a terminal. +test: ## Run the test suite go test ./... + +lint: ## go vet + go vet ./... + +# This exists because `go vet` does not look at import order: the move to +# git.ryuvia.com rewrote every import path without re-sorting, the new path sorts +# before github.com/..., and both repos sat unformatted through a green CI run and +# a release before anyone noticed. +# +# Both of gofmt's failure modes need handling, and they are not alike. A file that +# is merely misformatted is listed on stdout with exit 0 -- so the failure has to +# be raised by hand. A file that does not parse is the opposite: nothing on stdout +# and exit 2, which a naive `[ -n "$$unformatted" ]` reads as success. The first +# draft of this target had exactly that hole. +fmt: ## Fail on files that are not gofmt'd + @if ! unformatted=$$(gofmt -l .); then \ + echo "gofmt could not parse the tree:"; gofmt -l .; exit 1; \ + fi; \ + if [ -n "$$unformatted" ]; then \ + echo "not gofmt'd:"; echo "$$unformatted"; gofmt -d .; exit 1; \ + fi + +# What a release publishes. The asset names matter beyond being tidy: internal/updater +# looks for exactly terdut-tui--- in the latest release and reports +# every available name when it cannot find one. Renaming the pattern here breaks +# self-update for every installed binary. +DIST_TARGETS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 + +dist: ## Build every release binary into dist/ (VERSION=vX.Y.Z to name them) + @set -eu; mkdir -p dist; \ + for target in $(DIST_TARGETS); do \ + goos="$${target%/*}"; goarch="$${target#*/}"; \ + out="dist/terdut-tui-$(VERSION)-$$goos-$$goarch"; \ + echo "building $$out"; \ + GOOS="$$goos" GOARCH="$$goarch" go build -ldflags "-X main.version=$(VERSION)" -o "$$out" .; \ + done + +# The names dist produces, one per line, so the release skill can check the published +# release has every one of them rather than a count. +dist-assets: ## Print the asset names a release of VERSION carries + @for target in $(DIST_TARGETS); do \ + echo "terdut-tui-$(VERSION)-$${target%/*}-$${target#*/}"; \ + done + +# Read by the release skill for a repo that publishes binaries and no image or chart +# (KIND=binary in .release.conf). There is nothing to say about images or charts. +release-vars: ## Print the variables the release process reads + @printf 'APP=terdut-tui\n'