mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
408 lines
17 KiB
YAML
408 lines
17 KiB
YAML
name: Release
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
pull_request:
|
||
branches: [main]
|
||
types: [closed]
|
||
workflow_dispatch:
|
||
inputs:
|
||
release_branch:
|
||
description: Frozen release branch (release/VERSION)
|
||
required: true
|
||
type: string
|
||
phase:
|
||
description: Prepare the release PR, or publish its reviewed merge
|
||
required: true
|
||
default: prepare
|
||
type: choice
|
||
options: [prepare, publish]
|
||
|
||
env:
|
||
HUSKY: "0"
|
||
|
||
concurrency:
|
||
group: release-${{ inputs.release_branch || github.event.pull_request.merge_commit_sha || github.ref_name }}
|
||
cancel-in-progress: false
|
||
|
||
jobs:
|
||
release-proposal:
|
||
name: Update release proposal
|
||
if: github.event_name == 'push'
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
pull-requests: write
|
||
steps:
|
||
- name: Respect finalized release notes
|
||
id: editorial
|
||
env:
|
||
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||
GH_REPO: ${{ github.repository }}
|
||
run: |
|
||
finalized=$(gh api --paginate "repos/${GH_REPO}/pulls?state=open&base=main&per_page=100" \
|
||
--jq '.[] | select(.head.repo.full_name == .base.repo.full_name) | select(.head.ref | startswith("release-please--branches--main--")) | select(any(.labels[]; .name == "rea:release-notes-finalized")) | .number')
|
||
if [[ -n "${finalized}" ]]; then
|
||
echo "Finalized release notes on PR ${finalized}; automatic proposal refresh is paused." >> "$GITHUB_STEP_SUMMARY"
|
||
echo "frozen=true" >> "$GITHUB_OUTPUT"
|
||
fi
|
||
|
||
- uses: googleapis/release-please-action@v4
|
||
if: steps.editorial.outputs.frozen != 'true'
|
||
with:
|
||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||
target-branch: main
|
||
skip-github-release: true
|
||
|
||
merged-release:
|
||
name: Release reviewed main merge
|
||
if: >-
|
||
github.event_name == 'pull_request' && github.event.pull_request.merged == true &&
|
||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||
startsWith(github.event.pull_request.head.ref, 'release-please--branches--main--')
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
pull-requests: write
|
||
outputs:
|
||
release_created: ${{ steps.release.outputs.release_created }}
|
||
sha: ${{ steps.release.outputs.sha }}
|
||
steps:
|
||
- name: Check out reviewed release merge
|
||
uses: actions/checkout@v6
|
||
with:
|
||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version-file: .nvmrc
|
||
package-manager-cache: false
|
||
|
||
- run: npm ci
|
||
|
||
- name: Validate reviewed merge before release creation
|
||
env:
|
||
SOURCE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||
run: |
|
||
if [[ "${SOURCE_SHA}" != "${GITHUB_SHA}" ]]; then
|
||
echo "The reviewed merge differs from the workflow source; refusing mismatched publication provenance." >&2
|
||
exit 1
|
||
fi
|
||
node scripts/verify-release-checkpoint.mjs \
|
||
--phase publish --release-branch main --source-sha "$SOURCE_SHA"
|
||
|
||
- name: Validate finalized notes before release creation
|
||
env:
|
||
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||
GH_REPO: ${{ github.repository }}
|
||
RELEASE_PR: ${{ github.event.pull_request.number }}
|
||
SOURCE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||
run: |
|
||
node scripts/release-notes.mjs publication --repository "$GH_REPO" \
|
||
--source "$SOURCE_SHA" --pr "$RELEASE_PR"
|
||
|
||
- uses: googleapis/release-please-action@v4
|
||
id: release
|
||
with:
|
||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||
target-branch: main
|
||
skip-github-release: false
|
||
skip-github-pull-request: true
|
||
|
||
- name: Bind publication to the reviewed merge
|
||
env:
|
||
RELEASE_CREATED: ${{ steps.release.outputs.release_created }}
|
||
RELEASE_SHA: ${{ steps.release.outputs.sha }}
|
||
SOURCE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||
run: |
|
||
if [[ "${RELEASE_CREATED}" != true ]]; then
|
||
echo "Release Please did not create a release for the reviewed merge. Check its pending label and release metadata; for a partial publication, re-run failed jobs in the original run." >&2
|
||
exit 1
|
||
fi
|
||
if [[ "${RELEASE_SHA}" != "${SOURCE_SHA}" ]]; then
|
||
echo "The tagged release differs from the reviewed merge; refusing mismatched publication provenance. The GitHub release already exists; do not create another release as a retry." >&2
|
||
exit 1
|
||
fi
|
||
|
||
release-please:
|
||
name: Release Please
|
||
if: github.event_name == 'workflow_dispatch'
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
pull-requests: write
|
||
outputs:
|
||
release_created: ${{ steps.release.outputs.release_created }}
|
||
sha: ${{ steps.release.outputs.sha }}
|
||
steps:
|
||
- name: Validate release selection
|
||
shell: bash
|
||
env:
|
||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||
RELEASE_PHASE: ${{ inputs.phase }}
|
||
run: |
|
||
if [[ "${RELEASE_PHASE}" != prepare && "${RELEASE_PHASE}" != publish ]]; then
|
||
echo "phase must be prepare or publish." >&2
|
||
exit 1
|
||
fi
|
||
if [[ "${RELEASE_PHASE}" == prepare && "${GITHUB_REF}" != "refs/heads/main" ]]; then
|
||
echo "Run prepare from main; release_branch selects the frozen source." >&2
|
||
exit 1
|
||
fi
|
||
if [[ "${RELEASE_PHASE}" == publish && "${GITHUB_REF}" != "refs/heads/${RELEASE_BRANCH}" ]]; then
|
||
echo "Run publish from the selected frozen release branch so npm provenance records its source." >&2
|
||
exit 1
|
||
fi
|
||
if [[ ! "${RELEASE_BRANCH}" =~ ^release/[^/]+$ ]]; then
|
||
echo "release_branch must select a release/ branch; main is not a release checkpoint." >&2
|
||
exit 1
|
||
fi
|
||
if ! git check-ref-format "refs/heads/${RELEASE_BRANCH}"; then
|
||
echo "Invalid release_branch: ${RELEASE_BRANCH}" >&2
|
||
exit 1
|
||
fi
|
||
|
||
- name: Resolve release branch tip
|
||
id: branch
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||
run: |
|
||
sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${RELEASE_BRANCH}" --jq '.object | select(.type == "commit") | .sha')"
|
||
if [[ ! "${sha}" =~ ^[0-9a-f]{40}$ ]]; then
|
||
echo "The release branch tip is not a commit." >&2
|
||
exit 1
|
||
fi
|
||
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
|
||
|
||
- name: Require the publish dispatch to match the branch tip
|
||
if: inputs.phase == 'publish'
|
||
shell: bash
|
||
env:
|
||
BRANCH_SHA: ${{ steps.branch.outputs.sha }}
|
||
run: |
|
||
if [[ "${BRANCH_SHA}" != "${GITHUB_SHA}" ]]; then
|
||
echo "The release branch tip differs from the dispatched checkpoint; refusing to create a release for a moved source." >&2
|
||
exit 1
|
||
fi
|
||
|
||
- name: Check out release controller and Git history
|
||
uses: actions/checkout@v6
|
||
with:
|
||
ref: ${{ github.sha }}
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
|
||
- name: Set up Node.js for checkpoint validation
|
||
uses: actions/setup-node@v6
|
||
with:
|
||
node-version-file: .nvmrc
|
||
package-manager-cache: false
|
||
|
||
- name: Install checkpoint validation dependencies
|
||
run: npm ci
|
||
|
||
- name: Validate checkpoint version and ancestry before release creation
|
||
env:
|
||
RELEASE_PHASE: ${{ inputs.phase }}
|
||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||
SOURCE_SHA: ${{ steps.branch.outputs.sha }}
|
||
run: >-
|
||
node scripts/verify-release-checkpoint.mjs
|
||
--phase "$RELEASE_PHASE" --release-branch "$RELEASE_BRANCH"
|
||
--source-sha "$SOURCE_SHA"
|
||
|
||
- name: Validate manual finalized notes before release creation
|
||
if: inputs.phase == 'publish'
|
||
env:
|
||
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||
GH_REPO: ${{ github.repository }}
|
||
SOURCE_SHA: ${{ steps.branch.outputs.sha }}
|
||
run: |
|
||
node scripts/release-notes.mjs publication --repository "$GH_REPO" \
|
||
--source "$SOURCE_SHA"
|
||
|
||
- uses: googleapis/release-please-action@v4
|
||
id: release
|
||
with:
|
||
# A dedicated credential starts PR CI automatically; the built-in
|
||
# token creates approval-required PR runs for maintainers to approve.
|
||
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
|
||
target-branch: ${{ inputs.release_branch }}
|
||
skip-github-release: ${{ inputs.phase == 'prepare' }}
|
||
skip-github-pull-request: ${{ inputs.phase == 'publish' }}
|
||
|
||
- name: Require a merged release PR for publication
|
||
if: inputs.phase == 'publish' && steps.release.outputs.release_created != 'true'
|
||
run: |
|
||
echo "No new release was created. Merge the reviewed release PR into its frozen branch before publishing. For a partial publication, re-run failed jobs in the original run." >&2
|
||
exit 1
|
||
|
||
- name: Bind publication to the dispatched checkpoint
|
||
if: inputs.phase == 'publish' && steps.release.outputs.release_created == 'true'
|
||
shell: bash
|
||
env:
|
||
RELEASE_SHA: ${{ steps.release.outputs.sha }}
|
||
run: |
|
||
if [[ "${RELEASE_SHA}" != "${GITHUB_SHA}" ]]; then
|
||
echo "The tagged release SHA differs from the dispatched checkpoint; refusing to publish mismatched source provenance. The GitHub release already exists; do not dispatch publish again." >&2
|
||
exit 1
|
||
fi
|
||
|
||
- name: Check out release pull request
|
||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||
uses: actions/checkout@v6
|
||
with:
|
||
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
|
||
fetch-depth: 0
|
||
persist-credentials: false
|
||
|
||
- name: Set up Node.js for generated documentation
|
||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||
uses: actions/setup-node@v6
|
||
with:
|
||
node-version-file: .nvmrc
|
||
package-manager-cache: false
|
||
|
||
- name: Install dependencies
|
||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||
run: npm ci
|
||
|
||
- name: Validate prepared release version and migration notes
|
||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||
env:
|
||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||
SOURCE_SHA: ${{ steps.branch.outputs.sha }}
|
||
run: >-
|
||
node scripts/verify-release-checkpoint.mjs
|
||
--phase prepare --stage candidate --release-branch "$RELEASE_BRANCH"
|
||
--source-sha "$SOURCE_SHA"
|
||
|
||
- name: Regenerate release documentation
|
||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||
run: npm run docs:generate
|
||
|
||
- name: Validate generated release documentation
|
||
if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'
|
||
run: npm run docs:check && git diff --exit-code
|
||
|
||
publish:
|
||
name: Publish to npm
|
||
needs: [merged-release, release-please]
|
||
if: >-
|
||
always() && !cancelled() &&
|
||
((needs.merged-release.result == 'success' && needs.merged-release.outputs.release_created == 'true') ||
|
||
(needs.release-please.result == 'success' && inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'))
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
id-token: write
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
|
||
persist-credentials: false
|
||
|
||
- uses: actions/setup-node@v6
|
||
with:
|
||
node-version-file: .nvmrc
|
||
package-manager-cache: false
|
||
registry-url: https://registry.npmjs.org
|
||
|
||
- run: npm ci
|
||
|
||
- name: Prepare Windows native build toolchain
|
||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
|
||
|
||
- name: Build and verify bundled Windows native controls
|
||
run: npm run build:windows-native && npm run verify:windows-native:artifact
|
||
|
||
- name: Verify packaged release before publishing
|
||
run: npm run verify:package
|
||
|
||
- name: Publish
|
||
shell: bash
|
||
run: |
|
||
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
|
||
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||
echo "rea-agents@${version} is already published; verifying the existing package."
|
||
else
|
||
tag="$(node scripts/release-npm-tag.mjs "${version}")"
|
||
npm publish --access public --tag "${tag}"
|
||
fi
|
||
|
||
- name: Verify published CLI from npm
|
||
shell: bash
|
||
run: |
|
||
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
|
||
verification_root="$(mktemp -d)"
|
||
trap 'rm -rf -- "${verification_root}"' EXIT
|
||
cd "${verification_root}"
|
||
max_attempts=120
|
||
sleep_seconds=15
|
||
total_window_minutes=$((max_attempts * sleep_seconds / 60))
|
||
for attempt in $(seq 1 "${max_attempts}"); do
|
||
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||
echo "rea-agents@${version} is visible on npm after attempt ${attempt}/${max_attempts}; verifying the published CLI."
|
||
npm exec --yes --package="rea-agents@${version}" -- rea --help
|
||
npm exec --yes --package="rea-agents@${version}" -- rea --llms
|
||
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
|
||
exit 0
|
||
else
|
||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/${max_attempts}; up to ${total_window_minutes} minutes)" >&2
|
||
fi
|
||
if [ "${attempt}" -lt "${max_attempts}" ]; then
|
||
sleep "${sleep_seconds}"
|
||
fi
|
||
done
|
||
echo "rea-agents@${version} did not become visible on npm within ${total_window_minutes} minutes (${max_attempts} attempts × ${sleep_seconds}s)" >&2
|
||
echo "This indicates npm registry propagation delay, not a CLI verification failure." >&2
|
||
exit 1
|
||
|
||
publish-mcp:
|
||
name: Publish to MCP Registry
|
||
needs: [merged-release, release-please, publish]
|
||
if: >-
|
||
always() && !cancelled() && needs.publish.result == 'success' &&
|
||
((needs.merged-release.result == 'success' && needs.merged-release.outputs.release_created == 'true') ||
|
||
(needs.release-please.result == 'success' && inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true'))
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
id-token: write
|
||
steps:
|
||
- uses: actions/checkout@v6
|
||
with:
|
||
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
|
||
persist-credentials: false
|
||
|
||
- name: Verify package and registry metadata versions
|
||
run: npm run mcp-registry:check
|
||
|
||
- name: Install MCP Registry publisher
|
||
env:
|
||
MCP_PUBLISHER_VERSION: v1.8.0
|
||
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
|
||
run: |
|
||
archive="mcp-publisher_linux_amd64.tar.gz"
|
||
curl -fsSLo "${archive}" \
|
||
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
|
||
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
|
||
tar xzf "${archive}" mcp-publisher
|
||
chmod +x mcp-publisher
|
||
|
||
- name: Validate MCP Registry metadata
|
||
run: ./mcp-publisher validate
|
||
|
||
- name: Authenticate to MCP Registry
|
||
run: ./mcp-publisher login github-oidc
|
||
|
||
- name: Publish MCP Registry metadata
|
||
run: ./mcp-publisher publish
|