Skip to content

Commit 197dcbe

Browse files
authored
Merge pull request #5441 from code-yeongyu/release/v4.12.0-source-state-20260620
fix(publish): gate npm publish on release state merge
2 parents 6544c2d + eae7cff commit 197dcbe

3 files changed

Lines changed: 159 additions & 8 deletions

File tree

.github/workflows/publish.yml

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,143 @@ jobs:
329329
echo "Check the requested version format, bump input, or npm metadata lookup."
330330
} >> "$GITHUB_STEP_SUMMARY"
331331
332+
prepare-release-state:
333+
runs-on: ubuntu-latest
334+
needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata]
335+
if: >-
336+
always() &&
337+
github.repository == 'code-yeongyu/oh-my-openagent' &&
338+
needs.test.result == 'success' &&
339+
needs.typecheck.result == 'success' &&
340+
needs.codex-compatibility.result == 'success' &&
341+
needs.preflight-trust.result == 'success' &&
342+
needs.release-metadata.result == 'success'
343+
permissions:
344+
actions: read
345+
contents: write
346+
id-token: write
347+
pull-requests: write
348+
outputs:
349+
release_sha: ${{ steps.prepare.outputs.release_sha }}
350+
steps:
351+
- uses: actions/checkout@v5
352+
with:
353+
fetch-depth: 0
354+
355+
- run: git fetch --force --tags
356+
357+
- uses: oven-sh/setup-bun@v2
358+
with:
359+
bun-version: "1.3.12"
360+
361+
- name: Install dependencies
362+
run: bun install --frozen-lockfile
363+
364+
- name: Prepare and merge release state before publishing
365+
id: prepare
366+
env:
367+
VERSION: ${{ needs.release-metadata.outputs.version }}
368+
RELEASE_REF: ${{ github.ref_name }}
369+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
370+
run: |
371+
set -euo pipefail
372+
373+
BASE_REF="${RELEASE_REF:-dev}"
374+
RELEASE_BRANCH="release/v${VERSION}-source-state"
375+
376+
git fetch origin "${BASE_REF}" --tags
377+
378+
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
379+
RELEASE_SHA="$(git rev-list --max-count=1 "refs/tags/v${VERSION}")"
380+
echo "release_sha=${RELEASE_SHA}" >> "$GITHUB_OUTPUT"
381+
echo "Release tag v${VERSION} already exists locally at ${RELEASE_SHA}"
382+
exit 0
383+
fi
384+
385+
RELEASE_SHA="$(git rev-list --max-count=1 --grep="^release: v${VERSION}$" "origin/${BASE_REF}" 2>/dev/null || true)"
386+
if [ -n "$RELEASE_SHA" ]; then
387+
echo "release_sha=${RELEASE_SHA}" >> "$GITHUB_OUTPUT"
388+
echo "Release commit already exists on origin/${BASE_REF}: ${RELEASE_SHA}"
389+
exit 0
390+
fi
391+
392+
git checkout -B "$RELEASE_BRANCH" "origin/${BASE_REF}"
393+
394+
CURRENT_VERSION="$(node -p "require('./package.json').version")"
395+
if [ "$CURRENT_VERSION" = "$VERSION" ]; then
396+
RELEASE_SHA="$(git rev-parse HEAD)"
397+
echo "release_sha=${RELEASE_SHA}" >> "$GITHUB_OUTPUT"
398+
echo "origin/${BASE_REF} is already stamped as v${VERSION}: ${RELEASE_SHA}"
399+
exit 0
400+
fi
401+
402+
jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json
403+
404+
for platform in darwin-arm64 darwin-x64 darwin-x64-baseline linux-x64 linux-x64-baseline linux-arm64 linux-x64-musl linux-x64-musl-baseline linux-arm64-musl windows-x64 windows-x64-baseline windows-arm64; do
405+
package_dir="packages/oh-my-opencode-${platform}"
406+
jq --arg v "$VERSION" '.version = $v' "${package_dir}/package.json" > tmp.json
407+
mv tmp.json "${package_dir}/package.json"
408+
done
409+
410+
jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)' package.json > tmp.json && mv tmp.json package.json
411+
412+
node packages/omo-codex/plugin/scripts/sync-version.mjs
413+
node packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs
414+
bun install --lockfile-only
415+
416+
git config user.email "github-actions[bot]@users.noreply.github.com"
417+
git config user.name "github-actions[bot]"
418+
git add package.json packages/oh-my-opencode-*/package.json packages/omo-codex/package.json packages/omo-codex/plugin/package.json packages/omo-codex/plugin/.codex-plugin/plugin.json packages/omo-codex/plugin/components/*/package.json packages/omo-codex/plugin/hooks/*.json packages/omo-codex/plugin/components/*/hooks/hooks.json bun.lock
419+
git diff --cached --quiet || git commit -m "release: v${VERSION}"
420+
git push --force-with-lease origin "HEAD:${RELEASE_BRANCH}"
421+
422+
PR_NUMBER="$(gh pr list --head "$RELEASE_BRANCH" --base "$BASE_REF" --state open --json number --jq '.[0].number // empty')"
423+
if [ -z "$PR_NUMBER" ]; then
424+
PR_URL="$(gh pr create --base "$BASE_REF" --head "$RELEASE_BRANCH" --title "release: v${VERSION}" --body "Automated release-state PR for v${VERSION}. This must merge before npm publication starts so protected-branch push failures cannot create a half-published release.")"
425+
PR_NUMBER="${PR_URL##*/}"
426+
fi
427+
428+
gh pr merge "$PR_NUMBER" --merge --auto --delete-branch=false
429+
430+
for attempt in $(seq 1 120); do
431+
PR_STATE="$(gh pr view "$PR_NUMBER" --json state --jq '.state')"
432+
if [ "$PR_STATE" = "MERGED" ]; then
433+
RELEASE_SHA="$(gh pr view "$PR_NUMBER" --json mergeCommit --jq '.mergeCommit.oid')"
434+
echo "release_sha=${RELEASE_SHA}" >> "$GITHUB_OUTPUT"
435+
echo "Release-state PR #${PR_NUMBER} merged at ${RELEASE_SHA}"
436+
exit 0
437+
fi
438+
439+
FAILURES="$(gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '[.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "CANCELLED" or .conclusion == "TIMED_OUT")] | length')"
440+
if [ "$FAILURES" != "0" ]; then
441+
echo "::error::Release-state PR #${PR_NUMBER} has failing required checks; refusing to publish npm packages."
442+
gh pr view "$PR_NUMBER" --json url,statusCheckRollup --jq '{url, statusCheckRollup}'
443+
exit 1
444+
fi
445+
446+
echo "Waiting for release-state PR #${PR_NUMBER} to merge (attempt ${attempt}/120)"
447+
sleep 30
448+
done
449+
450+
echo "::error::Timed out waiting for release-state PR #${PR_NUMBER} to merge; refusing to publish npm packages."
451+
exit 1
452+
453+
- name: Write job summary
454+
if: always()
455+
shell: bash
456+
env:
457+
JOB_SUMMARY_TITLE: Release source-state gate
458+
JOB_SUMMARY_STATUS: ${{ job.status }}
459+
JOB_SUMMARY_DETAILS: |
460+
- Ensures the `release: v${{ needs.release-metadata.outputs.version }}` source-state commit exists on the release branch before npm publish starts.
461+
- Creates and auto-merges a release-state PR when source manifests need stamping.
462+
- Refuses to publish packages if protected-branch rules or required checks prevent that PR from merging.
463+
JOB_SUMMARY_NEXT: If this fails, merge the release-state PR or fix its checks before rerunning publish.
464+
run: GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" bash .github/scripts/write-job-summary.sh
465+
332466
publish-main:
333467
runs-on: ubuntu-latest
334-
needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, publish-platform]
468+
needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, prepare-release-state, publish-platform]
335469
if: >-
336470
always() &&
337471
github.repository == 'code-yeongyu/oh-my-openagent' &&
@@ -340,6 +474,7 @@ jobs:
340474
needs.codex-compatibility.result == 'success' &&
341475
needs.preflight-trust.result == 'success' &&
342476
needs.release-metadata.result == 'success' &&
477+
needs.prepare-release-state.result == 'success' &&
343478
(inputs.skip_platform == true || needs.publish-platform.result == 'success')
344479
steps:
345480
- uses: actions/checkout@v5
@@ -622,7 +757,7 @@ jobs:
622757
run: GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" bash .github/scripts/write-job-summary.sh
623758

624759
publish-platform:
625-
needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata]
760+
needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, prepare-release-state]
626761
if: >-
627762
always() &&
628763
github.repository == 'code-yeongyu/oh-my-openagent' &&
@@ -631,7 +766,8 @@ jobs:
631766
needs.typecheck.result == 'success' &&
632767
needs.codex-compatibility.result == 'success' &&
633768
needs.preflight-trust.result == 'success' &&
634-
needs.release-metadata.result == 'success'
769+
needs.release-metadata.result == 'success' &&
770+
needs.prepare-release-state.result == 'success'
635771
uses: ./.github/workflows/publish-platform.yml
636772
with:
637773
version: ${{ needs.release-metadata.outputs.version }}

script/ci-job-summary-workflow.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,16 @@ const workflowExpectations = [
3131
{ path: ".github/workflows/publish-platform.yml", jobs: ["build", "publish"] },
3232
{
3333
path: ".github/workflows/publish.yml",
34-
jobs: ["test", "typecheck", "codex-compatibility", "preflight-trust", "release-metadata", "publish-main", "release"],
34+
jobs: [
35+
"test",
36+
"typecheck",
37+
"codex-compatibility",
38+
"preflight-trust",
39+
"release-metadata",
40+
"prepare-release-state",
41+
"publish-main",
42+
"release",
43+
],
3544
},
3645
{ path: ".github/workflows/refresh-model-capabilities.yml", jobs: ["refresh"] },
3746
{ path: ".github/workflows/sisyphus-agent.yml", jobs: ["agent"] },

script/publish-workflow.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,14 @@ describe("test workflows", () => {
122122
codexCompatibilityJob.indexOf("name: Run Codex compatibility tests")
123123
const runsCodexCommand = codexCompatibilityJob.includes("run: bun run test:codex")
124124
const publishMainNeedsCodex =
125-
workflow.includes("needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, publish-platform]") &&
125+
workflow.includes(
126+
"needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, prepare-release-state, publish-platform]",
127+
) &&
126128
workflow.includes("needs.codex-compatibility.result == 'success'")
127129
const publishPlatformNeedsCodex =
128-
workflow.includes("needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata]") &&
130+
workflow.includes(
131+
"needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, prepare-release-state]",
132+
) &&
129133
workflow.includes("needs.codex-compatibility.result == 'success'")
130134

131135
// #then
@@ -296,7 +300,9 @@ describe("test workflows", () => {
296300
const computesVersionOnce = (workflow.match(/id: version/g) ?? []).length === 1
297301
const platformUsesMetadata = workflow.includes("version: ${{ needs.release-metadata.outputs.version }}") &&
298302
workflow.includes("dist_tag: ${{ needs.release-metadata.outputs.dist_tag }}")
299-
const mainWaitsForPlatform = workflow.includes("needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, publish-platform]") &&
303+
const mainWaitsForPlatform = workflow.includes(
304+
"needs: [test, typecheck, codex-compatibility, preflight-trust, release-metadata, prepare-release-state, publish-platform]",
305+
) &&
300306
workflow.includes("inputs.skip_platform == true || needs.publish-platform.result == 'success'")
301307
const releaseUsesMetadata = workflow.includes("VERSION: ${{ needs.release-metadata.outputs.version }}")
302308
const wrappersVerifyPlatformPackages = workflow.includes("name: Verify platform packages are published") &&
@@ -504,7 +510,7 @@ describe("test workflows", () => {
504510
expect(publishIds, "PLATFORM_PACKAGE_IDS must match build-binaries PLATFORMS exactly").toEqual(
505511
buildBinariesPlatforms,
506512
)
507-
expect(publishYmlLists.length, "publish.yml must enumerate platforms in 2 PLATFORMS arrays + 2 version-bump loops").toBe(4)
513+
expect(publishYmlLists.length, "publish.yml must enumerate platforms in 2 PLATFORMS arrays + 3 version-bump loops").toBe(5)
508514
for (const publishYmlList of publishYmlLists) {
509515
expect(publishYmlList, "every publish.yml platform list must match build-binaries PLATFORMS exactly").toEqual(
510516
buildBinariesPlatforms,

0 commit comments

Comments
 (0)