Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 11 additions & 1 deletion .rabbit/repo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,7 +44,8 @@ configuration:
- BITBUCKET_TOKEN
- CONTEXT7_API_KEY
- DEPENDABOT_REVIEWER_TOKEN
repository: []
repository:
- SLACK_WEBHOOK_RABBIT_SUPPORT
variables:
organization:
- DOCKER_LOGIN
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 18 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
43 changes: 28 additions & 15 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}