From 5421cbf328579c01f515107ef295ae518071921a Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 01/10] fix: switch commit-lint and pr-lint off pull_request_target Signed-off-by: Steven Wade --- .github/workflows/commit-lint.yaml | 14 +++++++++++--- .github/workflows/pr-lint.yml | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/commit-lint.yaml b/.github/workflows/commit-lint.yaml index aad340e..fff4456 100644 --- a/.github/workflows/commit-lint.yaml +++ b/.github/workflows/commit-lint.yaml @@ -1,7 +1,7 @@ name: commit-lint on: - pull_request_target: + pull_request: types: - opened - edited @@ -14,7 +14,15 @@ permissions: jobs: commit-lint: name: commit-lint + # Dependabot's own commit messages don't pass commitlint's stricter + # subject-case rules (its PR titles do - pr-lint still runs for these). + # Keyed on the PR's actual author, not github.actor - a manual "update + # branch" would otherwise flip actor away from dependabot[bot] and make + # this job run for real on a Dependabot commit it was meant to exempt. + if: github.event.pull_request.user.login != 'dependabot[bot]' runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: wagoid/commitlint-github-action@3d28780bbf0365e29b144e272b2121204d5be5f3 # v6 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1 diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 073dfec..a0e06a3 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -1,7 +1,7 @@ name: pr-lint on: - pull_request_target: + pull_request: types: - opened - edited @@ -16,6 +16,6 @@ jobs: name: pr-lint runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 085ceba902e45cad07e7d252fc4fadf9473867de Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 02/10] fix: modernize output mechanism, fix over-permissioned README example Signed-off-by: Steven Wade --- README.md | 10 ++++++++-- kustdiff | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 969635d..e8015ca 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Kustomize Diff GitHub Action +[![test](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/test.yml/badge.svg)](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/test.yml) +[![lint](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/lint.yml/badge.svg)](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/lint.yml) +[![commit-lint](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/commit-lint.yaml/badge.svg)](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/commit-lint.yaml) +[![pr-lint](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/pr-lint.yml/badge.svg)](https://github.com/swade1987/github-action-kustomize-diff/actions/workflows/pr-lint.yml) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/swade1987/github-action-kustomize-diff/badge)](https://scorecard.dev/viewer/?uri=github.com/swade1987/github-action-kustomize-diff) + This GitHub Action builds and compares Kustomize configurations between the base and head of a Pull Request, posting the differences as a PR comment. This helps reviewers easily identify configuration changes in Kubernetes manifests. ## Why Use This Action? @@ -52,7 +58,7 @@ This approach correctly handles cases where changes have been made to the base b ## Usage -The below example will run `kustomize-diff` against your branch and commit the changes due to be applied back to your Pull Request. +The below example will run `kustomize-diff` against your branch and post the resulting diff as a comment on your Pull Request. ``` name: kustomize-diff @@ -65,7 +71,7 @@ jobs: kustomize-diff: permissions: pull-requests: write - contents: write + contents: read runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/kustdiff b/kustdiff index 04918e0..abe50fd 100755 --- a/kustdiff +++ b/kustdiff @@ -114,7 +114,6 @@ function main { debug_log "Resolved base reference: $base_ref_resolved (from $INPUT_BASE_REF)" # Build BASE output - local safe_base_ref=$(safe_filename "$INPUT_BASE_REF") local base_output_dir="$TMP_DIR/base" build_ref "$base_ref_resolved" "$base_output_dir" @@ -163,13 +162,20 @@ function main { output="$diff" fi - local escaped_output=${output//$'\n'/'%0A'} - - if [ ${#escaped_output} -gt 65000 ]; then - escaped_output="Output is greater than 65000 characters, and therefore too large to print as a github comment." + if [ ${#output} -gt 65000 ]; then + output="Output is greater than 65000 characters, and therefore too large to print as a github comment." fi - echo "::set-output name=diff::$escaped_output" + # $GITHUB_OUTPUT (not the old "::set-output::" workflow command, which + # GitHub deprecated) natively supports multiline values via this + # delimiter form - the value between the two matching lines is taken + # verbatim, real newlines included, so no manual %0A-style escaping is + # needed (or wanted: a consumer reading this value expects real newlines). + { + echo "diff<>"$GITHUB_OUTPUT" } # Print initial configuration From a7f8125bcd981be8baffbf5c8c4de836fd689211 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 03/10] ci: pin actions and base image, sync kustomize/helm with flux Signed-off-by: Steven Wade --- .github/workflows/release.yml | 6 +++--- Dockerfile | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cda9c19..1a8c996 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,16 +17,16 @@ jobs: id-token: write # to enable use of OIDC for npm provenance steps: - name: checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false token: ${{ secrets.RELEASE_TOKEN }} - name: setup node.js - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "lts/*" - name: release - uses: cycjimmy/semantic-release-action@v4 + uses: cycjimmy/semantic-release-action@16ca923e6ccbb50770c415a0ccd43709a8c5f7a4 # v4.2.2 env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 32817ca..8355b5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,20 @@ -FROM alpine:3.23.3 +FROM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b RUN apk update && apk --no-cache add bash curl git SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# kustomize/helm versions are kept in step with what flux itself is built +# against - see scripts/upgrade-flux-versions.sh and +# .github/workflows/monitor-flux-versions.yml, which derive these from +# flux2's own release rather than chasing each tool's latest independently. ARG KUSTOMIZE=5.8.1 RUN curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE}/kustomize_v${KUSTOMIZE}_linux_amd64.tar.gz | \ tar xz && mv kustomize /usr/local/bin/kustomize -ARG HELM_V3=3.19.2 -RUN curl -sSL https://get.helm.sh/helm-v${HELM_V3}-linux-amd64.tar.gz | \ -tar xz && mv linux-amd64/helm /usr/local/bin/helmv3 && rm -rf linux-amd64 && ln -s /usr/local/bin/helmv3 /usr/local/bin/helm && helm version +ARG HELM_VERSION=3.19.2 +RUN curl -sSL https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz | \ +tar xz && mv linux-amd64/helm /usr/local/bin/helm && rm -rf linux-amd64 && helm version RUN rm -rf /var/cache/apk/* From 585d395dd15b36f112c2eab1ab4f618cdd63b4b8 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 04/10] ci: add hadolint and shellcheck Signed-off-by: Steven Wade --- .github/workflows/lint.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..1b6c433 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: lint + +on: + pull_request: + branches: + - main + push: + branches: + - main + +concurrency: + group: lint-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + hadolint: + name: hadolint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0 + with: + dockerfile: Dockerfile + failure-threshold: error + + shellcheck: + name: shellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 + with: + severity: error From 1440fcee801543c6e9e7a8c62567c2d372c7e344 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 05/10] ci: add build/e2e test against the fixture repo Signed-off-by: Steven Wade --- .github/workflows/test.yml | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..489e6c4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,81 @@ +name: test + +on: + pull_request: + +concurrency: + group: test-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: build + runs-on: ubuntu-latest + steps: + - name: Checkout kustomize-diff-test + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: swade1987/kustomize-diff-test + path: fixture + fetch-depth: 0 + + - name: Checkout this action + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: action + + - name: Build the action image + working-directory: action + run: docker build -t kustomize-diff:ci . + + # Deterministic change on a throwaway local branch, never pushed - a + # known-good target this test can assert against, independent of + # whatever state the fixture repo's own branches happen to be in. + - name: Create a known change on a throwaway branch + working-directory: fixture + run: | + git config user.email "ci@github-actions.local" + git config user.name "github-actions[ci]" + git checkout -b e2e-test-branch + sed -i 's/ENV=base/ENV=base-e2e-test/' kustomize/base/kustomization.yaml + git add -A + git commit -sm "test: e2e change" + + - name: Run the action against the fixture + working-directory: fixture + run: | + docker run --rm \ + -v "$(pwd):/github/workspace" \ + -v "$RUNNER_TEMP/output.txt:/github_output.txt" \ + -w /github/workspace \ + -e GITHUB_WORKSPACE=/github/workspace \ + -e GITHUB_OUTPUT=/github_output.txt \ + -e INPUT_BASE_REF=main \ + -e INPUT_HEAD_REF=e2e-test-branch \ + -e INPUT_ROOT_DIR=./kustomize \ + -e INPUT_MAX_DEPTH=2 \ + -e DEBUG=false \ + kustomize-diff:ci + env: + RUNNER_TEMP: ${{ runner.temp }} + + - name: Assert the diff output is correct + run: | + touch "$RUNNER_TEMP/output.txt" + if ! grep -q "^diff<<" "$RUNNER_TEMP/output.txt"; then + echo "::error::GITHUB_OUTPUT was never written - the action produced no diff output at all" + cat "$RUNNER_TEMP/output.txt" + exit 1 + fi + if ! grep -q -- "- ENV: base$" "$RUNNER_TEMP/output.txt" || ! grep -q -- "+ ENV: base-e2e-test$" "$RUNNER_TEMP/output.txt"; then + echo "::error::Expected diff content not found in the action's output" + cat "$RUNNER_TEMP/output.txt" + exit 1 + fi + echo "Diff output looks correct:" + cat "$RUNNER_TEMP/output.txt" + env: + RUNNER_TEMP: ${{ runner.temp }} From 722884c11a667bb428efdaec48557dba495fd00f Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 06/10] ci: add OpenSSF Scorecard analysis workflow Signed-off-by: Steven Wade --- .github/workflows/scorecard.yml | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..0a922ff --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,42 @@ +name: Scorecard analysis workflow +on: + push: + branches: + - main + schedule: + - cron: '30 1 * * 6' + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: "Upload artifact" + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + sarif_file: results.sarif From db1bf5a95348304e080bb1d0cf3b8ef91d9c39ed Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 07/10] chore: add Dependabot config for actions and docker Signed-off-by: Steven Wade --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..941ec36 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "ci" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "chore" From dd6c7e6101a1d9ceb82e9f053d833b6c5cde1ce3 Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 08/10] docs: add SECURITY.md and CODEOWNERS Signed-off-by: Steven Wade --- .github/CODEOWNERS | 4 ++++ SECURITY.md | 15 +++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 SECURITY.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3ec6ae0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Names the repo owner for GitHub's own UI (review requests, blame). +# The branch ruleset does not require owner review before merge - this +# file's effect is currently informational only. +* @swade1987 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b4be19e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported versions + +This action ships one rolling `latest` semantic-release version; there's no long-term-support branch to track. Security fixes land on `main` and are published as the next tagged release. + +## Reporting a vulnerability + +Please report security issues privately rather than opening a public GitHub issue: use [GitHub's private vulnerability reporting](https://github.com/swade1987/github-action-kustomize-diff/security/advisories/new) for this repository (Security tab → Report a vulnerability). + +Include what you'd include in any good bug report: the affected version or commit, what you found, and how to reproduce it. We'll acknowledge new reports within 5 business days and aim to have a fix or mitigation plan within 30 days, depending on severity. + +## Scope + +This is a Docker-based GitHub Action: consumers build it fresh from a tagged ref each time it's used (there's no pre-built image published to a registry). It checks out and merges the caller's own base/head refs locally to build a diff - reports about that logic, the Dockerfile, or the CI/release pipeline are in scope. Using this action with `pull_request_target` on an untrusted (fork) PR is a misuse of the trigger, not a vulnerability in the action itself - the documented usage in the README uses plain `pull_request`. From 88314d871d1d042fa1368c0fc745bfcef8bf15de Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:26:07 +0100 Subject: [PATCH 09/10] feat: automate kustomize/helm version sync with flux Signed-off-by: Steven Wade --- .github/workflows/monitor-flux-versions.yml | 50 ++++ scripts/lib/signed-pr.sh | 101 ++++++++ scripts/upgrade-flux-versions.sh | 261 ++++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 .github/workflows/monitor-flux-versions.yml create mode 100644 scripts/lib/signed-pr.sh create mode 100755 scripts/upgrade-flux-versions.sh diff --git a/.github/workflows/monitor-flux-versions.yml b/.github/workflows/monitor-flux-versions.yml new file mode 100644 index 0000000..b5f6567 --- /dev/null +++ b/.github/workflows/monitor-flux-versions.yml @@ -0,0 +1,50 @@ +name: Monitor Flux Versions + +on: + schedule: + - cron: '0 1 * * 1' # Weekly, Mondays at 01:00 - flux releases far less often than daily + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + check-flux-versions: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + token: ${{ secrets.RELEASE_TOKEN }} + + - name: Setup GitHub CLI + run: | + gh auth login --with-token <<< "${{ secrets.RELEASE_TOKEN }}" + + - name: Check for an existing upgrade PR + id: check-prs + run: | + EXISTING_PRS=$(gh pr list --json title --jq '.[] | select(.title | contains("sync kustomize/helm to flux")) | .title') + if [ -n "$EXISTING_PRS" ]; then + echo "Found an existing sync PR, skipping until it's merged:" + echo "$EXISTING_PRS" + echo "has_pr=true" >> "$GITHUB_OUTPUT" + else + echo "has_pr=false" >> "$GITHUB_OUTPUT" + fi + + - name: Configure Git + if: steps.check-prs.outputs.has_pr == 'false' + run: | + git config user.name "Steve Wade" + git config user.email "steven@stevenwade.co.uk" + + - name: Run upgrade script + if: steps.check-prs.outputs.has_pr == 'false' + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + chmod +x ./scripts/upgrade-flux-versions.sh + ./scripts/upgrade-flux-versions.sh --execute diff --git a/scripts/lib/signed-pr.sh b/scripts/lib/signed-pr.sh new file mode 100644 index 0000000..7ac6f93 --- /dev/null +++ b/scripts/lib/signed-pr.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +############################################################################### +# Library : signed-pr.sh +# Description : Pushes an empty branch, then commits the working tree's +# current changes onto it via GitHub's own API, and opens +# a pull request. +############################################################################### +# +# A plain "git commit" run in CI produces an unsigned commit (there's no +# signing key configured on the runner), which fails this repo's +# "required_signatures" branch ruleset rule and blocks the resulting PR from +# merging - it doesn't matter that every status check passes. GitHub's +# createCommitOnBranch GraphQL mutation sidesteps this entirely: it creates +# the commit server-side, signed by GitHub itself, the same way a squash +# merge is. The trade-off is that it can only add/replace file *content* at +# a path (no permission-bit or rename changes) - fine for this script, which +# only ever edits an existing text file in place. +# +# DCO checks the commit *message* for a "Signed-off-by:" trailer - a +# separate thing from the commit's cryptographic signature, which this +# mutation already provides regardless. "git commit -s" used to add this +# trailer automatically; creating the commit via the API instead means it +# has to be added explicitly here. +# +# Requires "log" to already be defined by the sourcing script, and expects +# to be called with the working tree already containing the uncommitted +# changes to ship - it reads their current on-disk content directly, not a +# git diff. +# +# Usage: create_signed_pr + +create_signed_pr() { + local branch_name="$1" commit_headline="$2" pr_title="$3" pr_body="$4" + + local base_sha repo + base_sha="$(git rev-parse HEAD)" + repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner')" + + log "INFO" "Pushing branch..." + git push origin "HEAD:refs/heads/${branch_name}" || { log "ERROR" "Failed to push branch"; exit 1; } + + local changed_files + changed_files="$(git diff --name-only HEAD)" + if [[ -z "${changed_files}" ]]; then + log "ERROR" "No changed files to commit" + exit 1 + fi + + log "INFO" "Building a signed commit via the GitHub API..." + local file_changes_json + file_changes_json="$( + while IFS= read -r f; do + jq -n --arg path "$f" --arg contents "$(base64 <"$f" | tr -d '\n')" \ + '{path: $path, contents: $contents}' + done <<<"${changed_files}" | jq -s '{additions: .}' + )" + + local signoff_name signoff_email + signoff_name="$(git config user.name)" + signoff_email="$(git config user.email)" + + local payload response commit_oid + payload="$(jq -n \ + --arg repo "$repo" \ + --arg branch "$branch_name" \ + --arg headline "$commit_headline" \ + --arg body "Signed-off-by: ${signoff_name} <${signoff_email}>" \ + --arg oid "$base_sha" \ + --argjson fileChanges "$file_changes_json" \ + '{ + query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }", + variables: { + input: { + branch: { repositoryNameWithOwner: $repo, branchName: $branch }, + message: { headline: $headline, body: $body }, + expectedHeadOid: $oid, + fileChanges: $fileChanges + } + } + }' + )" + + response="$(echo "${payload}" | gh api graphql --input -)" + commit_oid="$(echo "${response}" | jq -r '.data.createCommitOnBranch.commit.oid // empty')" + + if [[ -z "${commit_oid}" ]]; then + log "ERROR" "Failed to create signed commit: ${response}" + exit 1 + fi + log "SUCCESS" "Created signed commit ${commit_oid}" + + log "INFO" "Creating pull request..." + gh pr create \ + --title "$pr_title" \ + --body "$pr_body" \ + --base main \ + --head "$branch_name" || { log "ERROR" "Failed to create PR"; exit 1; } + + log "SUCCESS" "Successfully created pull request" +} diff --git a/scripts/upgrade-flux-versions.sh b/scripts/upgrade-flux-versions.sh new file mode 100755 index 0000000..252be22 --- /dev/null +++ b/scripts/upgrade-flux-versions.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash + +############################################################################### +# Script Name : upgrade-flux-versions.sh +# Description : Creates a pull request to keep the Dockerfile's +# kustomize/helm versions in step with what flux itself +# is built against +############################################################################### +# +# Flux's own release doesn't state which kustomize/Helm CLI versions it was +# built against directly - it has to be derived: +# +# 1. The flux2 release body has a "Components changelog" section listing +# kustomize-controller and helm-controller at their own versions. +# 2. kustomize-controller's go.mod, at that version, carries a literal +# "// Pin kustomize to vX.Y.Z" comment - the real kustomize CLI version. +# 3. helm-controller's go.mod, at that version, imports helm.sh/helm/vN - +# the module version *is* the Helm CLI release version. +# +# This keeps the action's kustomize/Helm aligned with what Flux itself is +# compatible with, rather than independently chasing each tool's own latest - +# including across a Helm major version move, since the ARG is named +# HELM_VERSION, not HELM_V3. +# +# Unlike a tool this repo tracks by its own version number (there's no +# "flux" ARG here - this repo only bundles kustomize/helm), there's nothing +# to compare flux's own release against for a "nothing to do" check. Instead +# the script always derives the current target kustomize/helm versions from +# flux's latest release and compares those directly to what's already in the +# Dockerfile. + +set -o errexit +set -o errtrace +set -o nounset +set -o pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" + +# shellcheck source=lib/signed-pr.sh +source "${SCRIPT_DIR}/scripts/lib/signed-pr.sh" + +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly NC='\033[0m' + +VERBOSE=true +DRY_RUN=true +LOG_FILE="${SCRIPT_DIR}/scripts/${SCRIPT_NAME}.log" +readonly DOCKERFILE="Dockerfile" + +usage() { + cat <> "${LOG_FILE}" + fi + + if [[ "${VERBOSE}" == "true" ]] || [[ "${level}" == "ERROR" ]]; then + case "${level}" in + ERROR) echo -e "${RED}${timestamp} [${level}] ${message}${NC}" >&2 ;; + WARN) echo -e "${YELLOW}${timestamp} [${level}] ${message}${NC}" ;; + SUCCESS) echo -e "${GREEN}${timestamp} [${level}] ${message}${NC}" ;; + *) echo "${timestamp} [${level}] ${message}" ;; + esac + fi +} + +check_requirements() { + local failed=false + for cmd in "$@"; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + log "ERROR" "${cmd} is required but not installed." + failed=true + fi + done + [[ "${failed}" == "true" ]] && exit 1 + return 0 +} + +fetch_file() { + local repo="$1" path="$2" ref="$3" + gh api "repos/${repo}/contents/${path}?ref=${ref}" --jq '.content' | base64 -d +} + +get_latest_flux_tag() { + gh api repos/fluxcd/flux2/releases/latest --jq '.tag_name' +} + +get_current_arg() { + local name="$1" + grep -E "^ARG ${name}=" "${DOCKERFILE}" | head -1 | cut -d= -f2 +} + +get_component_version() { + local body="$1" component="$2" + awk '/^## Components changelog/{f=1;next} /^## /{f=0} f' <<<"$body" \ + | grep -E "^- ${component} \[v" \ + | grep -oE '\[v[0-9]+\.[0-9]+\.[0-9]+\]' \ + | tr -d '[]v' +} + +get_kustomize_version() { + local kc_tag="$1" content + content="$(fetch_file "fluxcd/kustomize-controller" "go.mod" "v${kc_tag}")" + grep -m1 "// Pin kustomize to v" <<<"$content" \ + | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' \ + | tr -d v +} + +# Prints " ", e.g. "4 4.2.4" +get_helm_version() { + local hc_tag="$1" content + content="$(fetch_file "fluxcd/helm-controller" "go.mod" "v${hc_tag}")" + grep -m1 -E '^\s*helm\.sh/helm/v[0-9]+ v' <<<"$content" \ + | sed -E 's|.*helm\.sh/helm/v([0-9]+) v([0-9.]+).*|\1 \2|' +} + +show_config() { + log "INFO" "=== Configuration ===" + log "INFO" "flux release checked: v${FLUX_VERSION}" + log "INFO" "kustomize target: ${KUSTOMIZE_VERSION} (current: ${CURRENT_KUSTOMIZE})" + log "INFO" "Helm target: ${HELM_VERSION}, v${HELM_MAJOR} line (current: ${CURRENT_HELM})" + log "INFO" "Mode: $([ "$DRY_RUN" = true ] && echo 'DRY RUN' || echo 'EXECUTE')" + log "INFO" "====================" +} + +update_dockerfile() { + if [[ "${DRY_RUN}" == "true" ]]; then + log "INFO" "Would update KUSTOMIZE/HELM_VERSION ARGs in ${DOCKERFILE}" + return 0 + fi + sed -i.bak -E "s/^ARG KUSTOMIZE=.*/ARG KUSTOMIZE=${KUSTOMIZE_VERSION}/" "${DOCKERFILE}" + sed -i.bak -E "s/^ARG HELM_VERSION=.*/ARG HELM_VERSION=${HELM_VERSION}/" "${DOCKERFILE}" + rm -f "${DOCKERFILE}.bak" + log "SUCCESS" "Updated ${DOCKERFILE}" +} + +create_pull_request() { + if [[ "${DRY_RUN}" == "true" ]]; then + log "INFO" "Would create pull request for kustomize/Helm version upgrade" + return 0 + fi + + local branch_name="flux/sync-versions-${FLUX_VERSION}" + local commit_message="feat: sync kustomize/helm to flux v${FLUX_VERSION}" + + local body="Keeps kustomize and Helm in step with flux v${FLUX_VERSION}." + + if [[ "${KUSTOMIZE_VERSION}" != "${CURRENT_KUSTOMIZE}" ]]; then + body="${body} Bumps kustomize from ${CURRENT_KUSTOMIZE} to ${KUSTOMIZE_VERSION}, derived from kustomize-controller v${KC_VERSION}'s pinned version (flux v${FLUX_VERSION}'s Components changelog)." + fi + + if [[ "${HELM_VERSION}" != "${CURRENT_HELM}" ]]; then + body="${body} Bumps Helm from ${CURRENT_HELM} to ${HELM_VERSION} (helm-controller v${HC_VERSION} pins helm.sh/helm/v${HELM_MAJOR} v${HELM_VERSION})." + if [[ "${HELM_MAJOR}" != "${CURRENT_HELM_MAJOR}" ]]; then + body="${body} Note: this crosses a Helm major version (v${CURRENT_HELM_MAJOR} -> v${HELM_MAJOR}) - Helm's CLI surface can differ across majors, so review this one a bit more closely than a routine patch bump." + fi + fi + + create_signed_pr "$branch_name" "$commit_message" "$commit_message" "$body" +} + +main() { + check_requirements "git" "gh" "jq" "sed" "awk" + + log "INFO" "Fetching latest flux2 release..." + local latest_tag + latest_tag="$(get_latest_flux_tag)" + FLUX_VERSION="${latest_tag#v}" + readonly FLUX_VERSION + + log "INFO" "Fetching v${FLUX_VERSION} release notes..." + local body + body="$(gh api "repos/fluxcd/flux2/releases/tags/${latest_tag}" --jq '.body')" + + KC_VERSION="$(get_component_version "$body" "kustomize-controller")" + HC_VERSION="$(get_component_version "$body" "helm-controller")" + readonly KC_VERSION HC_VERSION + + if [[ -z "${KC_VERSION}" || -z "${HC_VERSION}" ]]; then + log "ERROR" "Could not find kustomize-controller/helm-controller versions in the Components changelog" + exit 1 + fi + + log "INFO" "Deriving kustomize version from kustomize-controller v${KC_VERSION}..." + KUSTOMIZE_VERSION="$(get_kustomize_version "${KC_VERSION}")" + readonly KUSTOMIZE_VERSION + + log "INFO" "Deriving Helm version from helm-controller v${HC_VERSION}..." + read -r HELM_MAJOR HELM_VERSION <<<"$(get_helm_version "${HC_VERSION}")" + readonly HELM_MAJOR HELM_VERSION + + if [[ -z "${KUSTOMIZE_VERSION}" || -z "${HELM_VERSION}" ]]; then + log "ERROR" "Could not derive kustomize/Helm versions from component go.mod files" + exit 1 + fi + + CURRENT_KUSTOMIZE="$(get_current_arg KUSTOMIZE)" + CURRENT_HELM="$(get_current_arg HELM_VERSION)" + readonly CURRENT_KUSTOMIZE CURRENT_HELM + CURRENT_HELM_MAJOR="${CURRENT_HELM%%.*}" + readonly CURRENT_HELM_MAJOR + + show_config + + if [[ "${KUSTOMIZE_VERSION}" == "${CURRENT_KUSTOMIZE}" && "${HELM_VERSION}" == "${CURRENT_HELM}" ]]; then + log "INFO" "Already in sync with flux v${FLUX_VERSION}; nothing to do." + exit 0 + fi + + update_dockerfile + create_pull_request +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + -e|--execute) + DRY_RUN=false + shift + ;; + -l|--log) + LOG_FILE="$2" + shift 2 + ;; + *) + log "ERROR" "Unexpected argument: $1" + usage + exit 1 + ;; + esac + done +} + +parse_args "$@" +main From 3988cc1d75754d6863f3a56c795d3610c38b2b7b Mon Sep 17 00:00:00 2001 From: Steven Wade Date: Thu, 3 Sep 2026 01:29:30 +0100 Subject: [PATCH 10/10] fix: pre-create the GITHUB_OUTPUT bind-mount target Signed-off-by: Steven Wade --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 489e6c4..1a088dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,12 @@ jobs: - name: Run the action against the fixture working-directory: fixture run: | + # The bind-mount source must exist as a file before "docker run" + # creates the mount, or Docker creates it as a directory instead - + # which silently breaks the container's write to it. Verified + # live: this exact failure happened on a real CI run once, before + # this touch existed. + touch "$RUNNER_TEMP/output.txt" docker run --rm \ -v "$(pwd):/github/workspace" \ -v "$RUNNER_TEMP/output.txt:/github_output.txt" \