Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/workflows/build-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 27 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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.
Loading