From c23a07a85fbca890a7cce096da41a0319abebf9a Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Tue, 18 Aug 2026 18:21:41 +0300 Subject: [PATCH 1/2] Harden action release and CI workflow --- .github/workflows/ci.yml | 19 ++++++++++++-- .github/workflows/release.yml | 48 +++++++++++++++++++++++++++++++++++ .rabbit/repo.yaml | 17 ++++++++++++- AGENTS.md | 8 ++++++ CHANGELOG.md | 15 +++++++++++ Makefile | 26 +++++++++++++++++-- README.md | 7 +++++ bin/lib/lifecycle.sh | 22 ++++++++++++++-- docs/releasing.md | 42 ++++++++++++++++++++++++++++++ tests/run-merge-tests.sh | 17 +++++++++++++ 10 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 docs/releasing.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2915a20..15ec5a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,19 @@ -name: ci +name: CI on: pull_request: push: branches: - production - - lifecycle-action-integration + workflow_dispatch: permissions: contents: read +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: validate: name: validate action contract @@ -34,3 +38,14 @@ jobs: - name: Run validation shell: bash run: make test + + lint-workflows: + name: lint GitHub Actions workflows + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Run actionlint + shell: bash + run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12 -color diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a5947ea --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Verify release + +on: + release: + types: + - published + +permissions: + contents: read + +jobs: + verify: + name: verify action release + if: startsWith(github.event.release.tag_name, 'v') + runs-on: ubuntu-latest + steps: + - name: Validate semantic release tag + shell: bash + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error title=Invalid release tag::Expected a semantic patch tag such as v1.0.3, got $RELEASE_TAG." + exit 1 + fi + + - name: Checkout release + uses: actions/checkout@v5 + with: + ref: ${{ github.event.release.tag_name }} + + - name: Install pinned yq + shell: bash + run: | + set -euo pipefail + version="v4.44.3" + sha256="a2c097180dd884a8d50c956ee16a9cec070f30a7947cf4ebf87d5f36213e9ed7" + install_dir="${RUNNER_TEMP}/rabbit-action-bin" + mkdir -p "$install_dir" + curl -fsSL "https://github.com/mikefarah/yq/releases/download/${version}/yq_linux_amd64" -o "$install_dir/yq" + printf '%s %s\n' "$sha256" "$install_dir/yq" | sha256sum --check --status + chmod +x "$install_dir/yq" + echo "$install_dir" >> "$GITHUB_PATH" + + - name: Verify release contract + shell: bash + run: make test diff --git a/.rabbit/repo.yaml b/.rabbit/repo.yaml index c2ded6e..8a5511b 100644 --- a/.rabbit/repo.yaml +++ b/.rabbit/repo.yaml @@ -5,6 +5,14 @@ repository: owner: udx default_branch: production branches: + - name: dependabot/github_actions/actions/checkout-7 + rules: {} + - name: dependabot/github_actions/actions/github-script-9 + rules: {} + - name: dependabot/github_actions/actions/upload-artifact-7 + rules: {} + - name: fix/gcp-credential-mount + rules: {} - name: infra-templates rules: {} - name: lifecycle-action-integration @@ -60,6 +68,13 @@ workflows: push: branches: - production - - lifecycle-action-integration + workflow_dispatch: {} + permissions: + contents: read + - path: .github/workflows/release.yml + triggers: + release: + types: + - published permissions: contents: read diff --git a/AGENTS.md b/AGENTS.md index da4e8d3..82c2cb4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,14 @@ Run `make test` before opening or updating a pull request. +## Releases + +Keep public action changes backward compatible within `v1`. Release immutable +`v1.x.y` tags from `production`, publish the release to GitHub Marketplace, +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. + ## Rabbit CI Context `.rabbit/repo.yaml` is generated by `rabbit.ci`; do not edit it by hand. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..394fc38 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this action are recorded here. Versions follow semantic +versioning; callers should normally use the maintained `v1` major tag. + +## v1.0.2 - 2026-08-18 + +- Made lifecycle resolution and Rabbit configuration merging self-contained. +- Added caller-selectable lifecycle policy and configuration-root inputs. +- Kept cloud identity in the caller workflow; the action consumes prepared + runtime credentials only. + +## v1.0.1 - 2026-04-30 + +- Initial GitHub Marketplace release. diff --git a/Makefile b/Makefile index 7c6c8d5..2ac6ab4 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,38 @@ -.PHONY: test validate-shell validate-action validate-workflow +.PHONY: test validate-shell validate-shellcheck validate-action validate-workflow -test: validate-shell validate-action validate-workflow +test: validate-shell validate-shellcheck validate-action validate-workflow tests/run-merge-tests.sh validate-shell: bash -n \ bin/merge-configs.sh \ bin/resolve-lifecycle.sh \ + bin/render-plan-summary.sh \ bin/lib/config.sh \ + bin/lib/discovery.sh \ bin/lib/github.sh \ bin/lib/lifecycle.sh \ bin/lib/validation.sh \ bin/lib/environment.sh \ + bin/lib/logging.sh \ + bin/lib/merge.sh \ + bin/lib/output.sh \ + tests/run-merge-tests.sh + +validate-shellcheck: + shellcheck --external-sources --severity=error \ + bin/merge-configs.sh \ + bin/resolve-lifecycle.sh \ + bin/render-plan-summary.sh \ + bin/lib/config.sh \ + bin/lib/discovery.sh \ + bin/lib/environment.sh \ + bin/lib/github.sh \ + bin/lib/lifecycle.sh \ + bin/lib/logging.sh \ + bin/lib/merge.sh \ + bin/lib/output.sh \ + bin/lib/validation.sh \ tests/run-merge-tests.sh validate-action: @@ -19,3 +40,4 @@ validate-action: validate-workflow: yq eval '.' .github/workflows/ci.yml >/dev/null + yq eval '.' .github/workflows/release.yml >/dev/null diff --git a/README.md b/README.md index 0ea9724..1aa9d90 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,13 @@ schema, prerequisites, examples, and outputs. Services deploy in ascending module order; destroy operations use the reverse order. +## Releases + +Use the maintained `@v1` major tag in caller workflows. Patch releases are +published as immutable `v1.x.y` GitHub releases, then `v1` moves to the tested +compatible release. See [the release guide](docs/releasing.md) and +[changelog](CHANGELOG.md). + --- ## Configuration Reference diff --git a/bin/lib/lifecycle.sh b/bin/lib/lifecycle.sh index 9d22266..80b5773 100755 --- a/bin/lib/lifecycle.sh +++ b/bin/lib/lifecycle.sh @@ -231,9 +231,27 @@ lifecycle_detect_environments() { fi # Standard lifecycle directories (production, staging, development) - if [[ " ${ALL_LIFECYCLES[@]} " =~ " ${dir_name} " ]]; then + local configured_lifecycle=false + local lifecycle + for lifecycle in "${ALL_LIFECYCLES[@]}"; do + if [[ "$lifecycle" == "$dir_name" ]]; then + configured_lifecycle=true + break + fi + done + + if [[ "$configured_lifecycle" == true ]]; then # Add once - if [[ ! " ${found_lifecycles[@]} " =~ " ${dir_name} " ]]; then + local already_found=false + local found_lifecycle + for found_lifecycle in "${found_lifecycles[@]}"; do + if [[ "$found_lifecycle" == "$dir_name" ]]; then + already_found=true + break + fi + done + + if [[ "$already_found" == false ]]; then envs_ref+=("$dir_name") found_lifecycles+=("$dir_name") dbg "Detected $dir_name at: $dir" >&2 diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..9fa5dbc --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,42 @@ +# Releasing Rabbit Automation Action + +The action is released from `production`. Patch releases are immutable +`v1.x.y` tags; `v1` is the movable compatibility tag that callers use. + +## Before publishing + +1. Merge a focused, reviewed pull request into `production`. +2. Confirm the CI action-contract and workflow-lint jobs pass. +3. Test the exact `production` commit from a caller's non-production + environment. Use `@production` only for that canary. +4. Add a concise, user-facing entry to `CHANGELOG.md` when the behavior + changes. + +## Publish the release + +1. Create a semantic GitHub release from the tested `production` commit, for + example `v1.0.3`. +2. In the release form, select **Publish this Action to the GitHub + Marketplace**. GitHub requires this UI step and may require 2FA; a release + created only through the REST or CLI release API is not enough. +3. Keep `Deployment` as the primary Marketplace category and `Security` as the + secondary category unless the action's public purpose changes. +4. Verify the Marketplace listing shows the new version, current `action.yml` + metadata, and current README before changing any caller references. + +Publishing triggers `Verify release`, which checks the semantic tag and runs +the action contract from that tag. It confirms the published artifact; it does +not replace the pre-release caller canary or the Marketplace UI verification. + +## Promote callers + +1. Move the `v1` tag to the tested immutable release commit. +2. Confirm `v1` and the patch tag resolve to the same commit with + `git ls-remote --tags origin 'v1*'`. +3. Update reusable workflows and callers from `@production` to `@v1`. +4. Run a non-production caller plan using `@v1` before merging the consumer + change. + +Use a new major tag for breaking input, output, safety, or lifecycle-contract +changes. Keep `v1` on the latest compatible patch release; do not rewrite a +patch release tag. diff --git a/tests/run-merge-tests.sh b/tests/run-merge-tests.sh index 9d4320d..b579fa1 100755 --- a/tests/run-merge-tests.sh +++ b/tests/run-merge-tests.sh @@ -228,6 +228,23 @@ run_resolve "$SOURCE" "dev-alice" "$subdir_out" assert_eq "$(read_output "$subdir_out" lifecycle)" "development" "Development subdirectory resolves in-repo" assert_eq "$(read_output "$subdir_out" resolution_reason)" "environment_subdirectory" "Subdirectory reason is recorded" +scenario "Scenario: lifecycle discovery recognizes configured roots once" +detected_lifecycles="$TEMP_DIR/detected-lifecycles.out" +( + GITHUB_WORKSPACE="$ROOT" + INPUT_SOURCE_DIR="$SOURCE" + INPUT_ENV_NAME="" + INPUT_LIFECYCLE_POLICY_PATH="" + source "$PROJECT_ROOT/bin/lib/config.sh" + source "$PROJECT_ROOT/bin/lib/lifecycle.sh" + detected=() + lifecycle_detect_environments "$SOURCE" detected + printf '%s\n' "${detected[@]}" +) > "$detected_lifecycles" +assert_eq "$(grep -cx 'production' "$detected_lifecycles")" "1" "Production lifecycle is discovered once" +assert_eq "$(grep -cx 'staging' "$detected_lifecycles")" "1" "Staging lifecycle is discovered once" +assert_eq "$(grep -cx 'development' "$detected_lifecycles")" "1" "Development lifecycle is discovered once" + scenario "Scenario: resolver uses a caller lifecycle policy for both policy metadata and merge" policy_out="$TEMP_DIR/policy.out" run_resolve "$SOURCE" "dev-alice" "$policy_out" ".rabbit/lifecycle-policy.yaml" From c970b2f458b453758fae4c80d3220cb2b61f7519 Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Tue, 18 Aug 2026 18:27:28 +0300 Subject: [PATCH 2/2] Validate all published release tags --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5947ea..6906cb6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,6 @@ permissions: jobs: verify: name: verify action release - if: startsWith(github.event.release.tag_name, 'v') runs-on: ubuntu-latest steps: - name: Validate semantic release tag