fix(release): detect a release by the version field, not the manifest diff (#571)

`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.
This commit is contained in:
Jack Chen authored and GitHub committed 2026-09-04 01:20:00 +08:00
1 parent 98516cedfc
commit c3722ab197
2 files changed
+42 -13

No files matched your search

+8 -3
View File
@@ -188,9 +188,14 @@ the full set of template traps.
the normal PR CI remain authoritative. Never merge a release proposal merely
because its model reviewer approved it.
3. **Wait for `CI` on the exact merged release commit.** A successful `CI`
workflow run on `main` triggers `publish.yml`. The publisher verifies that
both core and create-oma-app versions increased relative to that commit's
first parent; later unrelated commits cannot accidentally publish.
workflow run on `main` triggers `publish.yml`, which reads the `version`
field of the core and create-oma-app manifests at that commit and at its
first parent and stops before the publish job unless both moved. The
publisher then re-derives the same fact and refuses a commit whose versions
did not increment, so later unrelated commits cannot accidentally publish.
Reading the version rather than asking whether those manifests changed at
all is what keeps an ordinary dependency or metadata edit from starting a
job that mints a write-scoped token and then fails closed.
4. **Publish to npm in order: `core`, then `otel` when its declared version is
not already live, then `create-oma-app`.** The publisher checks the public
registry before every action and waits for each new version to resolve
+34 -10
View File
@@ -60,16 +60,40 @@ jobs:
run: |
set -euo pipefail
echo "sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
if git diff --quiet "$RELEASE_SHA^" "$RELEASE_SHA" -- packages/core/package.json; then
echo "is_release=false" >> "$GITHUB_OUTPUT"
echo "core version manifest did not change; publication is a no-op"
exit 0
fi
if git diff --quiet "$RELEASE_SHA^" "$RELEASE_SHA" -- packages/create-oma-app/package.json; then
echo "is_release=false" >> "$GITHUB_OUTPUT"
echo "create-oma-app version manifest did not change; publication is a no-op"
exit 0
fi
# 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: