From 4a31b05644f1bbc0a10c1d09641bba671ac63c7f Mon Sep 17 00:00:00 2001 From: Lennart Elsen Date: Wed, 17 Jun 2026 12:59:12 +0200 Subject: [PATCH] [feature] CI: automate goquery-ui chart release on tagged app builds Adds a release-chart job to build-docker.yml implementing avenue 2 of the chart release model: on a final v*.*.* tag, after the frontend image is pushed, retarget the chart's appVersion to it, patch-bump the chart's own version:, publish the chart to GHCR, and commit both fields back to main as bookkeeping. Publishes straight from the tag via `helm package --app-version`, which sidesteps the GITHUB_TOKEN re-trigger rule and needs no PAT. Idempotent on appVersion, so re-runs and partial failures converge without minting a spurious chart version. Skips prerelease tags. Documented in docs/adr/0002 and CONTEXT.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-docker.yml | 74 +++++++++++++++ CONTEXT.md | 27 ++++++ ...h-goquery-ui-chart-pinned-to-appversion.md | 94 +++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 CONTEXT.md create mode 100644 docs/adr/0002-tagged-releases-publish-goquery-ui-chart-pinned-to-appversion.md diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index c84aa964..1bf7089b 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -63,3 +63,77 @@ jobs: push: true platforms: linux/amd64,linux/arm64,linux/arm/v7 tags: goprobe/frontend:${{ env.COMMIT_SHA }},goprobe/frontend:${{ env.RELEASE_VERSION }},goprobe/frontend:latest + + # Avenue 2 of the chart release model (see docs/adr/0002): a tagged app release + # retargets the goquery-ui chart's appVersion to the frontend image just built, + # patch-bumps the chart's own version, ships the chart, and commits both fields + # back to main as bookkeeping. publish-chart.yml stays the path for chart-only + # changes (avenue 1). Runs only after the frontend image exists, and never for + # prerelease tags (the chart must not point at an rc). + release-chart: + name: Release goquery-ui chart + needs: [build-docker] + if: ${{ !contains(github.ref_name, '-') }} + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + steps: + - name: Check out main + uses: actions/checkout@v4 + with: + ref: main + + - name: Set up Helm + uses: azure/setup-helm@v4 + + - name: Log in to GHCR + run: helm registry login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} + + - name: Compute & apply chart versions + id: ver + run: | + VER="${GITHUB_REF#refs/*/v}" # 4.3.0 (frontend image tag) + CUR_APP="$(yq '.appVersion' charts/goquery-ui/Chart.yaml)" + if [ "$CUR_APP" = "$VER" ]; then + echo "appVersion is already $VER on main — chart for this release is shipped. Nothing to do." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + CUR_CHART="$(yq '.version' charts/goquery-ui/Chart.yaml)" # 0.1.1 + IFS='.' read -r MAJ MIN PAT <<< "$CUR_CHART" + NEW_CHART="${MAJ}.${MIN}.$((PAT + 1))" # 0.1.2 + echo "Bumping chart $CUR_CHART -> $NEW_CHART, appVersion $CUR_APP -> $VER" + # Surgical edit (not `yq -i`, which strips the blank lines between the + # chart's commented blocks). `^version:` cannot match `apiVersion:`. + sed -i -E "s/^version:.*/version: ${NEW_CHART}/" charts/goquery-ui/Chart.yaml + sed -i -E "s|^appVersion:.*|appVersion: \"${VER}\"|" charts/goquery-ui/Chart.yaml + { + echo "skip=false" + echo "ver=$VER" + echo "new_chart=$NEW_CHART" + } >> "$GITHUB_OUTPUT" + + - name: Package & publish to GHCR + if: steps.ver.outputs.skip == 'false' + run: | + helm package charts/goquery-ui # version/appVersion straight from Chart.yaml + if helm show chart oci://ghcr.io/els0r/charts/goquery-ui \ + --version "${{ steps.ver.outputs.new_chart }}" >/dev/null 2>&1; then + echo "Chart goquery-ui ${{ steps.ver.outputs.new_chart }} already published — skipping push, reconciling main." + else + helm push goquery-ui-*.tgz oci://ghcr.io/els0r/charts + fi + + - name: Commit bump back to main + if: steps.ver.outputs.skip == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add charts/goquery-ui/Chart.yaml + if git diff --staged --quiet; then + echo "Chart.yaml already at the target versions — nothing to commit." + exit 0 + fi + git commit -m "[trivial] Release goquery-ui chart ${{ steps.ver.outputs.new_chart }} tracking frontend ${{ steps.ver.outputs.ver }}" + git push origin HEAD:main diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..eb600d69 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,27 @@ +# goProbe — Context + +Shared language for the project. Terms here are the canonical names; use them in +code, commits, and discussion. Decisions with lasting trade-offs live in +[docs/adr/](docs/adr/). + +## goquery-ui chart releases + +The `charts/goquery-ui` Helm chart is released on two distinct **avenues**. +Keep them straight — they bump different fields and fire on different events. + +- **Chart-only bump (avenue 1)** — a change to the chart's templates or values. + Bumps the chart's own `version:` and is published by `publish-chart.yml` on + merge to `main`. Independent of the app's release tags. + +- **Tagged release (avenue 2)** — an app release via a `v*.*.*` git tag. Builds + a new `goprobe/frontend` image and, in the same pipeline, retargets the + chart's `appVersion` to that image, patch-bumps `version:`, ships the chart, + and commits both fields back to `main`. Prerelease tags (`-rc*`) build images + but do not ship a chart. See ADR 0002. + +- **Chart version (`version:`)** — the chart's *own* semver (`0.x` line), + bumped on both avenues. Distinct from `appVersion`. + +- **appVersion** — the `goprobe/frontend` image tag the chart is validated + against and pins by default. `image.tag` overrides it per deployment. It is + an exact pin, not a floating tag — installs are reproducible (ADR 0002). diff --git a/docs/adr/0002-tagged-releases-publish-goquery-ui-chart-pinned-to-appversion.md b/docs/adr/0002-tagged-releases-publish-goquery-ui-chart-pinned-to-appversion.md new file mode 100644 index 00000000..b7516e10 --- /dev/null +++ b/docs/adr/0002-tagged-releases-publish-goquery-ui-chart-pinned-to-appversion.md @@ -0,0 +1,94 @@ +# 2. Tagged releases publish the goquery-ui chart, pinned to appVersion + +Date: 2026-06-17 + +## Status + +Accepted + +## Context + +The `charts/goquery-ui` chart ships on two independent avenues: + +- **Chart-only bump (avenue 1)** — a change to templates/values bumps the + chart's own `version:` and is published by `publish-chart.yml` on merge to + `main`. Independent of the app's `v*.*.*` tags. +- **Tagged release (avenue 2)** — an app release (`v*.*.*`) builds a new + `goprobe/frontend` image; the chart's `appVersion` must be retargeted to it, + its `version:` bumped, and a new chart shipped. + +Avenue 2 was **manual**: a human hand-edited `appVersion` and `version:` in +`Chart.yaml`, merged to `main`, and let `publish-chart.yml` ship it. We want it +automated, fired by the same `v*.*.*` tag that builds the images. + +Three alternatives were weighed: + +1. **Default the image to `latest` (drop appVersion tracking).** Simplest CI — + the chart never tracks `appVersion`. Rejected: it only updates with + `pullPolicy: Always`, which lets two pods in one Deployment run different + `latest` builds; it makes `helm install --version X` non-reproducible and + `helm rollback` unable to revert the frontend; and it turns `appVersion` and + the `app.kubernetes.io/version` label into a lie. That contradicts the + chart's pinned, hardened posture (see ADR 0001). `image.tag` already exists + for deployers who *want* to float or pin a specific build. + +2. **Commit the bump to `main` and let `publish-chart.yml` re-fire.** Keeps a + single publish path, but a push made with the default `GITHUB_TOKEN` does + **not** trigger another workflow — so `publish-chart.yml`'s `on: push` never + runs and the chart silently never ships. Bridging that needs a long-lived PAT + / GitHub App token or a `repository_dispatch` hop. Rejected: a managed secret + and broader write scope for a mechanical bump. + +3. **Publish straight from the tag workflow with `helm package + --app-version`.** `helm package` sets `appVersion` at package time, so the + tag workflow can ship a correctly-pinned chart with no secret and no reliance + on a re-trigger. Chosen. + +The chart's `version:` is an independent semver line (`0.x`), so its next value +is state that must be carried somewhere to stay monotonic — otherwise two +consecutive tagged releases both read the same base and collide. + +## Decision + +A `release-chart` job in `build-docker.yml`, `needs: [build-docker]` (so the +frontend image is pushed first), gated to final tags only +(`if: !contains(github.ref_name, '-')`): + +1. Checks out `main`, reads `version:` from `Chart.yaml`, **patch-bumps** it, + and sets `appVersion` to the tag's `X.Y.Z`. Patch is the conventional Helm + signal for an appVersion retarget; minor/major stay reserved for real + template changes shipped via avenue 1. +2. Runs `helm package charts/goquery-ui --app-version "$VER" --version "$NEW"`, + checks GHCR for that version (idempotent, mirroring `publish-chart.yml`), and + `helm push`es if absent. +3. Commits both fields back to `main` with the default `GITHUB_TOKEN` as + bookkeeping. It intentionally does **not** re-trigger `publish-chart.yml` + (that is the very `GITHUB_TOKEN` behaviour rejected above) — and here that is + a feature: the chart is already published, so no double publish occurs. + +The commit-back is a **direct push to `main`**, not a PR, so the PR-title rules +in `pr-naming-rules.yml` never apply to it; the message uses the sanctioned +`[trivial]` prefix anyway (it is a mechanical, generated bump). + +Re-runs are made idempotent by guarding on the bump's intent rather than the +chart's existence: if `appVersion` on `main` already equals the tag, the job is +a no-op. This converges the partial-failure cases (image pushed but chart not, +chart pushed but commit-back not) without ever minting a spurious second chart +version for the same release. + +`image.tag` continues to override the pin for ad-hoc deployments. + +## Consequences + +- Installs stay reproducible and `helm rollback` stays meaningful: a chart + version maps to one frontend image. +- No long-lived secret; the job uses the workflow's `GITHUB_TOKEN` + (`contents: write`, `packages: write`). +- `publish-chart.yml` remains the path for avenue 1 (chart-only pushes to + `main`). The two share package/push/idempotency steps — minor duplication that + could later be factored into a composite action. +- Each tagged release lands one bookkeeping commit on `main`, so `main` always + reflects the shipped chart. +- Prerelease tags (`v*.*.*-rc*`) still build images but never ship a chart. +- Edge case: two final tags pushed in quick succession serialize through the + commit-back; a concurrency guard can be added if it ever races.