|
1 | 1 | name: helm chart |
2 | 2 |
|
| 3 | +# Lints + unit-tests + render-checks the chart on every PR/main push, and |
| 4 | +# PUBLISHES it to GHCR as an OCI artifact on `chart-v*` tags. |
| 5 | +# |
| 6 | +# Publishing mirrors the relay image (see docker.yml): the chart is versioned |
| 7 | +# independently of the desktop app and the relay via its own `chart-v*` tags |
| 8 | +# (Chart.yaml `version`, cut by merging a `chart-release/<version>` PR). Only |
| 9 | +# `chart-v*` tags publish — `main` pushes and PRs stay lint/render-only so we |
| 10 | +# never overwrite a released chart version from an in-progress `main`. |
| 11 | +# |
| 12 | +# Why workflow_dispatch carries version/ref inputs (same reason as docker.yml): |
| 13 | +# auto-tag-on-release-pr-merge.yml pushes `chart-v*` with the default |
| 14 | +# GITHUB_TOKEN, which GitHub's recursion guard blocks from firing any |
| 15 | +# `on: push` trigger. So the `push.tags` trigger below is dead for |
| 16 | +# auto-pushed tags — auto-tag instead dispatches this workflow with the bare |
| 17 | +# version + tag ref, and the publish job checks out inputs.ref and packages at |
| 18 | +# inputs.version. On a real manual `git push` of a `chart-v*` tag the |
| 19 | +# push.tags trigger fires and inputs are empty. |
| 20 | + |
3 | 21 | on: |
4 | 22 | workflow_dispatch: |
| 23 | + inputs: |
| 24 | + version: |
| 25 | + description: "Chart semver e.g. 0.1.0 (no chart-v prefix) — for chart-tag rescue dispatch" |
| 26 | + required: false |
| 27 | + ref: |
| 28 | + description: "Chart tag ref to publish, e.g. chart-v0.1.0 (required when version is set)" |
| 29 | + required: false |
| 30 | + default: main |
5 | 31 | push: |
| 32 | + # No `paths` filter here: GitHub applies a push `paths` filter to tag |
| 33 | + # pushes too, so a `chart-v*` tag whose commit didn't touch a chart file |
| 34 | + # would be filtered out and never publish. docker.yml / release.yml / sprig |
| 35 | + # all keep `paths` out of a tag-carrying `push` for exactly this reason — |
| 36 | + # PR runs stay scoped via `pull_request.paths` below; main pushes lint |
| 37 | + # unconditionally (cheap), and tag pushes always run so publish can fire. |
6 | 38 | branches: [main] |
7 | | - paths: |
8 | | - - "deploy/charts/buzz/**" |
9 | | - - ".github/workflows/helm-chart.yml" |
10 | | - - "ct.yaml" |
| 39 | + tags: ["chart-v[0-9]*"] |
11 | 40 | pull_request: |
12 | 41 | paths: |
13 | 42 | - "deploy/charts/buzz/**" |
14 | 43 | - ".github/workflows/helm-chart.yml" |
15 | 44 | - "ct.yaml" |
16 | 45 |
|
| 46 | +# Match docker.yml: deny-by-default, each job grants only what it needs. |
| 47 | +permissions: {} |
| 48 | + |
| 49 | +env: |
| 50 | + # Single source of truth for the OCI chart repository (helm appends the chart |
| 51 | + # name `buzz`, yielding oci://ghcr.io/block/buzz/charts/buzz, which is exactly |
| 52 | + # the install ref documented in deploy/charts/buzz/README.md). Set |
| 53 | + # GHCR_CHART_REPO as a repo variable to override (e.g., forks pushing to their |
| 54 | + # own namespace without editing this file) — mirrors docker.yml's GHCR_IMAGE. |
| 55 | + CHART_REPO: ${{ vars.GHCR_CHART_REPO != '' && vars.GHCR_CHART_REPO || 'oci://ghcr.io/block/buzz/charts' }} |
| 56 | + |
17 | 57 | jobs: |
18 | 58 | lint-and-unittest: |
19 | 59 | name: lint + unittest + render matrix |
20 | 60 | runs-on: ubuntu-latest |
| 61 | + permissions: |
| 62 | + contents: read |
21 | 63 | steps: |
22 | 64 | - uses: actions/checkout@v4 |
23 | 65 | with: |
| 66 | + # On chart-tag rescue dispatch, lint/render the tagged commit that the |
| 67 | + # publish job will package, not whatever `main` is when the dispatch |
| 68 | + # runs. Empty string = default ref for push/PR events. |
| 69 | + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} |
24 | 70 | fetch-depth: 0 |
25 | 71 |
|
26 | 72 | - name: Set up Helm |
@@ -62,10 +108,15 @@ jobs: |
62 | 108 | # exist and to embed Max's startup migrations. Runs only after Sami's |
63 | 109 | # image PR merges (`workflow_dispatch`) or on a schedule once main carries |
64 | 110 | # both prerequisites. Render/lint above is the per-PR signal. |
| 111 | + # |
| 112 | + # `inputs.version == ''` excludes the chart-tag rescue dispatch (which |
| 113 | + # carries a version): that path only packages+publishes, it does not install. |
65 | 114 | name: install on kind (gated) |
66 | | - if: github.event_name == 'workflow_dispatch' |
| 115 | + if: github.event_name == 'workflow_dispatch' && inputs.version == '' |
67 | 116 | runs-on: ubuntu-latest |
68 | 117 | needs: lint-and-unittest |
| 118 | + permissions: |
| 119 | + contents: read |
69 | 120 | steps: |
70 | 121 | - uses: actions/checkout@v4 |
71 | 122 | with: |
@@ -95,3 +146,110 @@ jobs: |
95 | 146 |
|
96 | 147 | - name: ct install (quickstart profile) |
97 | 148 | run: ct install --config ct.yaml --charts deploy/charts/buzz --helm-extra-args "--timeout 600s" |
| 149 | + |
| 150 | + publish: |
| 151 | + # Packages the chart and pushes it to GHCR as an OCI artifact. Fires only on |
| 152 | + # a `chart-v*` tag push (manual) or the auto-tag rescue dispatch (which |
| 153 | + # carries inputs.version) — never on `main` pushes or PRs, so an in-progress |
| 154 | + # `main` can never overwrite a released chart version. Mirrors docker.yml's |
| 155 | + # GHCR publish (login with GITHUB_TOKEN, packages: write, fork override var). |
| 156 | + name: publish chart to GHCR |
| 157 | + if: > |
| 158 | + startsWith(github.ref, 'refs/tags/chart-v') || |
| 159 | + (github.event_name == 'workflow_dispatch' && inputs.version != '') |
| 160 | + runs-on: ubuntu-latest |
| 161 | + needs: lint-and-unittest |
| 162 | + timeout-minutes: 15 |
| 163 | + permissions: |
| 164 | + contents: read |
| 165 | + packages: write # push the chart to GHCR |
| 166 | + steps: |
| 167 | + - name: Checkout |
| 168 | + uses: actions/checkout@v4 |
| 169 | + with: |
| 170 | + # On the rescue dispatch, build the tagged commit (github.ref is |
| 171 | + # `main` there); on a tag push, the default ref is already the tag. |
| 172 | + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }} |
| 173 | + fetch-depth: 0 |
| 174 | + persist-credentials: false |
| 175 | + |
| 176 | + - name: Set up Helm |
| 177 | + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 |
| 178 | + with: |
| 179 | + version: v3.16.4 |
| 180 | + |
| 181 | + - name: Resolve chart version |
| 182 | + id: ver |
| 183 | + env: |
| 184 | + # Bare on the rescue dispatch; empty on a tag push (derive from ref). |
| 185 | + INPUT_VERSION: ${{ inputs.version }} |
| 186 | + REF_NAME: ${{ github.ref_name }} |
| 187 | + run: | |
| 188 | + set -euo pipefail |
| 189 | + if [ -n "$INPUT_VERSION" ]; then |
| 190 | + version="$INPUT_VERSION" |
| 191 | + tag_ref="refs/tags/chart-v${version}" |
| 192 | + tag_sha="$(git rev-parse -q --verify "${tag_ref}^{commit}" || true)" |
| 193 | + head_sha="$(git rev-parse HEAD)" |
| 194 | + if [ -z "$tag_sha" ] || [ "$head_sha" != "$tag_sha" ]; then |
| 195 | + echo "::error::workflow_dispatch with version '$version' must check out matching tag 'chart-v${version}' (HEAD=$head_sha, tag=${tag_sha:-missing})" |
| 196 | + exit 1 |
| 197 | + fi |
| 198 | + else |
| 199 | + # refs/tags/chart-v0.1.0 → github.ref_name is `chart-v0.1.0`. |
| 200 | + version="${REF_NAME#chart-v}" |
| 201 | + fi |
| 202 | + if ! echo "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then |
| 203 | + echo "::error::Resolved chart version '$version' is not valid semver" |
| 204 | + exit 1 |
| 205 | + fi |
| 206 | + # The tag is the source of truth, but the published artifact's version |
| 207 | + # comes from Chart.yaml — they must agree or we'd publish a mislabeled |
| 208 | + # chart. Fail loudly on drift rather than silently shipping a mismatch. |
| 209 | + chart_version="$(helm show chart deploy/charts/buzz | awk '/^version:/ {print $2}')" |
| 210 | + if [ "$chart_version" != "$version" ]; then |
| 211 | + echo "::error::Tag version '$version' != Chart.yaml version '$chart_version'. Bump Chart.yaml to match the tag." |
| 212 | + exit 1 |
| 213 | + fi |
| 214 | + echo "version=$version" >> "$GITHUB_OUTPUT" |
| 215 | + echo "Publishing chart version $version" |
| 216 | +
|
| 217 | + - name: Log in to GHCR |
| 218 | + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 |
| 219 | + with: |
| 220 | + registry: ghcr.io |
| 221 | + username: ${{ github.repository_owner }} |
| 222 | + password: ${{ secrets.GITHUB_TOKEN }} |
| 223 | + |
| 224 | + - name: Build chart dependencies |
| 225 | + run: helm dependency build deploy/charts/buzz |
| 226 | + |
| 227 | + - name: Package chart |
| 228 | + run: helm package deploy/charts/buzz --destination dist |
| 229 | + |
| 230 | + - name: Push chart to GHCR |
| 231 | + env: |
| 232 | + CHART_REPO: ${{ env.CHART_REPO }} |
| 233 | + VERSION: ${{ steps.ver.outputs.version }} |
| 234 | + run: | |
| 235 | + set -euo pipefail |
| 236 | + helm push "dist/buzz-${VERSION}.tgz" "$CHART_REPO" |
| 237 | +
|
| 238 | + - name: Summary |
| 239 | + env: |
| 240 | + CHART_REPO: ${{ env.CHART_REPO }} |
| 241 | + VERSION: ${{ steps.ver.outputs.version }} |
| 242 | + run: | |
| 243 | + # CHART_REPO is oci://ghcr.io/block/buzz/charts; helm push appends the |
| 244 | + # chart name, so the install ref is .../charts/buzz. |
| 245 | + INSTALL_REF="${CHART_REPO}/buzz" |
| 246 | + { |
| 247 | + echo "### Published chart \`buzz\` \`${VERSION}\`" |
| 248 | + echo |
| 249 | + echo "**OCI ref:** \`${INSTALL_REF}:${VERSION}\`" |
| 250 | + echo |
| 251 | + echo "Install:" |
| 252 | + echo '```' |
| 253 | + echo "helm install buzz ${INSTALL_REF} --version ${VERSION}" |
| 254 | + echo '```' |
| 255 | + } >> "$GITHUB_STEP_SUMMARY" |
0 commit comments