Files
terdut-server/.gitea/workflows/release.yaml
T
Niklas Ye 69fcc24a4d
CI / chart (push) Successful in 0s
CI / test (push) Successful in 57s
Drive the pipeline through make, the way riksdata and rd-web do
Both workflows restated the build in YAML: gofmt, go vet and go test inline
in two places, buildx inline in a third, and the chart's version sed'd into
Chart.yaml before packaging. The Makefile added in 1081260 then described
the same checks a second time for local use, which made "green locally means
green in CI" a promise about keeping two files in step rather than a
property of the setup.

riksdata and rd-web never had that problem — their workflows call make and
have done all along. This repo was the odd one out, and only because it had
no Makefile until today. Now ci.yaml runs `make fmt lint test`, release.yaml
runs the same plus `make binaries`, `make push`, `make helm-package` and
`make helm-push`, and the reasoning behind each check lives on the target
rather than in whichever YAML file was edited last.

Three things change rather than just move:

The chart is linted before it is published. release.yaml packaged and pushed
without ever rendering the templates, so a chart that did not compile would
have reached the registry and been found by Flux. ci.yaml gained a chart job
for the same reason.

helm package --version --app-version replaces the sed. The published
metadata is identical, but the tree is no longer mutated mid-build, and it
is what the rest of the release process already assumed happened.

`make push` refuses VERSION=dev. Publishing is one command now, so it is
also one command to run by accident; dev is not a version anyone releases.

Deliberately not moved: uploading the release assets. Compiling them is
`make binaries` and runs anywhere, but the upload needs a token and the
Gitea release API, which is the workflow's business and not something worth
a target.

`push` builds and pushes in one step, unlike riksdata's separate build and
push, because buildx cannot load a multi-platform image into the local store
— it can only push it. `build` stays single-platform and local-only.

Claude-Session: https://claude.ai/code/session_01S7R4gWTz5wh5xCY4nCSJjN
2026-09-01 22:36:54 +02:00

190 lines
7.8 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 at all.
on:
push:
tags:
- 'v*'
workflow_dispatch:
# A tag is not normally re-pushed, so this mostly matters when one is force-moved during
# a botched release -- the superseded run stops holding runner slots.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
env:
REPO_URL: https://git.ryuvia.com/niklas/terdut-server.git
API: https://git.ryuvia.com/api/v1/repos/niklas/terdut-server
REGISTRY: git.ryuvia.com
IMAGE: git.ryuvia.com/niklas/terdut-server
jobs:
# Gates every publishing job below. A tag that fails here publishes nothing: the
# binaries, the image and the chart are all downstream of it.
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 gate as ci.yaml, and the same one a developer runs. See the Makefile for why
# each check is there; restating it here is how the two drift apart.
- 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" .
# Compiling is the Makefile's; uploading is not. `make binaries` is runnable on a
# laptop, while the step below needs a token and the Gitea release API, which is
# this workflow's business and nothing a developer wants a target for.
- name: Build every target
env:
REF_NAME: ${{ github.ref_name }}
run: make binaries 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"
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
# Host mode on purpose (no `container:`): this is the only context with a Docker CLI
# pointed at the dind daemon. A `container:` job would sit on the dind bridge with no
# docker socket at all.
image:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
env:
REF_NAME: ${{ github.ref_name }}
run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
- name: Log in to the registry
env:
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: echo "$TOKEN" | docker login "$REGISTRY" -u niklas --password-stdin
# buildx setup, the platform list and why there is no QEMU all live on the `push`
# target now, so the same command publishes from a laptop and from here.
- name: Build and push
env:
REF_NAME: ${{ github.ref_name }}
run: make push VERSION="$REF_NAME"
# Also host mode: helm is baked into the runner image, and a `container:` job could not
# install it -- get.helm.sh is unreachable from the dind bridge.
chart:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
env:
REF_NAME: ${{ github.ref_name }}
run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
# This job is the only thing that publishes the chart, which is what keeps the
# published metadata honest. There used to be a second publisher on every charts/**
# push to main, and the two raced for the same chart version with different answers:
# this one stamps version and appVersion from the tag, that one took Chart.yaml
# verbatim, where appVersion is the hardcoded "latest". Whichever landed first won,
# so the metadata of a release depended on which runner was quicker -- chart 0.9.0
# went out on 2026-08-08 reading appVersion "latest" that way.
#
# It could not be fixed by making both agree: the tag is pushed after the branch, so
# a workflow triggered by the main push cannot know the version it is about to be
# tagged with. One publisher, triggered by the tag.
#
# The cost is that the chart only ships with an app release. That is no real loss --
# `make helm-package` derives the chart version from the tag, so a chart-only change
# has no version of its own to be released under anyway. Chart fixes ride the next
# tag.
- name: Refuse a non-version tag
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -eu
if ! echo "$REF_NAME" | grep -qE '^v[0-9]'; then
echo "::error::refusing to publish a chart for non-version tag ${REF_NAME}"
exit 1
fi
# Render before publishing. Until 2026-09-01 this job packaged and pushed without
# linting, so a template that did not compile reached the registry and was found by
# Flux instead.
- name: Lint and render the chart
run: make helm-lint
# The version and appVersion are no longer sed'd into Chart.yaml before packaging:
# `helm package --version --app-version` sets both from the tag without mutating the
# tree mid-build, which is what the rest of the release process already assumed
# happened. The isolated helm repo list moved onto the targets with them.
- name: Package and push
env:
REF_NAME: ${{ github.ref_name }}
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
echo "$TOKEN" | helm registry login "$REGISTRY" -u niklas --password-stdin
make helm-package helm-push VERSION="$REF_NAME"