mirror of
https://github.com/open-multi-agent/open-multi-agent.git
synced 2026-10-19 01:31:07 -05:00
`publish.yml` decided whether a commit on main was a release by asking
whether packages/core/package.json and packages/create-oma-app/package.json
had changed at all. Those two files carry more than a version: dependency
specs, `exports`, `files`, `engines`, keywords. Any edit to either one read as
a release.
98516ce was the first commit to hit it. It only pinned dependency specs, and
neither version moved, but both manifests changed, so `detect_release_commit`
reported `is_release=true` and the publish job started. It entered the
`npm-release` environment, minted a contents:write GitHub App token, checked
out, installed, built, and then stopped at the publisher's own guard:
98516ce... is not a release commit: @open-multi-agent/core did not
increment from its first parent.
Nothing was published, tagged, or released, and the registry versions are
untouched, so the safety net did its job. But the outer gate should not have
opened. A dependency bump is the most ordinary maintenance there is, and it
should not cost a write-scoped token and a red Publish run every time.
The detection now reads the `version` field of both manifests at the release
commit and at its first parent and stops unless both moved, which is the same
fact the publisher re-derives later. A manifest that does not exist at the
release commit is a no-op rather than an error, matching what the old file
comparison concluded for the pre-monorepo tags. A manifest that exists but
declares no version fails the job, because that is a broken tree rather than
an ordinary commit.
It also closes a fail-open path found while building the check harness.
`git diff --quiet` exits 1 for "changed" and 128 for "could not run", and the
old step read both as "changed", so any git failure resolved to
`is_release=true`. Reading the version field fails closed instead. Unreachable
in practice, since checkout and the ancestry verification both run first, but
a publish gate should not lean on that.
Checked against every `v*` tag in the repository. All three tags from after
publish.yml existed — v1.16.0, v1.16.1, v1.17.0 — still detect as releases,
as do v1.8.0 through v1.10.0 and v1.13.0 through v1.15.0. v1.11.0 and v1.12.1
now report false because their tagged commits genuinely did not move both
versions; both predate this workflow, and the publisher would have refused
them for exactly that reason, so the gate now agrees with the publisher
instead of deferring the disagreement to a failed job.
138 lines
5.0 KiB
YAML
138 lines
5.0 KiB
YAML
name: Publish
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows: [CI]
|
|
types: [completed]
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
inputs:
|
|
sha:
|
|
description: Exact merged release commit to recover
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: oma-publish
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
detect_release_commit:
|
|
if: >-
|
|
github.repository == 'open-multi-agent/open-multi-agent' &&
|
|
(github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success')
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
outputs:
|
|
is_release: ${{ steps.detect.outputs.is_release }}
|
|
sha: ${{ steps.detect.outputs.sha }}
|
|
env:
|
|
RELEASE_SHA: ${{ github.event_name == 'workflow_dispatch' && inputs.sha || github.event.workflow_run.head_sha }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ env.RELEASE_SHA }}
|
|
fetch-depth: 0
|
|
|
|
- name: Verify the candidate belongs to main
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch origin main
|
|
git merge-base --is-ancestor "$RELEASE_SHA" origin/main
|
|
test "$(git rev-parse HEAD)" = "$RELEASE_SHA"
|
|
|
|
- name: Verify manual recovery already passed CI
|
|
if: github.event_name == 'workflow_dispatch'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
gh api "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$RELEASE_SHA&status=completed&per_page=100" \
|
|
--jq '.workflow_runs[] | select(.name == "CI" and .conclusion == "success") | .head_sha' \
|
|
| grep -Fxq "$RELEASE_SHA"
|
|
|
|
- name: Detect a release version commit
|
|
id: detect
|
|
run: |
|
|
set -euo pipefail
|
|
echo "sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
|
|
# Read the version field rather than asking whether the manifest
|
|
# changed at all, so this agrees with the publisher's own guard:
|
|
# both versions must have incremented from the first parent. A
|
|
# dependency bump, a metadata fix, or an `exports` edit touches
|
|
# these files without being a release, and detecting on the file
|
|
# made every one of those start the publish job, mint a
|
|
# contents:write App token, and only then fail closed. Verified on
|
|
# 98516ce, a dependency-only commit that reached this step as a
|
|
# false positive.
|
|
manifest_exists() { git cat-file -e "$1:$2" 2>/dev/null; }
|
|
version_at() { git show "$1:$2" | jq -r '.version // empty'; }
|
|
for manifest in packages/core/package.json packages/create-oma-app/package.json; do
|
|
if ! manifest_exists "$RELEASE_SHA" "$manifest"; then
|
|
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
|
echo "$manifest does not exist at $RELEASE_SHA; publication is a no-op"
|
|
exit 0
|
|
fi
|
|
version=$(version_at "$RELEASE_SHA" "$manifest")
|
|
if [ -z "$version" ]; then
|
|
echo "::error::$manifest declares no version at $RELEASE_SHA"
|
|
exit 1
|
|
fi
|
|
# A missing parent manifest means this one is new here, which
|
|
# counts as a change rather than a reason to skip publication.
|
|
parent=''
|
|
if manifest_exists "$RELEASE_SHA^" "$manifest"; then
|
|
parent=$(version_at "$RELEASE_SHA^" "$manifest")
|
|
fi
|
|
if [ "$version" = "$parent" ]; then
|
|
echo "is_release=false" >> "$GITHUB_OUTPUT"
|
|
echo "$manifest still declares $version; publication is a no-op"
|
|
exit 0
|
|
fi
|
|
done
|
|
echo "is_release=true" >> "$GITHUB_OUTPUT"
|
|
|
|
publish:
|
|
needs: detect_release_commit
|
|
if: needs.detect_release_commit.outputs.is_release == 'true'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
environment: npm-release
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
env:
|
|
RELEASE_SHA: ${{ needs.detect_release_commit.outputs.sha }}
|
|
steps:
|
|
- name: Create narrowly scoped GitHub App token
|
|
id: app-token
|
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
with:
|
|
client-id: ${{ vars.RELEASE_BOT_APP_CLIENT_ID }}
|
|
private-key: ${{ secrets.RELEASE_BOT_APP_PRIVATE_KEY }}
|
|
permission-contents: write
|
|
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ env.RELEASE_SHA }}
|
|
fetch-depth: 0
|
|
token: ${{ steps.app-token.outputs.token }}
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24
|
|
registry-url: https://registry.npmjs.org
|
|
|
|
- run: npm ci --ignore-scripts
|
|
- run: npm run build
|
|
|
|
- name: Publish missing packages, tag, and create the GitHub Release
|
|
env:
|
|
RELEASE_BOT_EXPECTED_SHA: ${{ env.RELEASE_SHA }}
|
|
RELEASE_BOT_GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
run: node packages/release-bot/dist/cli.js publish
|