Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions .github/workflows/publish-persona.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
# `workflow_dispatch` pins `github.sha` at dispatch time; a queued run
# would otherwise bump and changelog from a stale commit. See the same
# note in publish.yml.
ref: ${{ github.ref_name }}

- name: Setup pnpm
uses: pnpm/action-setup@v5
Expand Down Expand Up @@ -177,11 +181,26 @@ jobs:
echo "==> Publishing $TARBALL $COMMON_FLAGS"
npm publish "$TARBALL" $COMMON_FLAGS

- name: Tag + push
# Same shape as publish.yml: the package is already on npm by now, so the
# push reconciles onto whatever landed on the branch mid-run rather than
# failing, and the tag is created only once the commit is on the branch.
- name: Push release commit
if: ${{ github.event.inputs.dry_run != 'true' && (github.event.inputs.version != 'none' || github.event.inputs.custom_version != '') }}
env:
BRANCH: ${{ github.ref_name }}
Comment on lines +189 to +190

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Persona reconcile reverts unrelated package versions

This step runs push-release-commit.sh without setting RELEASE_RE, so it uses the default regex (scripts/push-release-commit.sh:29) matching every package's package.json and CHANGELOG.md, yet this run bumps only personas-core. On a rejected push the reconcile checks out the run-start copies of all those files over the branch tip, reverting anything that landed mid-run. A concurrent main publish that bumped every package is rolled back in git while npm keeps the new versions — the split brain this change set out to fix.

Suggested change
env:
BRANCH: ${{ github.ref_name }}
env:
BRANCH: ${{ github.ref_name }}
RELEASE_RE: '^packages/personas-core/package\.json$'
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

run: scripts/push-release-commit.sh

- name: Tag + push tags
if: ${{ github.event.inputs.dry_run != 'true' && (github.event.inputs.version != 'none' || github.event.inputs.custom_version != '') }}
run: |
git tag -a "personas-core-v${{ steps.bump.outputs.version }}" -m "${{ steps.package.outputs.npm_name }}@${{ steps.bump.outputs.version }}"
git push origin HEAD --follow-tags
set -euo pipefail
TAG="personas-core-v${{ steps.bump.outputs.version }}"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::warning::tag $TAG already exists - leaving it as is"
exit 0
fi
git tag -a "$TAG" -m "${{ steps.package.outputs.npm_name }}@${{ steps.bump.outputs.version }}"
git push origin "refs/tags/$TAG"
Comment on lines +198 to +203

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: If another run creates the same tag on origin after checkout, this step still tries to create and push it because git rev-parse only checks local tags. The push rejection then fails the workflow after publish. Check tag existence on origin and treat push rejection from an already-existing remote tag as a warning/exit 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/publish-persona.yml, line 198:

<comment>If another run creates the same tag on origin after checkout, this step still tries to create and push it because `git rev-parse` only checks local tags. The push rejection then fails the workflow after publish. Check tag existence on origin and treat push rejection from an already-existing remote tag as a warning/exit 0.</comment>

<file context>
@@ -177,11 +181,26 @@ jobs:
-          git push origin HEAD --follow-tags
+          set -euo pipefail
+          TAG="personas-core-v${{ steps.bump.outputs.version }}"
+          if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
+            echo "::warning::tag $TAG already exists - leaving it as is"
+            exit 0
</file context>
Suggested change
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "::warning::tag $TAG already exists - leaving it as is"
exit 0
fi
git tag -a "$TAG" -m "${{ steps.package.outputs.npm_name }}@${{ steps.bump.outputs.version }}"
git push origin "refs/tags/$TAG"
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::warning::tag $TAG already exists on origin - leaving it as is"
exit 0
fi
git tag -a "$TAG" -m "${{ steps.package.outputs.npm_name }}@${{ steps.bump.outputs.version }}"
if ! git push origin "refs/tags/$TAG"; then
if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::warning::tag $TAG already exists on origin - leaving it as is"
exit 0
fi
exit 1
fi


- name: Summary
run: |
Expand Down
49 changes: 44 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
# `workflow_dispatch` pins `github.sha` at dispatch time, but the
# concurrency group above makes a second dispatch *queue* — so by the
# time it runs, that SHA can be several commits stale. Building,
# bumping and changelogging from it produced 2026-08-24's split brain
# (run 32713237250 bumped from a pre-release commit and then could not
# push). Take the branch tip at run start instead.
ref: ${{ github.ref_name }}

- name: Setup pnpm
uses: pnpm/action-setup@v5
Expand Down Expand Up @@ -625,18 +632,50 @@ jobs:
fi
done

# Annotated tags (-a) so `git push --follow-tags` actually pushes them;
# lightweight tags are skipped by --follow-tags.
- name: Tag + push
# This step runs *after* the packages are already on npm, so failing
# here is the worst outcome available: the registry moves ahead of git
# and the next run bumps from a version main has never seen. A plain
# `git push` is rejected by anything that landed on the branch mid-run —
# a merged PR, or another publish run's release commit (2026-08-24).
#
# So don't fail: rebuild the release commit on the current tip and retry.
# This run's version strings are what was actually published, so they win;
# every other file comes from the newer branch tip.
- name: Push release commit
if: ${{ github.event.inputs.dry_run != 'true' && (github.event.inputs.version != 'none' || github.event.inputs.custom_version != '') }}
env:
BRANCH: ${{ github.ref_name }}
run: scripts/push-release-commit.sh

# Tags are created only once the release commit is on the branch, so a
# rejected push can never strand them on an unreachable commit — which is
# exactly what run 32713237250 did with fifteen 4.1.49 tags. Annotated
# (-a) because the create-release job checks the canonical tag out.
- name: Tag + push tags
if: ${{ github.event.inputs.dry_run != 'true' && (github.event.inputs.version != 'none' || github.event.inputs.custom_version != '') }}
run: |
set -euo pipefail
CREATED=""
for entry in ${{ steps.bump.outputs.versions }}; do
pkg="${entry%%:*}"
version="${entry##*:}"
TAG="$pkg-v$version"
NPM_NAME=$(node -p "require('./packages/$pkg/package.json').name")
git tag -a "$pkg-v$version" -m "$NPM_NAME@$version"
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
# Left behind by an earlier run that published this version but
# failed before its commit landed. Repointing a published tag is
# not this workflow's call, so leave it and say so.
echo "::warning::tag $TAG already exists - leaving it as is"
continue
fi
git tag -a "$TAG" -m "$NPM_NAME@$version"
CREATED="$CREATED refs/tags/$TAG"
done
git push origin HEAD --follow-tags
if [ -n "$CREATED" ]; then
set -f
git push origin $CREATED
set +f
fi

- name: Summary
run: |
Expand Down
90 changes: 90 additions & 0 deletions scripts/push-release-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
#
# Push the release commit at HEAD to $BRANCH, rebuilding it on the branch tip
# if the push is rejected.
#
# Publish workflows run this *after* the packages are already on npm, so a
# rejected push is the worst failure available: the registry moves ahead of git
# and the next run bumps from a version the branch has never seen. That is what
# happened on 2026-08-24 (run 32713237250), when a queued second publish run
# built from a stale dispatch SHA and lost the push race with the run ahead of
# it. Anything landing on the branch mid-run does this — a merged PR, another
# release commit — so reconcile instead of failing.
#
# HEAD must be a single release commit with a parent. Callers that create
# several commits per run need a different shape and should not use this script.
#
# Env:
# BRANCH branch to push to (default: main)
# PUSH_ATTEMPTS how many pushes to make before giving up (default: 5)
#
set -euo pipefail

BRANCH="${BRANCH:-main}"
ATTEMPTS="${PUSH_ATTEMPTS:-5}"

for attempt in $(seq 1 "$ATTEMPTS"); do
if git push origin "HEAD:refs/heads/$BRANCH"; then
echo "Pushed the release commit to $BRANCH on attempt $attempt."
exit 0
fi

# Every rebuild must get a push of its own, so stop rebuilding once the last
# attempt has been spent rather than leaving a commit that never gets tried.
if [ "$attempt" -eq "$ATTEMPTS" ]; then
break
fi

echo "::warning::push to $BRANCH was rejected (attempt $attempt) - rebuilding the release commit on the current tip"
REL=$(git rev-parse HEAD)
if ! git rev-parse -q --verify "$REL^" >/dev/null; then
echo "::error title=Release commit not pushed::HEAD has no parent, so there is no release commit to rebuild. Reconcile $BRANCH by hand." >&2
exit 1
fi
MSG=$(git log -1 --format=%B "$REL")
git fetch origin "$BRANCH"

# Exactly the files this release commit changed, taken from its own diff.
# A pattern over the tree would also pick up files the release never touched
# and revert them — another package's version bumped by whatever landed on
# the branch mid-run, say. `--diff-filter=d` drops paths the commit deleted,
# which cannot be checked out of it.
FILES=$(git diff --name-only --diff-filter=d "$REL^" "$REL")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the newer branch tip deletes a release-owned file, this retry restores it because FILES is not intersected with the tip before checkout. Intersect the release diff with paths present in origin/$BRANCH so reconciliation preserves concurrent deletions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/push-release-commit.sh, line 52:

<comment>When the newer branch tip deletes a release-owned file, this retry restores it because `FILES` is not intersected with the tip before checkout. Intersect the release diff with paths present in `origin/$BRANCH` so reconciliation preserves concurrent deletions.</comment>

<file context>
@@ -11,55 +11,63 @@
+  # and revert them — another package's version bumped by whatever landed on
+  # the branch mid-run, say. `--diff-filter=d` drops paths the commit deleted,
+  # which cannot be checked out of it.
+  FILES=$(git diff --name-only --diff-filter=d "$REL^" "$REL")
   if [ -z "$FILES" ]; then
-    echo "::error title=Release commit not pushed::None of this run's release files exist on $BRANCH. Packages are on npm; reconcile $BRANCH by hand." >&2
</file context>
Suggested change
FILES=$(git diff --name-only --diff-filter=d "$REL^" "$REL")
FILES=$(comm -12 \
<(git diff --name-only --diff-filter=d "$REL^" "$REL" | sort) \
<(git ls-tree -r --name-only "origin/$BRANCH" | sort))

if [ -z "$FILES" ]; then
echo "::error title=Release commit not pushed::The commit at HEAD adds or modifies no files. Packages may already be on npm; reconcile $BRANCH by hand." >&2
exit 1
fi

git diff --name-only --diff-filter=D "$REL^" "$REL" |
while read -r removed; do
echo "::warning::$removed was deleted by the release commit; the rebuild does not re-apply that deletion"
done

# $FILES is deliberately unquoted below so it splits into one argument per
# path. Release files are package.json / CHANGELOG.md paths with no spaces;
# noglob keeps the shell from expanding any of them as a pattern.
set -f

# This run's copy of a file it owns overwrites the tip's. That is right for a
# concurrent release commit and wrong for a hand-edited changelog, so name the
# overlap instead of losing it silently.
git diff --name-only "$REL^" "origin/$BRANCH" -- $FILES |
while read -r changed; do
echo "::warning::$changed also changed on $BRANCH during this run - this run's copy wins"
done

git reset --hard "origin/$BRANCH"
git checkout "$REL" -- $FILES
git add -- $FILES
set +f

if git diff --cached --quiet; then
echo "$BRANCH already carries this run's release files; nothing to push."
exit 0
fi
git commit -m "$MSG"
sleep $((attempt * 5))
done

echo "::error title=Release commit not pushed::Packages are on npm but $BRANCH could not be updated in $ATTEMPTS attempts. Reconcile the workspace versions on $BRANCH by hand." >&2
exit 1
Loading
Loading