496e7b6d90
The release page has been empty since the first release: the workflow attached the binaries and created the release with no body, so the changelog lived only in the annotated tag, where nobody reads it. The tag message, minus its subject line, is now set as the release notes. Only an annotated tag has a message worth copying, and only a release with no notes is filled, so re-running a failed release repairs an empty one without overwriting notes somebody edited by hand afterwards. The image has no jq, so the JSON string is escaped with sed and awk. Checked locally against the real v0.10.0 tag text and a string with quotes, backslashes, tabs, CRLF, backticks and $; each round-trips through a JSON parser unchanged. Not run in the pipeline: the PATCH call and the shallow tag clone's git commands are first exercised by the next tag push, and a failure there fails the step rather than leaving the notes silently empty.
165 lines
6.6 KiB
YAML
165 lines
6.6 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 and reports every available name
|
|
# when it cannot find one. Renaming the pattern here breaks self-update for every
|
|
# installed binary.
|
|
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" .
|
|
|
|
# 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 ./...
|
|
|
|
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" .
|
|
|
|
- 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
|
|
|
|
# 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
|