mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
Audit actual first-parent ancestry instead of trusting the release bot chronological cutoff, and block mismatched versions before tag creation. Sync the published 6.0.0 baseline without dropping newer main changes. Fixes #1063 Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)
319 lines
13 KiB
YAML
319 lines
13 KiB
YAML
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
|
||
|
||
- 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"
|
||
|
||
- 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: 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
|