Skip to content

fix(design): preserve nested pointer parity #19279

fix(design): preserve nested pointer parity

fix(design): preserve nested pointer parity #19279

name: Auto-merge Version Packages PR
# The "Version Packages" PR is opened by `changesets/action` (see
# auto-publish.yml) and contains nothing but version bumps + CHANGELOG
# updates + .changeset/*.md removals. It needs no human review and no
# heavy CI — but it sits behind the same required checks as every other
# PR (Lint & format, Test, Build, Header rules, Redirect rules, Pages
# changed), so it lingers until Build (~10 min) and the Netlify deploys
# finish. That delays publish-to-npm by the same amount.
#
# This workflow auto-merges the PR with admin bypass as soon as it's
# opened or updated. The `builder-io-integration` GitHub App
# (BUILDER_BOT_FOR_LINT_APP_ID — same identity that creates the PR) is
# in `bypass_actors` of the "Require CI checks on PRs to main" ruleset
# with `bypass_mode: always`, so `gh pr merge --admin` skips the
# in-progress required checks.
#
# The merge to main fires auto-publish.yml again; that run finds no pending
# changesets and publishes the bumped packages to npm `latest`. Its
# `[stable-release]` marker keeps the nightly snapshot job off this merge.
on:
pull_request:
branches: [main]
types: [opened, reopened, synchronize, ready_for_review]
permissions: {}
# Cancel earlier runs if the bot pushes another update to the same PR
# while we're mid-merge.
concurrency:
group: auto-merge-version-packages-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
auto-merge:
name: Force-merge changeset-release/main
# The webhook sender is the GitHub-recorded actor that caused this event;
# unlike commit author fields, it cannot be forged inside a commit object.
if: |
github.event.pull_request.head.ref == 'changeset-release/main' &&
github.event.pull_request.user.login == 'builder-io-integration[bot]' &&
github.event.sender.login == 'builder-io-integration[bot]' &&
!github.event.pull_request.draft
runs-on: ubuntu-latest
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
with:
app-id: ${{ secrets.BUILDER_BOT_FOR_LINT_APP_ID }}
private-key: ${{ secrets.BUILDER_BOT_FOR_LINT_PRIVATE_KEY }}
# `synchronize` can fire for a later push to the same branch. The job
# gate above binds this run to the actor recorded on the webhook before
# this step checks the head.
- name: Verify event-bound head
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
actual_head=$(gh api "repos/$REPO/pulls/$PR_NUMBER" --jq '.head.sha')
if [ "$actual_head" != "$HEAD_SHA" ]; then
echo "::error::PR head moved from verified event SHA $HEAD_SHA to $actual_head — refusing admin-merge." >&2
exit 1
fi
- name: Verify release-only diff
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
files=$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')
while IFS= read -r file; do
[ -z "$file" ] && continue
case "$file" in
.changeset/*.md|package.json|pnpm-lock.yaml|*/package.json|*/CHANGELOG.md|*/changelog/*)
;;
*)
echo "::error::Refusing admin-merge for unexpected release PR path: $file" >&2
exit 1
;;
esac
done <<< "$files"
# Major promotions remain human-only. Minor promotions are valid for
# intentional breaking changes in 0.x packages.
- name: Refuse unattended major promotions
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
node <<'NODE'
const { execFileSync } = require("node:child_process");
const repo = process.env.REPO;
const gh = (args) =>
execFileSync("gh", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
/** Read one package.json at a ref, or null when it does not exist there. */
function versionAt(file, ref) {
let raw;
try {
raw = gh(["api", `repos/${repo}/contents/${file}?ref=${ref}`, "--jq", ".content"]);
} catch {
return null; // added in this PR, or unreadable at this ref
}
const text = Buffer.from(raw.replace(/\s/g, ""), "base64").toString("utf8");
let parsed;
try {
parsed = JSON.parse(text);
} catch {
throw new Error(`${file} at ${ref} is not valid JSON`);
}
return typeof parsed.version === "string" ? parsed.version : null;
}
const versionParts = (version) => {
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
if (!match) throw new Error(`unparseable version "${version}"`);
return match.slice(1).map(Number);
};
const files = gh([
"api", "--paginate", `repos/${repo}/pulls/${process.env.PR_NUMBER}/files`,
"--jq", ".[].filename",
])
.split("\n")
.map((line) => line.trim())
.filter((line) => line === "package.json" || line.endsWith("/package.json"));
const promotions = [];
for (const file of files) {
const before = versionAt(file, process.env.BASE_SHA);
const after = versionAt(file, process.env.HEAD_SHA);
if (!before || !after || before === after) continue;
const beforeParts = versionParts(before);
const afterParts = versionParts(after);
const majorPromotion = afterParts[0] > beforeParts[0];
if (majorPromotion) {
promotions.push(`${file}: ${before} -> ${after}`);
}
}
if (promotions.length > 0) {
console.error(
"::error::This release PR contains a forbidden major promotion:\n" +
promotions.map((line) => ` ${line}`).join("\n") +
"\nMajor promotions remain human-only. Leaving the PR open for deliberate release handling.",
);
process.exit(1);
}
console.log(
files.length > 0
? `No forbidden promotion in ${files.length} changed package.json file(s).`
: "No package.json changes in this release PR.",
);
NODE
- name: Force-merge with admin bypass
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: gh pr merge "$PR_NUMBER" --repo "$REPO" --admin --squash --match-head-commit "$HEAD_SHA"