diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..e6011cc --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,126 @@ +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 new version + id: release + shell: bash + run: | + set -euo pipefail + + 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 version was not changed by this production push." + exit 0 + fi + + 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 + echo "::error title=Release already exists::Tag $release_tag already exists. Bump package.json 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" + printf 'notes_file=%s\n' "$notes_file" + 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..368122d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,9 +6,12 @@ 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 +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. 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..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: @@ -41,3 +43,17 @@ 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 + +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 9fa5dbc..7f52a47 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -3,30 +3,43 @@ 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. 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. -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 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. + +`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. A production push creates a release only when this version changes, +and its version must increase. + +## 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 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" + } +}