84146fc903
terdut-server was the only one of the three release-managed repos with no image scanning at all. riksdata and rd-web have had a scan-image job since they were set up; everything published here up to and including v0.9.3 went out without a CVE check. It scans the pushed image rather than a locally built one, for the same reason the siblings do: trivy cannot read a local image on this runner, since Talos has no docker socket and the dind sidecar shares no filesystem with the job. So it runs after image rather than gating it, and a red scan unpublishes nothing. What it means is narrower and worth stating plainly: do not bump the wrapper chart in Ryuvia/charts to that version. Checked before wiring it in rather than after. v0.9.3 scans clean at HIGH,CRITICAL with unfixed findings ignored, so this does not turn the pipeline red on arrival, and the same command exits 1 on an image that does have findings — a gate that cannot fail is not a gate. One platform is scanned, not both. The image is FROM scratch, so there are no OS packages and trivy sees a single target: the Go binary and its module graph. linux/amd64 and linux/arm64 are that same module set built for a different GOARCH, so a finding in one is a finding in both. On an image with a base layer that reasoning would not hold. Still no govulncheck and no gitleaks here, which riksdata and rd-web run in a separate CI job. This is the only security scanning terdut-server has. Claude-Session: https://claude.ai/code/session_01S7R4gWTz5wh5xCY4nCSJjN
224 lines
9.4 KiB
YAML
224 lines
9.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 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"
|
|
|
|
# Host mode, like image and chart: this needs a docker daemon to run trivy in, and a
|
|
# `container:` job would sit on the dind bridge with none.
|
|
#
|
|
# It scans the pushed image rather than a locally built one, because trivy cannot read a
|
|
# local image on this runner -- Talos has no docker socket and the dind sidecar shares no
|
|
# filesystem with the job -- so it pulls from the registry. That is also why this runs
|
|
# after `image` rather than gating it: a red scan does not unpublish anything.
|
|
#
|
|
# What a red scan means is therefore not "the release failed" but "do not bump the wrapper
|
|
# chart in Ryuvia/charts to this version". The image and chart are already published by
|
|
# the time this runs, and deliberately so -- this pipeline does not deploy.
|
|
#
|
|
# riksdata and rd-web have had this since they were set up; terdut-server went without any
|
|
# image scanning until 2026-09-02, so every release before v0.9.4 was published with no
|
|
# CVE check at all.
|
|
scan-image:
|
|
needs: image
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
|
|
# Credentials are passed even though these packages are anonymously pullable -- that
|
|
# is a property of the personal namespace this publishes to, not something a release
|
|
# should depend on staying true.
|
|
- name: Scan the pushed image (trivy)
|
|
env:
|
|
TRIVY_USERNAME: niklas
|
|
TRIVY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: make security-image VERSION="$REF_NAME"
|