2 Commits

Author SHA1 Message Date
Niklas Ye 451ce99a62 Say plainly that stats cover all teams
CI / test (push) Successful in 3s
The README said stats were "not team-scoped by the server", which reads
as though they were unscoped. They are scoped, to all of the caller's
teams (TestTeams_AlertsAndStatsAreScoped in terdut-server); what they
cannot do is narrow to one. Docs only, no code change.
2026-09-25 13:10:41 +02:00
Niklas Ye 79e77fadc6 Let the release skill drive this repo, and let make drive the pipeline
CI / test (push) Successful in 4s
Release / test (push) Successful in 4s
Release / binaries (push) Successful in 12s
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-<tag>-<goos>-<goarch> 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.
2026-09-24 08:38:02 +02:00
7 changed files with 101 additions and 79 deletions
+5 -31
View File
@@ -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
+9 -44
View File
@@ -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-<tag>-<goos>-<goarch> 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-<tag>-<goos>-<goarch> 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
+1
View File
@@ -1,2 +1,3 @@
terdut-tui
.graymatter/
dist/
+9
View File
@@ -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
+19
View File
@@ -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-<tag>-<goos>-<goarch>` 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+
+56 -2
View File
@@ -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-<tag>-<goos>-<goarch> 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'
+2 -2
View File
@@ -32,8 +32,8 @@ The schedule is one team's rota, so the Schedule section shows the active team,
or with *all* showing the first team you own. Only a team's owners, and
administrators, can change its rota; anyone else gets the reason in the status
bar instead of a picker. The picker offers only that team's members, because the
server refuses anybody else. Stats are not team-scoped by the server and always
cover all your teams.
server refuses anybody else. Stats always cover all your teams; the server
cannot narrow them to one.
Administrators are the only users who can create or delete users, or act on
someone else's password, topic or API keys. Everyone can manage their own.