Skip to content

Commit 8d3de97

Browse files
EliShteinmanclaude
andcommitted
feat(ci): pin actions to SHAs + opt-in Helm chart publish phase
Two related hardenings to the airgap-build workflow. 1. Pin every `uses:` to an immutable commit SHA with a readable tag comment, so a future tag move by a third-party maintainer cannot silently inject code into a workflow that holds DOCKERHUB_TOKEN and contents:write. Where a newer major was available and verified compatible with our inputs, also bumped: actions/checkout v4.3.1 -> v6.0.2 actions/cache v4.3.0 -> v5.0.5 actions/upload-artifact v4.6.2 -> v7.0.1 actions/download-artifact v4.3.0 -> v8.0.1 docker/{qemu,buildx,login,build-push} unchanged (already latest) The four `actions/*` bumps move from Node 20 to Node 24 runtime, which also clears the "Node 20 is deprecated" warnings on every run. 2. New opt-in `publish_chart` input (default false) wires a Phase 5 into the workflow that auto-bumps the Helm chart patch version and publishes it as an OCI artifact — the manual BUILD.md step 5 + step 6 flow, automated. Order is bump-files -> git commit & push -> helm publish, so a transient OCI failure leaves the bump in git and the user reruns helm push by hand. The bump uses context-aware sed (anchored on `^appVersion:` and `<hex>-unprivileged`) instead of a blind global replace of the old hash, so a stray hex string elsewhere in the target files cannot be corrupted. NEW_HASH is validated as 9-char lowercase hex before any interpolation into sed. Both the commit and the publish use marketplace actions (stefanzweifel/git-auto-commit-action, appany/helm-oci-chart-releaser), pinned to SHA per (1). Skipped automatically on partial builds (variant/platforms != both or tag_override set) since those are testing flows that should not produce a chart release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f11328a commit 8d3de97

1 file changed

Lines changed: 175 additions & 23 deletions

File tree

.github/workflows/airgap-build.yml

Lines changed: 175 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ on:
4141
normal builds.
4242
type: string
4343
default: ''
44+
publish_chart:
45+
description: >-
46+
After the image push, also auto-bump the Helm chart patch version,
47+
commit the update back to this branch, and publish the chart as an
48+
OCI artifact. Skipped automatically on partial builds and override
49+
tags. For non-patch bumps build the chart manually per BUILD.md.
50+
type: boolean
51+
default: false
4452

4553
env:
4654
REPO: a0533057932/redis-docs
@@ -65,7 +73,7 @@ jobs:
6573
rs_versions: ${{ steps.matrix.outputs.rs }}
6674
redisvl_versions: ${{ steps.matrix.outputs.redisvl }}
6775
steps:
68-
- uses: actions/checkout@v4
76+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
6977

7078
- name: Compute build params
7179
id: meta
@@ -211,7 +219,7 @@ jobs:
211219
permissions:
212220
contents: read
213221
steps:
214-
- uses: actions/checkout@v4
222+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
215223
with:
216224
fetch-depth: 0 # full history for Hugo's enableGitInfo
217225

@@ -226,7 +234,7 @@ jobs:
226234

227235
- name: Restore node_modules
228236
id: cache-npm
229-
uses: actions/cache@v4
237+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
230238
with:
231239
path: node_modules
232240
key: npm-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json') }}
@@ -237,7 +245,7 @@ jobs:
237245

238246
- name: Restore python venv
239247
id: cache-pip
240-
uses: actions/cache@v4
248+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
241249
with:
242250
path: ./venv
243251
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
@@ -250,7 +258,7 @@ jobs:
250258
251259
- name: Restore examples + data/examples.json
252260
id: cache-examples
253-
uses: actions/cache@v4
261+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
254262
with:
255263
path: |
256264
examples
@@ -322,7 +330,7 @@ jobs:
322330
323331
- name: Restore Hugo output cache
324332
id: cache-hugo
325-
uses: actions/cache@v4
333+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
326334
with:
327335
path: output
328336
key: hugo-latest-${{ steps.key.outputs.key }}
@@ -343,7 +351,7 @@ jobs:
343351
- name: Stage output as artifact
344352
run: tar -czf /tmp/out.tar.gz output
345353

346-
- uses: actions/upload-artifact@v4
354+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
347355
with:
348356
name: build-latest
349357
path: /tmp/out.tar.gz
@@ -375,7 +383,7 @@ jobs:
375383
matrix:
376384
include: ${{ fromJSON(needs.setup.outputs.matrix) }}
377385
steps:
378-
- uses: actions/checkout@v4
386+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
379387
with:
380388
fetch-depth: 0
381389

@@ -390,7 +398,7 @@ jobs:
390398

391399
- name: Restore node_modules
392400
id: cache-npm
393-
uses: actions/cache@v4
401+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
394402
with:
395403
path: node_modules
396404
key: npm-${{ runner.os }}-${{ hashFiles('package.json', 'package-lock.json') }}
@@ -401,7 +409,7 @@ jobs:
401409

402410
- name: Restore python venv
403411
id: cache-pip
404-
uses: actions/cache@v4
412+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
405413
with:
406414
path: ./venv
407415
key: pip-${{ runner.os }}-${{ hashFiles('requirements.txt') }}
@@ -414,7 +422,7 @@ jobs:
414422
415423
- name: Restore examples + data/examples.json
416424
id: cache-examples
417-
uses: actions/cache@v4
425+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
418426
with:
419427
path: |
420428
examples
@@ -485,7 +493,7 @@ jobs:
485493
486494
- name: Restore Hugo output cache
487495
id: cache-hugo
488-
uses: actions/cache@v4
496+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
489497
with:
490498
path: output
491499
key: hugo-${{ matrix.name }}-${{ steps.key.outputs.key }}
@@ -555,7 +563,7 @@ jobs:
555563
done
556564
tar -czf /tmp/out.tar.gz -C /tmp/stage output
557565
558-
- uses: actions/upload-artifact@v4
566+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
559567
with:
560568
name: build-${{ matrix.name }}
561569
path: /tmp/out.tar.gz
@@ -572,7 +580,7 @@ jobs:
572580
permissions:
573581
contents: read
574582
steps:
575-
- uses: actions/checkout@v4
583+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
576584

577585
- name: Install system deps
578586
run: sudo apt-get install -y --no-install-recommends rsync python3-venv
@@ -582,7 +590,7 @@ jobs:
582590
python3 -m venv /tmp/venv
583591
/tmp/venv/bin/pip install --quiet -r requirements.txt
584592
585-
- uses: actions/download-artifact@v4
593+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
586594
with:
587595
path: /tmp/builds
588596
pattern: build-*
@@ -636,7 +644,7 @@ jobs:
636644
tar -czf /tmp/public.tar.gz public
637645
echo "Files: $(find public -type f | wc -l), Size: $(du -sh public | cut -f1)"
638646
639-
- uses: actions/upload-artifact@v4
647+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
640648
with:
641649
name: public
642650
path: /tmp/public.tar.gz
@@ -657,13 +665,13 @@ jobs:
657665
permissions:
658666
contents: read
659667
steps:
660-
- uses: actions/checkout@v4
668+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
661669
with:
662670
sparse-checkout: |
663671
Dockerfile.runtime
664672
sparse-checkout-cone-mode: false
665673

666-
- uses: actions/download-artifact@v4
674+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
667675
with:
668676
name: public
669677
path: /tmp/
@@ -673,21 +681,21 @@ jobs:
673681

674682
- name: Set up QEMU
675683
if: inputs.platforms != 'amd64'
676-
uses: docker/setup-qemu-action@v4
684+
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
677685
with:
678686
platforms: arm64
679687

680-
- uses: docker/setup-buildx-action@v4
688+
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
681689

682-
- uses: docker/login-action@v4
690+
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
683691
with:
684692
username: ${{ vars.DOCKERHUB_USERNAME }}
685693
password: ${{ secrets.DOCKERHUB_TOKEN }}
686694

687695
- name: Build & push privileged
688696
id: push_priv
689697
if: inputs.variant != 'unprivileged'
690-
uses: docker/build-push-action@v7
698+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
691699
with:
692700
context: .
693701
file: Dockerfile.runtime
@@ -703,7 +711,7 @@ jobs:
703711
- name: Build & push unprivileged
704712
id: push_unpriv
705713
if: inputs.variant != 'privileged'
706-
uses: docker/build-push-action@v7
714+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
707715
with:
708716
context: .
709717
file: Dockerfile.runtime
@@ -794,3 +802,147 @@ jobs:
794802
echo '```'
795803
fi
796804
} >> "$GITHUB_STEP_SUMMARY"
805+
806+
# =========================================================================
807+
# Phase 5: publish_chart — opt-in.
808+
# Order: bump files -> git commit & push -> helm publish to OCI.
809+
# If helm publish fails the bump is already in git; rerun helm push by hand.
810+
# Skipped on partial builds (variant/platforms != both, or tag_override set).
811+
# =========================================================================
812+
publish_chart:
813+
name: 5. Publish Helm chart (opt-in)
814+
needs: [setup, docker]
815+
if: >-
816+
inputs.publish_chart &&
817+
inputs.variant == 'both' &&
818+
inputs.platforms == 'both' &&
819+
inputs.tag_override == ''
820+
runs-on: ubuntu-24.04
821+
permissions:
822+
contents: write
823+
steps:
824+
# Default ref (github.ref) and persist-credentials (true) are what we
825+
# need: git-auto-commit-action reuses the auth header to push back to
826+
# this branch.
827+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
828+
829+
- name: Bump chart files
830+
id: bump
831+
env:
832+
NEW_HASH: ${{ needs.setup.outputs.hash }}
833+
run: |
834+
set -euo pipefail
835+
CHART=helm/redis-docs/Chart.yaml
836+
837+
# NEW_HASH is interpolated into sed below, so it must match the
838+
# setup phase's convention exactly (git rev-parse --short=9). A
839+
# malformed hash here would corrupt every targeted line.
840+
if [[ ! "$NEW_HASH" =~ ^[a-f0-9]{9}$ ]]; then
841+
echo "::error::NEW_HASH '$NEW_HASH' is not 9-char lowercase hex. Setup phase produced an unexpected value."
842+
exit 1
843+
fi
844+
845+
OLD_VERSION=$(grep '^version:' "$CHART" | awk '{print $2}')
846+
OLD_HASH=$(sed -nE 's/^appVersion:[[:space:]]*"([^"]+)".*/\1/p' "$CHART")
847+
848+
# OLD_VERSION drives the patch increment ($((PATCH + 1))), so any
849+
# non-X.Y.Z form crashes the arithmetic mid-bump. Reject early.
850+
if [[ ! "$OLD_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
851+
echo "::error::version '$OLD_VERSION' is not plain X.Y.Z. Pre-release / build-meta suffixes are not auto-bumped — bump manually per BUILD.md step 5."
852+
exit 1
853+
fi
854+
855+
# OLD_HASH is used only for the same-hash skip check; it is no
856+
# longer interpolated into sed, so loose validation suffices.
857+
if [ -z "$OLD_HASH" ]; then
858+
echo "::error::Could not read appVersion from $CHART. Format may have changed."
859+
exit 1
860+
fi
861+
862+
{
863+
echo "old_version=$OLD_VERSION"
864+
echo "old_hash=$OLD_HASH"
865+
} >> "$GITHUB_OUTPUT"
866+
867+
if [ "$OLD_HASH" = "$NEW_HASH" ]; then
868+
echo "appVersion already $NEW_HASH — nothing to publish."
869+
echo "changed=false" >> "$GITHUB_OUTPUT"
870+
exit 0
871+
fi
872+
873+
IFS=. read -r MAJ MIN PATCH <<<"$OLD_VERSION"
874+
NEW_VERSION="$MAJ.$MIN.$((PATCH + 1))"
875+
876+
# Context-aware replacements. Each pattern matches exactly the
877+
# lines BUILD.md step 5 lists, so a stray hex string elsewhere in
878+
# these files (release notes, examples, etc.) can't be corrupted.
879+
sed -i -E "s/^appVersion:[[:space:]]*\"[a-f0-9]+\"/appVersion: \"${NEW_HASH}\"/" "$CHART"
880+
sed -i -E "s/[a-f0-9]+-unprivileged/${NEW_HASH}-unprivileged/g" \
881+
helm/redis-docs/examples/values-openshift-airgapped.yaml \
882+
helm/redis-docs/README.md \
883+
helm/redis-docs/README-he.md
884+
sed -i "s/^version: .*/version: ${NEW_VERSION}/" "$CHART"
885+
886+
{
887+
echo "new_version=$NEW_VERSION"
888+
echo "new_hash=$NEW_HASH"
889+
echo "changed=true"
890+
} >> "$GITHUB_OUTPUT"
891+
892+
# Skip when the bump step short-circuited (changed=false because the
893+
# appVersion was already current). GHA implicitly ANDs success() with
894+
# any step-level `if:` that does not contain a status function — no
895+
# need to repeat it here.
896+
- name: Commit bump
897+
if: steps.bump.outputs.changed == 'true'
898+
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
899+
with:
900+
commit_message: "chore(helm): bump chart ${{ steps.bump.outputs.new_version }}, appVersion ${{ steps.bump.outputs.new_hash }}"
901+
file_pattern: helm/redis-docs/
902+
903+
- name: Publish chart to OCI
904+
if: steps.bump.outputs.changed == 'true'
905+
uses: appany/helm-oci-chart-releaser@d94988c92bed2e09c6113981f15f8bb495d10943 # v0.5.0
906+
with:
907+
name: redis-docs
908+
repository: ${{ vars.DOCKERHUB_USERNAME }}
909+
tag: ${{ steps.bump.outputs.new_version }}
910+
path: helm/redis-docs
911+
registry: registry-1.docker.io
912+
registry_username: ${{ vars.DOCKERHUB_USERNAME }}
913+
registry_password: ${{ secrets.DOCKERHUB_TOKEN }}
914+
915+
- name: Write chart summary
916+
if: always()
917+
env:
918+
STATUS: ${{ job.status }}
919+
CHANGED: ${{ steps.bump.outputs.changed }}
920+
OLD_VERSION: ${{ steps.bump.outputs.old_version }}
921+
NEW_VERSION: ${{ steps.bump.outputs.new_version }}
922+
NEW_HASH: ${{ steps.bump.outputs.new_hash }}
923+
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }}
924+
run: |
925+
{
926+
echo ""
927+
echo "## Helm chart publish: \`$STATUS\`"
928+
echo ""
929+
if [ "$STATUS" = "success" ] && [ "$CHANGED" = "true" ]; then
930+
echo "| Field | Value |"
931+
echo "|-------|-------|"
932+
echo "| version | \`$OLD_VERSION\` → \`$NEW_VERSION\` |"
933+
echo "| appVersion | \`$NEW_HASH\` |"
934+
echo "| OCI ref | \`oci://registry-1.docker.io/$DOCKERHUB_USERNAME/redis-docs:$NEW_VERSION\` |"
935+
elif [ "$STATUS" = "success" ]; then
936+
echo "Chart appVersion already up to date — nothing pushed."
937+
elif [ "$CHANGED" = "true" ]; then
938+
echo "⚠️ Bump committed (or attempted) but a later step failed."
939+
echo ""
940+
echo "If \`Commit bump\` succeeded but \`Publish chart\` failed, run manually:"
941+
echo '```bash'
942+
echo "helm package helm/redis-docs/ -d /tmp/"
943+
echo "helm push /tmp/redis-docs-${NEW_VERSION}.tgz oci://registry-1.docker.io/$DOCKERHUB_USERNAME"
944+
echo '```'
945+
else
946+
echo "❌ Bump step failed (validation or sed) — nothing committed, nothing published. See step logs."
947+
fi
948+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)