@@ -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 }}
0 commit comments