Etappe 34: the panel must not be first in the kill order #27
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Publishing is a consequence of tagging, not a checklist someone remembers. | |
| # | |
| # Before this existed, releasing meant running the commands in CONTRIBUTING by | |
| # hand — so images 0.1.10 through 0.1.14 were built locally, imported straight | |
| # into k3s, and never published. GHCR sat on 0.1.9 while the README told strangers | |
| # to install `latest`, and nothing anywhere said the two disagreed. | |
| name: Release | |
| on: | |
| push: | |
| tags: ["v*"] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version to publish (without the leading v), e.g. 0.1.15" | |
| required: true | |
| permissions: | |
| # write, because this workflow publishes the GitHub Release too. A release page | |
| # created by hand is the same bug §4.17 removed from the artefacts: a publishing | |
| # step that survives only as long as someone remembers it. | |
| contents: write | |
| packages: write | |
| jobs: | |
| release: | |
| name: Publish image & chart to GHCR | |
| runs-on: ubuntu-latest | |
| # The first attempt spent 25 minutes emulating an arm64 frontend build before | |
| # dying. With the builder stages running natively this job takes a few minutes, | |
| # so anything approaching this limit means something is wrong again — fail | |
| # while that is still obvious rather than burning half an hour first. | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # One version for the whole release. Everything below reads $VERSION; | |
| # nothing re-derives it, so there is no second place to drift. | |
| - name: Resolve version | |
| id: v | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| VERSION="${{ inputs.version }}" | |
| else | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Releasing $VERSION" | |
| # A tag that disagrees with the chart means one of the two is a lie. Stop | |
| # rather than publish it and find out later which one. | |
| - name: Chart version must match the tag | |
| env: | |
| VERSION: ${{ steps.v.outputs.version }} | |
| run: | | |
| CHART=$(grep '^version:' deploy/helm/matrixctrl/Chart.yaml | awk '{print $2}') | |
| APP=$(grep '^appVersion:' deploy/helm/matrixctrl/Chart.yaml | awk '{print $2}' | tr -d '"') | |
| echo "tag=$VERSION chart=$CHART appVersion=$APP" | |
| [ "$CHART" = "$VERSION" ] || { echo "::error::Chart.yaml version ($CHART) != tag ($VERSION)"; exit 1; } | |
| [ "$APP" = "$VERSION" ] || { echo "::error::Chart.yaml appVersion ($APP) != tag ($VERSION)"; exit 1; } | |
| # The README deliberately does not pin a chart version — Helm resolves the | |
| # newest, so the quickstart cannot go stale and there is nothing to remember. | |
| # This guard is only a backstop: if someone ever writes a concrete version | |
| # into the docs, it has to be the one being released, or the docs start lying | |
| # again the moment the next version ships. | |
| # | |
| # No pinned version is the expected case and passes. | |
| - name: Any version pinned in the docs must match the tag | |
| env: | |
| VERSION: ${{ steps.v.outputs.version }} | |
| run: | | |
| FOUND=$(grep -ohE '\-\-version [0-9]+\.[0-9]+\.[0-9]+' README.md docs/*.md 2>/dev/null | awk '{print $2}' | sort -u) | |
| if [ -z "$FOUND" ]; then | |
| echo "No hardcoded chart version in the docs — nothing to drift." | |
| exit 0 | |
| fi | |
| echo "Found pinned versions: $FOUND" | |
| for v in $FOUND; do | |
| [ "$v" = "$VERSION" ] || { echo "::error::docs pin --version $v but the tag is $VERSION"; exit 1; } | |
| done | |
| # Checked here, before anything is published, rather than at the end where | |
| # the artefacts would already be in GHCR and a missing entry could only be | |
| # reported after the fact. | |
| - name: CHANGELOG must have a section for this version | |
| env: | |
| VERSION: ${{ steps.v.outputs.version }} | |
| run: | | |
| awk -v v="## [$VERSION]" ' | |
| index($0, v) == 1 { found = 1; next } | |
| found && /^## / { exit } | |
| found { print } | |
| ' CHANGELOG.md > release-notes.md | |
| if [ ! -s release-notes.md ]; then | |
| echo "::error::CHANGELOG.md has no '## [$VERSION]' section — write it before tagging" | |
| exit 1 | |
| fi | |
| echo "Release notes ($(wc -l < release-notes.md) lines):" | |
| cat release-notes.md | |
| # The embedded frontend is committed, and a stale copy has shipped before | |
| # (see BACKLOG P2-2). Rebuild it here so the released image cannot embed an | |
| # older UI than the source it was built from. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Build frontend | |
| run: | | |
| cd web && npm ci && npm run build | |
| cd .. && rm -rf cmd/matrixctrl/dist && cp -r web/dist cmd/matrixctrl/dist | |
| # No setup-qemu: nothing is emulated any more. | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # amd64 only, for now. | |
| # | |
| # Two release attempts failed in this step. The first took 25 minutes — | |
| # buildx was emulating the frontend build for arm64, fixed in the Dockerfile | |
| # by building the builder stages natively. The second failed in 4 minutes, | |
| # so emulation is no longer the bottleneck but something else in the arm64 | |
| # path still breaks: the runtime stage's `apk add` runs under QEMU, and that | |
| # is exactly where a local reproduction died. | |
| # | |
| # A release that publishes nothing is worth less than an amd64 release that | |
| # publishes, so arm64 is dropped rather than debugged further under a tag. | |
| # The proper fix is a native arm64 runner (ubuntu-24.04-arm, free for public | |
| # repos) building each architecture on its own hardware and merging the | |
| # manifests — no emulation anywhere. Tracked in BACKLOG. | |
| # | |
| # This attempt doubles as the experiment that tells the two candidate causes | |
| # apart: if amd64 alone publishes, arm64/QEMU was the problem; if it still | |
| # fails, the Actions token cannot write to these pre-existing packages. | |
| # Build and push are deliberately separate steps. | |
| # | |
| # Three attempts failed inside a combined "Build & push image", and job logs | |
| # need a token the agent does not have — so "which half broke" was unknowable | |
| # from the API, and each guess cost a full release cycle. Split, the step | |
| # names themselves carry the diagnosis: a failure in "Build image" is the | |
| # Dockerfile, a failure in "Push image" is registry permissions. | |
| # | |
| # Worth keeping even once this is resolved. It costs nothing and turns an | |
| # opaque failure into a labelled one. | |
| - name: Build image (no push) | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| push: false | |
| load: true | |
| platforms: linux/amd64 | |
| build-args: | | |
| VERSION=${{ steps.v.outputs.version }} | |
| tags: | | |
| ghcr.io/bxnnyg/matrixctrl:${{ steps.v.outputs.version }} | |
| ghcr.io/bxnnyg/matrixctrl:latest | |
| - name: Push image | |
| env: | |
| VERSION: ${{ steps.v.outputs.version }} | |
| run: | | |
| set -x | |
| docker push "ghcr.io/bxnnyg/matrixctrl:$VERSION" | |
| docker push "ghcr.io/bxnnyg/matrixctrl:latest" | |
| # The chart's default image tag is "latest", which is how a chart pinned to | |
| # one version ends up pulling a different build months later. Pin the | |
| # released copy to the exact version it was cut with, so | |
| # `helm install --version X` is reproducible. | |
| # | |
| # helm package has no --set, so values.yaml is rewritten in the workspace | |
| # first. This is a throwaway checkout; the committed default stays "latest". | |
| - name: Package & push chart | |
| env: | |
| VERSION: ${{ steps.v.outputs.version }} | |
| run: | | |
| helm registry login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}" | |
| sed -i "s|^ tag: \".*\"| tag: \"$VERSION\"|" deploy/helm/matrixctrl/values.yaml | |
| grep -A1 '^image:' deploy/helm/matrixctrl/values.yaml | |
| PINNED=$(grep -E '^ tag:' deploy/helm/matrixctrl/values.yaml | awk '{print $2}' | tr -d '"') | |
| [ "$PINNED" = "$VERSION" ] || { echo "::error::failed to pin image.tag (got '$PINNED')"; exit 1; } | |
| helm package deploy/helm/matrixctrl --version "$VERSION" --app-version "$VERSION" | |
| helm push "matrixctrl-$VERSION.tgz" oci://ghcr.io/bxnnyg/charts | |
| helm registry logout ghcr.io || true | |
| # Last, so the page only appears once the artefacts it describes exist. | |
| # Idempotent: re-running a tag updates the notes instead of failing, which | |
| # matters because tags do get re-cut (the history rewrite in P0-1c moved | |
| # v0.1.15). | |
| - name: Publish GitHub Release | |
| env: | |
| VERSION: ${{ steps.v.outputs.version }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if gh release view "v$VERSION" >/dev/null 2>&1; then | |
| echo "Release v$VERSION exists — updating notes." | |
| gh release edit "v$VERSION" --title "MatrixCtrl $VERSION" --notes-file release-notes.md | |
| else | |
| gh release create "v$VERSION" --title "MatrixCtrl $VERSION" --notes-file release-notes.md | |
| fi | |
| - name: Summary | |
| env: | |
| VERSION: ${{ steps.v.outputs.version }} | |
| run: | | |
| { | |
| echo "### Released \`$VERSION\` (linux/amd64)" | |
| echo | |
| echo "What a new user runs — no version pinned, Helm resolves the newest:" | |
| echo '```bash' | |
| echo "helm install matrixctrl oci://ghcr.io/bxnnyg/charts/matrixctrl \\" | |
| echo " --namespace matrixctrl --create-namespace \\" | |
| echo " --set ingress.host=matrixctrl.example.com" | |
| echo '```' | |
| echo | |
| echo "To verify this exact release:" | |
| echo '```bash' | |
| echo "helm show chart oci://ghcr.io/bxnnyg/charts/matrixctrl --version $VERSION" | |
| echo '```' | |
| echo | |
| echo "New GHCR packages default to **private** — check visibility if this is a first publish." | |
| } >> "$GITHUB_STEP_SUMMARY" |