79e77fadc6
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.
130 lines
5.4 KiB
YAML
130 lines
5.4 KiB
YAML
name: Release
|
|
|
|
# Checkout, interpolation and caching conventions match ci.yaml -- see the header there
|
|
# for why there are no JS actions and why every `${{ }}` goes through `env:`.
|
|
#
|
|
# There is no upload-artifact/download-artifact equivalent here (both are JS actions, and
|
|
# this Gitea has no artifact store wired up), so the job that builds the binaries is also
|
|
# 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. 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:
|
|
- 'v*'
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: release-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
REPO_URL: https://git.ryuvia.com/niklas/terdut-tui.git
|
|
API: https://git.ryuvia.com/api/v1/repos/niklas/terdut-tui
|
|
|
|
jobs:
|
|
# Gates the build, so a tag that fails here publishes no binaries.
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: golang:1.26.6-bookworm
|
|
volumes:
|
|
- go-mod-cache:/go/pkg/mod
|
|
- go-build-cache:/root/.cache/go-build
|
|
- gobin-cache:/go/bin
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
|
|
# 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
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: golang:1.26.6-bookworm
|
|
volumes:
|
|
- go-mod-cache:/go/pkg/mod
|
|
- go-build-cache:/root/.cache/go-build
|
|
- gobin-cache:/go/bin
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
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: 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
|
|
# replaced the same way, so a re-run repairs a partial upload.
|
|
- name: Publish the release
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
auth="Authorization: token $TOKEN"
|
|
|
|
body=$(curl -sf -H "$auth" "$API/releases/tags/$REF_NAME" || true)
|
|
if [ -z "$body" ]; then
|
|
body=$(curl -sf -X POST -H "$auth" -H 'Content-Type: application/json' \
|
|
-d "{\"tag_name\":\"$REF_NAME\",\"name\":\"$REF_NAME\"}" \
|
|
"$API/releases")
|
|
fi
|
|
|
|
# The release object serialises `id` first, so the first match is the release's
|
|
# own id and not one of the nested author/asset ids.
|
|
release_id=$(printf '%s' "$body" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
|
[ -n "$release_id" ] || { echo "::error::could not determine release id"; exit 1; }
|
|
echo "release id $release_id"
|
|
|
|
# The release notes are the tag's own message, minus its subject line: the
|
|
# tag body is the changelog for this project, and without this the release
|
|
# page stays empty. Only an annotated tag has one, and only a release with
|
|
# no notes is filled, so a re-run repairs a release created empty without
|
|
# overwriting notes somebody has since edited by hand.
|
|
#
|
|
# There is no jq in this image, so the JSON string is escaped by hand:
|
|
# backslashes first (or the ones added next would double), then quotes and
|
|
# tabs, then each line end becomes a literal \n.
|
|
if [ "$(git cat-file -t "$REF_NAME")" = tag ] \
|
|
&& printf '%s' "$body" | grep -q '"body":""'; then
|
|
notes=$(git tag -l --format='%(contents)' "$REF_NAME" | sed '1,2d')
|
|
if [ -n "$notes" ]; then
|
|
notes_json=$(printf '%s\n' "$notes" \
|
|
| sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' -e 's/\r$//' \
|
|
| awk 'BEGIN { ORS = "\\n" } { print }')
|
|
echo "setting release notes from the tag message"
|
|
curl -sf -X PATCH -H "$auth" -H 'Content-Type: application/json' \
|
|
-d "{\"body\":\"$notes_json\"}" "$API/releases/$release_id" > /dev/null
|
|
fi
|
|
fi
|
|
|
|
for f in dist/*; do
|
|
name=$(basename "$f")
|
|
# Drop an existing asset of the same name first: Gitea happily stores two
|
|
# attachments with one name, and the updater matches by name.
|
|
old=$(curl -sf -H "$auth" "$API/releases/$release_id/assets" \
|
|
| tr '}' '\n' | grep "\"name\":\"$name\"" \
|
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
|
if [ -n "$old" ]; then
|
|
curl -sf -X DELETE -H "$auth" "$API/releases/$release_id/assets/$old" || true
|
|
fi
|
|
echo "uploading $name"
|
|
curl -sf -X POST -H "$auth" -F "attachment=@$f" \
|
|
"$API/releases/$release_id/assets?name=$name" > /dev/null
|
|
done
|