From 74c1a238e3f6ea2aa5fd17b6ab588400b2c351e8 Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Tue, 18 Aug 2026 18:54:57 +0300 Subject: [PATCH 1/4] Publish versioned releases from production --- .github/workflows/publish-release.yml | 120 ++++++++++++++++++++++++++ .rabbit/repo.yaml | 12 ++- AGENTS.md | 12 +-- CHANGELOG.md | 6 ++ Makefile | 1 + docs/releasing.md | 42 +++++---- 6 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/publish-release.yml diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..c085e0e --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,120 @@ +name: Publish release + +on: + push: + branches: + - production + +permissions: + contents: write + +concurrency: + group: publish-release-production + cancel-in-progress: false + +jobs: + publish: + name: publish versioned release + runs-on: ubuntu-latest + steps: + - name: Checkout production + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - 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: Validate release candidate + shell: bash + run: make test + + - name: Read next version from changelog + id: release + shell: bash + run: | + set -euo pipefail + + if git diff --quiet "${{ github.event.before }}" "$GITHUB_SHA" -- CHANGELOG.md; then + echo "publish=false" >> "$GITHUB_OUTPUT" + echo "No release: CHANGELOG.md was not changed by this production push." + exit 0 + fi + + release_tag="" + while IFS= read -r line; do + if [[ "$line" =~ ^##\ (v[0-9]+\.[0-9]+\.[0-9]+)\ -\ ]]; then + release_tag="${BASH_REMATCH[1]}" + break + fi + done < CHANGELOG.md + + if [[ -z "$release_tag" ]]; then + echo "::error title=Missing release version::The first version heading in CHANGELOG.md must be a semantic tag such as v1.0.3." + exit 1 + fi + + if git ls-remote --exit-code --tags origin "refs/tags/$release_tag" >/dev/null; then + echo "::error title=Release already exists::Tag $release_tag already exists. Add the next semantic version to CHANGELOG.md before merging." + exit 1 + fi + + notes_file="${RUNNER_TEMP}/release-notes.md" + awk -v heading="## ${release_tag} - " ' + index($0, heading) == 1 { include = 1; next } + include && /^## / { exit } + include { print } + ' CHANGELOG.md > "$notes_file" + + if [[ ! -s "$notes_file" ]]; then + echo "::error title=Missing release notes::Add release notes below the $release_tag heading in CHANGELOG.md." + exit 1 + fi + + printf 'tag=%s\n' "$release_tag" >> "$GITHUB_OUTPUT" + printf 'notes_file=%s\n' "$notes_file" >> "$GITHUB_OUTPUT" + printf 'publish=true\n' >> "$GITHUB_OUTPUT" + + - name: Publish GitHub release + if: steps.release.outputs.publish == 'true' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.release.outputs.tag }} + RELEASE_NOTES: ${{ steps.release.outputs.notes_file }} + shell: bash + run: | + set -euo pipefail + gh release create "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$GITHUB_SHA" \ + --title "Rabbit Automation Action $RELEASE_TAG" \ + --notes-file "$RELEASE_NOTES" + + - name: Notify Rabbit support + if: steps.release.outputs.publish == 'true' + env: + RELEASE_TAG: ${{ steps.release.outputs.tag }} + RELEASE_URL: https://github.com/${{ github.repository }}/releases/tag/${{ steps.release.outputs.tag }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_RABBIT_SUPPORT }} + shell: bash + run: | + set -euo pipefail + payload="$(jq -nc \ + --arg tag "$RELEASE_TAG" \ + --arg url "$RELEASE_URL" \ + '{text: ("Rabbit Automation Action " + $tag + " is published. Marketplace action: open " + $url + ", wait for Verify release to pass, then publish to the GitHub Marketplace. Verify the listing and complete the caller canary before moving v1.")}')" + curl --fail-with-body --silent --show-error \ + --request POST \ + --header 'Content-type: application/json' \ + --data "$payload" \ + "$SLACK_WEBHOOK" diff --git a/.rabbit/repo.yaml b/.rabbit/repo.yaml index 8a5511b..b123aa7 100644 --- a/.rabbit/repo.yaml +++ b/.rabbit/repo.yaml @@ -5,6 +5,8 @@ repository: owner: udx default_branch: production branches: + - name: chore/action-release-standards + rules: {} - name: dependabot/github_actions/actions/checkout-7 rules: {} - name: dependabot/github_actions/actions/github-script-9 @@ -42,7 +44,8 @@ configuration: - BITBUCKET_TOKEN - CONTEXT7_API_KEY - DEPENDABOT_REVIEWER_TOKEN - repository: [] + repository: + - SLACK_WEBHOOK_RABBIT_SUPPORT variables: organization: - DOCKER_LOGIN @@ -71,6 +74,13 @@ workflows: workflow_dispatch: {} permissions: contents: read + - path: .github/workflows/publish-release.yml + triggers: + push: + branches: + - production + permissions: + contents: write - path: .github/workflows/release.yml triggers: release: diff --git a/AGENTS.md b/AGENTS.md index 82c2cb4..6df1037 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,11 +6,13 @@ 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. +Keep public action changes backward compatible within `v1`. Add the next +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 [`docs/releasing.md`](docs/releasing.md); do not publish or +move tags as part of an ordinary pull-request update. ## Rabbit CI Context diff --git a/CHANGELOG.md b/CHANGELOG.md index 394fc38..4abbbcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this action are recorded here. Versions follow semantic versioning; callers should normally use the maintained `v1` major tag. +## v1.0.3 - 2026-08-18 + +- Added release verification, workflow linting, and a production release + workflow that validates and publishes each new semantic version. +- Enforced ShellCheck error checks and corrected lifecycle-root discovery. + ## v1.0.2 - 2026-08-18 - Made lifecycle resolution and Rabbit configuration merging self-contained. diff --git a/Makefile b/Makefile index 2ac6ab4..b3c7097 100644 --- a/Makefile +++ b/Makefile @@ -41,3 +41,4 @@ validate-action: validate-workflow: yq eval '.' .github/workflows/ci.yml >/dev/null yq eval '.' .github/workflows/release.yml >/dev/null + yq eval '.' .github/workflows/publish-release.yml >/dev/null diff --git a/docs/releasing.md b/docs/releasing.md index 9fa5dbc..3ca59ea 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -3,30 +3,42 @@ 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 +## Prepare a release -1. Merge a focused, reviewed pull request into `production`. -2. Confirm the CI action-contract and workflow-lint jobs pass. +1. Add the next semantic version and concise user-facing notes at the top of + `CHANGELOG.md` in the pull request that changes action behavior. +2. Merge the focused, reviewed pull request into `production`. 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. +The `Publish release` workflow runs after every `production` push. It does +nothing unless that push changes `CHANGELOG.md`; then it runs `make test`, +reads the first semantic version, refuses to reuse an existing tag, and +publishes that GitHub release from the merged commit. The `Verify release` +workflow then validates the published tag. After the GitHub release is +created, `#rabbit-support` receives the Marketplace handoff through +`SLACK_WEBHOOK_RABBIT_SUPPORT`; the message directs the operator to wait for +verification before publishing to Marketplace. + +`CHANGELOG.md` is the only release-version source. A change to it must put a +new semantic version heading first; editing it while its leading version is +already tagged fails the release workflow instead of creating an ambiguous +release. + +## Publish to GitHub Marketplace + +1. Confirm the `Verify release` workflow passed for the automatically + published semantic GitHub release, for example `v1.0.3`. +2. Open that release and, 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. +The published-tag verification does not replace the pre-release caller canary +or the Marketplace UI verification. ## Promote callers From c7e1cd3b23d66759e5a282d959195dfd8223e15d Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Tue, 18 Aug 2026 18:57:23 +0300 Subject: [PATCH 2/4] Use package metadata for action releases --- .github/workflows/publish-release.yml | 22 ++++++--------------- AGENTS.md | 15 +++++++------- Makefile | 19 ++++++++++++++++-- README.md | 6 ++++-- docs/releasing.md | 28 +++++++++++++-------------- package.json | 10 ++++++++++ 6 files changed, 59 insertions(+), 41 deletions(-) create mode 100644 package.json diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index c085e0e..dd97db4 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -39,33 +39,23 @@ jobs: shell: bash run: make test - - name: Read next version from changelog + - name: Read new version id: release shell: bash run: | set -euo pipefail - if git diff --quiet "${{ github.event.before }}" "$GITHUB_SHA" -- CHANGELOG.md; then + if git diff --quiet "${{ github.event.before }}" "$GITHUB_SHA" -- package.json; then echo "publish=false" >> "$GITHUB_OUTPUT" - echo "No release: CHANGELOG.md was not changed by this production push." + echo "No release: package.json was not changed by this production push." exit 0 fi - release_tag="" - while IFS= read -r line; do - if [[ "$line" =~ ^##\ (v[0-9]+\.[0-9]+\.[0-9]+)\ -\ ]]; then - release_tag="${BASH_REMATCH[1]}" - break - fi - done < CHANGELOG.md - - if [[ -z "$release_tag" ]]; then - echo "::error title=Missing release version::The first version heading in CHANGELOG.md must be a semantic tag such as v1.0.3." - exit 1 - fi + release_version="$(jq -r '.version // empty' package.json)" + release_tag="v${release_version}" if git ls-remote --exit-code --tags origin "refs/tags/$release_tag" >/dev/null; then - echo "::error title=Release already exists::Tag $release_tag already exists. Add the next semantic version to CHANGELOG.md before merging." + echo "::error title=Release already exists::Tag $release_tag already exists. Bump package.json before merging." exit 1 fi diff --git a/AGENTS.md b/AGENTS.md index 6df1037..368122d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,13 +6,14 @@ Run `make test` before opening or updating a pull request. ## Releases -Keep public action changes backward compatible within `v1`. Add the next -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 [`docs/releasing.md`](docs/releasing.md); do not publish or -move tags as part of an ordinary pull-request update. +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 +[`docs/releasing.md`](docs/releasing.md); do not publish or move tags as part +of an ordinary pull-request update. ## Rabbit CI Context diff --git a/Makefile b/Makefile index b3c7097..cceae09 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ -.PHONY: test validate-shell validate-shellcheck validate-action validate-workflow +SHELL := /bin/bash -test: validate-shell validate-shellcheck validate-action validate-workflow +.PHONY: test validate-shell validate-shellcheck validate-action validate-workflow validate-release-version + +test: validate-shell validate-shellcheck validate-action validate-workflow validate-release-version tests/run-merge-tests.sh validate-shell: @@ -42,3 +44,16 @@ validate-workflow: yq eval '.' .github/workflows/ci.yml >/dev/null yq eval '.' .github/workflows/release.yml >/dev/null yq eval '.' .github/workflows/publish-release.yml >/dev/null + +validate-release-version: + jq empty package.json + release_version="$$(jq -r '.version // empty' package.json)"; \ + if [[ ! "$$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$$ ]]; then \ + echo "package.json version must be semantic, got: $$release_version" >&2; \ + exit 1; \ + fi; \ + changelog_tag="$$(awk '/^## v[0-9]+\.[0-9]+\.[0-9]+ - / { print $$2; exit }' CHANGELOG.md)"; \ + if [[ "$$changelog_tag" != "v$$release_version" ]]; then \ + echo "The first semantic CHANGELOG.md heading must match package.json version ($$release_version), got: $${changelog_tag:-missing}" >&2; \ + exit 1; \ + fi diff --git a/README.md b/README.md index 1aa9d90..209de1d 100644 --- a/README.md +++ b/README.md @@ -243,8 +243,10 @@ order. 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). +compatible release. The [`package.json`](package.json) version is the release +source and the +[changelog](CHANGELOG.md) supplies its release notes. See [the release +guide](docs/releasing.md) for the complete process. --- diff --git a/docs/releasing.md b/docs/releasing.md index 3ca59ea..03b41cd 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -5,25 +5,25 @@ The action is released from `production`. Patch releases are immutable ## Prepare a release -1. Add the next semantic version and concise user-facing notes at the top of - `CHANGELOG.md` in the pull request that changes action behavior. +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. 2. Merge the focused, reviewed pull request into `production`. 3. Test the exact `production` commit from a caller's non-production environment. Use `@production` only for that canary. The `Publish release` workflow runs after every `production` push. It does -nothing unless that push changes `CHANGELOG.md`; then it runs `make test`, -reads the first semantic version, refuses to reuse an existing tag, and -publishes that GitHub release from the merged commit. The `Verify release` -workflow then validates the published tag. After the GitHub release is -created, `#rabbit-support` receives the Marketplace handoff through -`SLACK_WEBHOOK_RABBIT_SUPPORT`; the message directs the operator to wait for -verification before publishing to Marketplace. - -`CHANGELOG.md` is the only release-version source. A change to it must put a -new semantic version heading first; editing it while its leading version is -already tagged fails the release workflow instead of creating an ambiguous -release. +nothing unless that push changes `package.json`; then it runs `make test`, +refuses to reuse an existing tag, and publishes that GitHub release from the +merged commit. The `Verify release` workflow then validates the published tag. +After the GitHub release is created, `#rabbit-support` receives the Marketplace +handoff through `SLACK_WEBHOOK_RABBIT_SUPPORT`; the message directs the +operator to wait for verification before publishing to Marketplace. + +`package.json` is the single release-version source. Its semantic version maps +to the Git tag by adding `v` and must match the first semantic heading in +`CHANGELOG.md`; `make test` enforces that contract before a release can be +published. ## Publish to GitHub Marketplace diff --git a/package.json b/package.json new file mode 100644 index 0000000..65a9cdf --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "@udx/github-rabbit-action", + "version": "1.0.3", + "private": true, + "description": "Rabbit Automation Action release manifest", + "license": "GPL-2.0-only", + "scripts": { + "test": "make test" + } +} From 11fb334400f639b352b477fc31dd6f81a95e9997 Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Tue, 18 Aug 2026 18:58:28 +0300 Subject: [PATCH 3/4] Satisfy release workflow lint --- .github/workflows/publish-release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index dd97db4..3a31c08 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -71,9 +71,11 @@ jobs: exit 1 fi - printf 'tag=%s\n' "$release_tag" >> "$GITHUB_OUTPUT" - printf 'notes_file=%s\n' "$notes_file" >> "$GITHUB_OUTPUT" - printf 'publish=true\n' >> "$GITHUB_OUTPUT" + { + printf 'tag=%s\n' "$release_tag" + printf 'notes_file=%s\n' "$notes_file" + printf 'publish=true\n' + } >> "$GITHUB_OUTPUT" - name: Publish GitHub release if: steps.release.outputs.publish == 'true' From f5806e5f94a43684697d4c987fbc7608f1942a73 Mon Sep 17 00:00:00 2001 From: Dmytro Smirnov Date: Tue, 18 Aug 2026 19:02:02 +0300 Subject: [PATCH 4/4] Gate releases on package version changes --- .github/workflows/publish-release.yml | 20 +++++++++++++++++--- docs/releasing.md | 9 +++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 3a31c08..e6011cc 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -45,13 +45,27 @@ jobs: run: | set -euo pipefail - if git diff --quiet "${{ github.event.before }}" "$GITHUB_SHA" -- package.json; then + release_version="$(jq -r '.version // empty' package.json)" + previous_version="$(git show "${{ github.event.before }}:package.json" 2>/dev/null | jq -r '.version // empty' 2>/dev/null || true)" + + if [[ "$release_version" == "$previous_version" ]]; then echo "publish=false" >> "$GITHUB_OUTPUT" - echo "No release: package.json was not changed by this production push." + echo "No release: package.json version was not changed by this production push." exit 0 fi - release_version="$(jq -r '.version // empty' package.json)" + if [[ -n "$previous_version" ]]; then + IFS=. read -r previous_major previous_minor previous_patch <<< "$previous_version" + IFS=. read -r release_major release_minor release_patch <<< "$release_version" + + if (( release_major < previous_major || + (release_major == previous_major && release_minor < previous_minor) || + (release_major == previous_major && release_minor == previous_minor && release_patch < previous_patch) )); then + echo "::error title=Release version must increase::package.json version $release_version is lower than the previous production version $previous_version." + exit 1 + fi + fi + release_tag="v${release_version}" if git ls-remote --exit-code --tags origin "refs/tags/$release_tag" >/dev/null; then diff --git a/docs/releasing.md b/docs/releasing.md index 03b41cd..7f52a47 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -13,9 +13,9 @@ The action is released from `production`. Patch releases are immutable environment. Use `@production` only for that canary. The `Publish release` workflow runs after every `production` push. It does -nothing unless that push changes `package.json`; then it runs `make test`, -refuses to reuse an existing tag, and publishes that GitHub release from the -merged commit. The `Verify release` workflow then validates the published tag. +nothing unless that push changes the `package.json` version; then it runs +`make test`, refuses to reuse an existing tag, and publishes that GitHub +release from the merged commit. The `Verify release` workflow then validates the published tag. After the GitHub release is created, `#rabbit-support` receives the Marketplace handoff through `SLACK_WEBHOOK_RABBIT_SUPPORT`; the message directs the operator to wait for verification before publishing to Marketplace. @@ -23,7 +23,8 @@ operator to wait for verification before publishing to Marketplace. `package.json` is the single release-version source. Its semantic version maps to the Git tag by adding `v` and must match the first semantic heading in `CHANGELOG.md`; `make test` enforces that contract before a release can be -published. +published. A production push creates a release only when this version changes, +and its version must increase. ## Publish to GitHub Marketplace