From a29e6c6364660e84a855410b0f9cd6511cf21d1d Mon Sep 17 00:00:00 2001 From: Andy Potanin Date: Sat, 29 Aug 2026 07:22:19 -0400 Subject: [PATCH 1/4] Make WIF credential file readable inside the R2A container google-github-actions/auth writes the credential file 0600. The R2A container runs as the non-root udx user, so the read-only bind mount fails with 'Configured GCP credential file could not be read'. Temporarily chmod 0644 for the duration of the container run and restore the original mode on exit. --- action.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/action.yml b/action.yml index b84f92a..57046cd 100644 --- a/action.yml +++ b/action.yml @@ -551,6 +551,16 @@ runs: echo "::error title=GCP credentials unavailable::Resolved credentials file is no longer readable." exit 1 fi + # google-github-actions/auth writes the credential file 0600 and the + # R2A container runs as the non-root udx user, so the read-only bind + # mount is unreadable inside the container. Make it world-readable + # only while the container runs, then restore the original mode. + credential_mode="$(stat -c '%a' "$GCP_CREDENTIALS_PATH")" + restore_credential_mode() { + chmod "$credential_mode" "$GCP_CREDENTIALS_PATH" 2>/dev/null || true + } + trap restore_credential_mode EXIT + chmod 0644 "$GCP_CREDENTIALS_PATH" gcp_mount=(-v "$GCP_CREDENTIALS_PATH:/tmp/gcp-credentials.json:ro" -e "GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-credentials.json") fi From e49a2d7240c506821fbaeb5285456a52f4aaa659 Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Sat, 29 Aug 2026 14:45:36 +0300 Subject: [PATCH 2/4] Harden WIF credential staging and release checks --- .github/workflows/ci.yml | 41 +++++++++++++++++++++++++++ .github/workflows/publish-release.yml | 2 +- .rabbit/repo.yaml | 2 ++ AGENTS.md | 12 ++++---- CHANGELOG.md | 5 ++++ README.md | 2 +- action.yml | 21 ++++++++------ docs/releasing.md | 13 ++++++--- docs/validation.md | 6 +++- package.json | 2 +- tests/run-merge-tests.sh | 4 +++ 11 files changed, 87 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae358f1..6137b24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,47 @@ jobs: shell: bash run: make test + release-contract: + name: validate release contract + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Require a version bump for runtime changes + shell: bash + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + + if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- action.yml bin; then + echo "No action runtime change detected." + exit 0 + fi + + base_version="$(git show "$BASE_SHA:package.json" | jq -r '.version // empty')" + head_version="$(jq -r '.version // empty' package.json)" + if [[ ! "$base_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ || ! "$head_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error title=Invalid release version::Expected semantic versions in package.json." + exit 1 + fi + + IFS=. read -r base_major base_minor base_patch <<< "$base_version" + IFS=. read -r head_major head_minor head_patch <<< "$head_version" + if (( head_major < base_major || + (head_major == base_major && head_minor < base_minor) || + (head_major == base_major && head_minor == base_minor && head_patch <= base_patch) )); then + echo "::error title=Release version required::Runtime changes to action.yml or bin/ must increase package.json from $base_version." + exit 1 + fi + + echo "Runtime change is prepared for release: $base_version -> $head_version" + lint-workflows: name: lint GitHub Actions workflows runs-on: ubuntu-latest diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 70f7e8f..9775d5f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -130,7 +130,7 @@ jobs: payload="$(jq -nc \ --arg tag "$RELEASE_TAG" \ --arg url "$RELEASE_URL" \ - '{text: (":rocket: Rabbit Automation Action *" + $tag + "* is published.\n" + $url + "\n\nNext:\n• Wait for *Verify release* to pass.\n• Verify Marketplace shows this version and current metadata/README.\n• Run a plan-only non-production caller canary with `@production`, then move `v1`.")}')" + '{text: (":rocket: Rabbit Automation Action *" + $tag + "* is published.\n" + $url + "\n\nNext:\n• Wait for *Verify release* to pass.\n• Verify Marketplace shows this version and current metadata/README.\n• Run a plan-only non-production caller canary with `@" + $tag + "`, then move `v1`.")}')" curl --fail-with-body --silent --show-error \ --request POST \ --header 'Content-type: application/json' \ diff --git a/.rabbit/repo.yaml b/.rabbit/repo.yaml index 40bd427..06d2296 100644 --- a/.rabbit/repo.yaml +++ b/.rabbit/repo.yaml @@ -15,6 +15,8 @@ branches: rules: {} - name: fix/gcp-credential-mount rules: {} + - name: fix/wif-credential-readable + rules: {} - name: infra-templates rules: {} - name: lifecycle-action-integration diff --git a/AGENTS.md b/AGENTS.md index 368122d..d94abe0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,12 +6,12 @@ Run `make test` before opening or updating a pull request. ## Releases -Keep public action changes backward compatible within `v1`. Bump the version -in `package.json` and add its matching immutable `v1.x.y` entry to -`CHANGELOG.md` with each release-worthy change; the production release workflow -publishes it after validation and notifies `#rabbit-support` through its -dedicated repository secret. Confirm the Marketplace UI publication, then move -the `v1` tag only after caller canary validation. Follow +Keep public action changes backward compatible within `v1`. Any runtime change +to `action.yml` or `bin/` is release-worthy: bump the version in `package.json` +and add its matching immutable `v1.x.y` entry to `CHANGELOG.md`. The production +release workflow publishes it after validation and notifies `#rabbit-support` +through its dedicated repository secret. Confirm the Marketplace UI publication, +then move the `v1` tag only after caller canary validation. Follow [`docs/releasing.md`](docs/releasing.md); do not publish or move tags as part of an ordinary pull-request update. diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f1d62..2317cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this action are recorded here. Versions follow semantic versioning; callers should normally use the maintained `v1` major tag. +## v1.0.5 - 2026-08-29 + +- Mount a temporary private copy of Google Workload Identity credentials so the + non-root R2A container can read them without changing the caller file's mode. + ## v1.0.4 - 2026-08-24 - Clarified the one-time GitHub Marketplace setup and the checks required for diff --git a/README.md b/README.md index 209de1d..5b9b74a 100644 --- a/README.md +++ b/README.md @@ -426,7 +426,7 @@ The workflow dispatch inputs provide safe manual control: ## Authentication and state ownership -The caller workflow owns cloud authentication. Authenticate with Google Cloud before invoking the action; the action mounts the resulting `GOOGLE_APPLICATION_CREDENTIALS` file read-only into the R2A container and never copies it into the workspace. Configure AWS credentials in the caller when the configuration uses AWS; the action forwards the resulting AWS session variables to R2A. +The caller workflow owns cloud authentication. Authenticate with Google Cloud before invoking the action; the action copies the credential file into a private runner directory, mounts that copy read-only for R2A, then removes it when the step exits. It never changes the caller credential file or copies credentials into the workspace. Configure AWS credentials in the caller when the configuration uses AWS; the action forwards the resulting AWS session variables to R2A. The optional state-backend inputs are passed through to the IaC engine. Omit them to retain its existing GCS default; provide the backend type, configuration, and state-path key only when the selected backend requires an override. diff --git a/action.yml b/action.yml index 57046cd..ad3c432 100644 --- a/action.yml +++ b/action.yml @@ -552,16 +552,19 @@ runs: exit 1 fi # google-github-actions/auth writes the credential file 0600 and the - # R2A container runs as the non-root udx user, so the read-only bind - # mount is unreadable inside the container. Make it world-readable - # only while the container runs, then restore the original mode. - credential_mode="$(stat -c '%a' "$GCP_CREDENTIALS_PATH")" - restore_credential_mode() { - chmod "$credential_mode" "$GCP_CREDENTIALS_PATH" 2>/dev/null || true + # R2A container runs as the non-root udx user. Keep the caller's + # credential file private and mount a world-readable copy from a + # runner-private directory instead. + gcp_credentials_dir="$(mktemp -d "${RUNNER_TEMP:-/tmp}/rabbit-gcp-credentials.XXXXXX")" + gcp_credentials_copy="$gcp_credentials_dir/gcp-credentials.json" + cleanup_gcp_credentials() { + local status=$? + rm -rf "$gcp_credentials_dir" || true + return "$status" } - trap restore_credential_mode EXIT - chmod 0644 "$GCP_CREDENTIALS_PATH" - gcp_mount=(-v "$GCP_CREDENTIALS_PATH:/tmp/gcp-credentials.json:ro" -e "GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-credentials.json") + trap cleanup_gcp_credentials EXIT + install -m 0644 -- "$GCP_CREDENTIALS_PATH" "$gcp_credentials_copy" + gcp_mount=(-v "$gcp_credentials_copy:/tmp/gcp-credentials.json:ro" -e "GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-credentials.json") fi echo "🐰 Running Rabbit Automation Action..." diff --git a/docs/releasing.md b/docs/releasing.md index 662d3d4..6470bce 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -10,6 +10,10 @@ The action is released from `production`. Patch releases are immutable changes action behavior. 2. Merge the focused, reviewed pull request into `production`. +Pull-request CI rejects a change to `action.yml` or `bin/` unless its +`package.json` version increases. `make test` also requires the first semantic +heading in `CHANGELOG.md` to match that version. + The `Publish release` workflow runs after every `production` push. It does nothing unless that push changes the `package.json` version; then it runs `make test`, refuses to reuse an existing tag, and publishes that GitHub @@ -49,13 +53,14 @@ Marketplace listing check. ## Promote callers -1. In a caller repository's non-production environment, run a plan using - `udx/github-rabbit-action@production`. This caller canary proves the exact +1. After the semantic release is published, in a caller repository's + non-production environment run a plan using its immutable tag, for example + `udx/github-rabbit-action@v1.0.5`. This caller canary proves the exact release commit works in a real consumer workflow; it must not apply infrastructure. 2. Move the `v1` tag to the tested immutable release commit. -3. Confirm `v1` and the patch tag resolve to the same commit with - `git ls-remote --tags origin 'v1*'`. +3. Compare the `v1` and new patch-tag SHAs to confirm they resolve to the same + commit: `git ls-remote --tags origin refs/tags/v1 refs/tags/v1.0.5`. 4. Update reusable workflows and callers from `@production` to `@v1`. 5. Run a non-production caller plan using `@v1` before merging the consumer change. diff --git a/docs/validation.md b/docs/validation.md index 3b7f258..9c4e24e 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -13,4 +13,8 @@ rabbit.ci ## CI -The `ci` workflow runs on pull requests and pushes to `production` and `lifecycle-action-integration`. It installs a pinned `yq` binary and runs `make test`. +The `ci` workflow runs on pull requests and pushes to `production`. Its action +contract job installs a pinned `yq` binary and runs `make test`; a separate job +uses `actionlint` for GitHub Actions workflow linting. On pull requests, a +release-contract job requires a semantic `package.json` version increase when +`action.yml` or `bin/` changes. diff --git a/package.json b/package.json index 70cb43f..3676992 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@udx/github-rabbit-action", - "version": "1.0.4", + "version": "1.0.5", "private": true, "description": "Rabbit Automation Action release manifest", "license": "GPL-2.0-only", diff --git a/tests/run-merge-tests.sh b/tests/run-merge-tests.sh index 93eb463..6a13662 100755 --- a/tests/run-merge-tests.sh +++ b/tests/run-merge-tests.sh @@ -112,6 +112,10 @@ assert_eq "$(yq -r '.runs.steps[] | select(.name == "Upload terraform plans") | assert_eq "$(yq -r '[.runs.steps[] | select(.uses == "google-github-actions/auth@v3" or .uses == "aws-actions/configure-aws-credentials@v6")] | length' "$PROJECT_ROOT/action.yml")" "0" "Action does not configure cloud credentials" assert_eq "$(grep -c 'Authenticate with Google Cloud before invoking github-rabbit-action' "$PROJECT_ROOT/action.yml")" "1" "Action requires caller-provided GCP credentials" assert_eq "$(grep -c 'AWS credentials configured by the caller workflow' "$PROJECT_ROOT/action.yml")" "1" "Action forwards caller AWS credentials" +assert_eq "$(grep -c 'mktemp -d "\${RUNNER_TEMP:-/tmp}/rabbit-gcp-credentials.XXXXXX"' "$PROJECT_ROOT/action.yml")" "1" "Action stages GCP credentials in a private temporary directory" +assert_eq "$(grep -c 'install -m 0644 -- "\$GCP_CREDENTIALS_PATH" "\$gcp_credentials_copy"' "$PROJECT_ROOT/action.yml")" "1" "Action mounts a readable credential copy" +assert_eq "$(grep -c 'rm -rf "\$gcp_credentials_dir"' "$PROJECT_ROOT/action.yml")" "1" "Action removes the staged GCP credential copy" +assert_eq "$(grep -c 'chmod 0644 "\$GCP_CREDENTIALS_PATH"' "$PROJECT_ROOT/action.yml" || true)" "0" "Action leaves the caller GCP credential mode unchanged" write_yaml "$SOURCE/production/10-base.yaml" 'services: - module: test-module From 436d985fe9e02ffff7632983a610d2641b2f7bf7 Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Sat, 29 Aug 2026 14:52:13 +0300 Subject: [PATCH 3/4] Simplify release contract validation --- .github/workflows/ci.yml | 13 +++---------- .rabbit/repo.yaml | 2 ++ docs/validation.md | 9 ++++----- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6137b24..02a03f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Install yq shell: bash @@ -37,17 +39,8 @@ jobs: shell: bash run: make test - release-contract: - name: validate release contract - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - name: Require a version bump for runtime changes + if: github.event_name == 'pull_request' shell: bash env: BASE_SHA: ${{ github.event.pull_request.base.sha }} diff --git a/.rabbit/repo.yaml b/.rabbit/repo.yaml index 06d2296..0ec64a3 100644 --- a/.rabbit/repo.yaml +++ b/.rabbit/repo.yaml @@ -15,6 +15,8 @@ branches: rules: {} - name: fix/gcp-credential-mount rules: {} + - name: fix/stage-wif-credential-copy + rules: {} - name: fix/wif-credential-readable rules: {} - name: infra-templates diff --git a/docs/validation.md b/docs/validation.md index 9c4e24e..04e7995 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -13,8 +13,7 @@ rabbit.ci ## CI -The `ci` workflow runs on pull requests and pushes to `production`. Its action -contract job installs a pinned `yq` binary and runs `make test`; a separate job -uses `actionlint` for GitHub Actions workflow linting. On pull requests, a -release-contract job requires a semantic `package.json` version increase when -`action.yml` or `bin/` changes. +The `ci` workflow runs on pull requests and pushes to `production`. It installs +a pinned `yq` binary, runs `make test`, and uses `actionlint` for GitHub Actions +workflow linting. On pull requests, validation also requires a semantic +`package.json` version increase when `action.yml` or `bin/` changes. From 853685204410412de490c5a1f065aa02945311a4 Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Sat, 29 Aug 2026 15:19:23 +0300 Subject: [PATCH 4/4] Scope release checks to production PRs --- .github/workflows/ci.yml | 5 ++++- AGENTS.md | 11 ++++++----- docs/releasing.md | 12 ++++++------ docs/validation.md | 5 +++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02a03f1..2a76914 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,9 @@ on: permissions: contents: read +env: + RELEASE_BRANCH: production + concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -40,7 +43,7 @@ jobs: run: make test - name: Require a version bump for runtime changes - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.base_ref == env.RELEASE_BRANCH shell: bash env: BASE_SHA: ${{ github.event.pull_request.base.sha }} diff --git a/AGENTS.md b/AGENTS.md index d94abe0..c67bba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,11 +7,12 @@ Run `make test` before opening or updating a pull request. ## Releases Keep public action changes backward compatible within `v1`. Any runtime change -to `action.yml` or `bin/` is release-worthy: bump the version in `package.json` -and add its matching immutable `v1.x.y` entry to `CHANGELOG.md`. The production -release workflow publishes it after validation and notifies `#rabbit-support` -through its dedicated repository secret. Confirm the Marketplace UI publication, -then move the `v1` tag only after caller canary validation. Follow +to `action.yml` or `bin/` is release-worthy. The pull request targeting +`production` must bump the version in `package.json` and add its matching +immutable `v1.x.y` entry to `CHANGELOG.md`. The production release workflow +publishes it after validation and notifies `#rabbit-support` through its +dedicated repository secret. Confirm the Marketplace UI publication, then move +the `v1` tag only after caller canary validation. Follow [`docs/releasing.md`](docs/releasing.md); do not publish or move tags as part of an ordinary pull-request update. diff --git a/docs/releasing.md b/docs/releasing.md index 6470bce..217efe5 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -5,14 +5,14 @@ The action is released from `production`. Patch releases are immutable ## Prepare a release -1. Bump `package.json` to the next semantic version and add matching concise - user-facing notes at the top of `CHANGELOG.md` in the pull request that - changes action behavior. +1. In the pull request targeting `production`, bump `package.json` to the next + semantic version and add matching concise user-facing notes at the top of + `CHANGELOG.md` for the action behavior changes in that release. 2. Merge the focused, reviewed pull request into `production`. -Pull-request CI rejects a change to `action.yml` or `bin/` unless its -`package.json` version increases. `make test` also requires the first semantic -heading in `CHANGELOG.md` to match that version. +CI rejects a production-targeting pull request that changes `action.yml` or +`bin/` unless its `package.json` version increases. `make test` also requires +the first semantic heading in `CHANGELOG.md` to match that version. The `Publish release` workflow runs after every `production` push. It does nothing unless that push changes the `package.json` version; then it runs diff --git a/docs/validation.md b/docs/validation.md index 04e7995..bb0f916 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -15,5 +15,6 @@ rabbit.ci The `ci` workflow runs on pull requests and pushes to `production`. It installs a pinned `yq` binary, runs `make test`, and uses `actionlint` for GitHub Actions -workflow linting. On pull requests, validation also requires a semantic -`package.json` version increase when `action.yml` or `bin/` changes. +workflow linting. On pull requests targeting `production`, validation also +requires a semantic `package.json` version increase when `action.yml` or `bin/` +changes.