name: Release on: 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 }} cancel-in-progress: false jobs: release-please: name: Release Please 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 - 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 }} 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: Regenerate release documentation if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' run: npm run docs:generate - name: Commit canonical release catalog if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' shell: bash env: # Use the same identity for the follow-up commit so it also triggers CI. GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} RELEASE_PR_JSON: ${{ steps.release.outputs.pr }} run: | release_pr_branch="$(node --input-type=module -e ' const value = JSON.parse(process.env.RELEASE_PR_JSON ?? ""); if ( typeof value !== "object" || value === null || typeof value.headBranchName !== "string" || value.headBranchName.length === 0 ) { process.exit(1); } process.stdout.write(value.headBranchName); ')" changed_paths="$(git diff --name-only)" if [[ -z "${changed_paths}" ]]; then echo "Release documentation is already canonical." exit 0 fi if [[ "${changed_paths}" != "docs/product-catalog.json" ]]; then echo "docs:generate changed unexpected tracked paths:" >&2 printf '%s\n' "${changed_paths}" >&2 exit 1 fi git diff --check git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add -- docs/product-catalog.json git commit -m "chore: normalize release product catalog" gh auth setup-git git push origin "HEAD:${release_pr_branch}" publish: name: Publish to npm needs: release-please if: 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.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 npm publish --access public 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: [release-please, publish] if: inputs.phase == 'publish' && needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success' runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v6 with: ref: ${{ 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