diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae358f1..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 @@ -21,6 +24,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Install yq shell: bash @@ -37,6 +42,38 @@ jobs: shell: bash run: make test + - name: Require a version bump for runtime changes + if: github.event_name == 'pull_request' && github.base_ref == env.RELEASE_BRANCH + 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..0ec64a3 100644 --- a/.rabbit/repo.yaml +++ b/.rabbit/repo.yaml @@ -15,6 +15,10 @@ branches: rules: {} - name: fix/gcp-credential-mount rules: {} + - name: fix/stage-wif-credential-copy + 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..c67bba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,9 +6,10 @@ 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 +Keep public action changes backward compatible within `v1`. Any runtime change +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 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 b84f92a..ad3c432 100644 --- a/action.yml +++ b/action.yml @@ -551,7 +551,20 @@ runs: echo "::error title=GCP credentials unavailable::Resolved credentials file is no longer readable." exit 1 fi - gcp_mount=(-v "$GCP_CREDENTIALS_PATH:/tmp/gcp-credentials.json:ro" -e "GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-credentials.json") + # google-github-actions/auth writes the credential file 0600 and the + # 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 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..217efe5 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -5,11 +5,15 @@ 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`. +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 `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..bb0f916 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`. It installs +a pinned `yq` binary, runs `make test`, and uses `actionlint` for GitHub Actions +workflow linting. On pull requests targeting `production`, validation also +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