From a2c0866a84dbb129aaf50463c608eda380e5db9a Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 09:36:44 -0400 Subject: [PATCH 1/6] Name the catalog PR when the test subset pin is not on catalog main Co-Authored-By: Claude Opus 5.5 --- packages/catalog/scripts/sync-test-subset.ts | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/catalog/scripts/sync-test-subset.ts b/packages/catalog/scripts/sync-test-subset.ts index fc1995dac6c..b168a59b037 100644 --- a/packages/catalog/scripts/sync-test-subset.ts +++ b/packages/catalog/scripts/sync-test-subset.ts @@ -535,6 +535,26 @@ function bump(manifest: Manifest) { return { ...manifest, revision: sha }; } +// For a commit main does not contain, GitHub lists only the open pull requests +// that carry it. The lookup only enriches a failure message, so an error +// answers no pull requests rather than masking that failure. +async function pullRequestsFor( + manifest: Manifest, + headers: Record, +): Promise { + let url = `https://api.github.com/repos/${manifest.repository}/commits/${manifest.revision}/pulls`; + try { + let response = await fetch(url, { headers }); + if (!response.ok) { + return []; + } + let prs = (await response.json()) as { html_url: string }[]; + return prs.map((pr) => pr.html_url); + } catch { + return []; + } +} + // The deployed catalog realm serves boxel-catalog's main, so a pin that main // does not contain describes definitions no deployment has. A pin that main // contains can still be behind it: once main changes a subset file, boxel's @@ -554,8 +574,10 @@ async function checkPin(manifest: Manifest) { } let { status } = (await response.json()) as { status: string }; if (status !== 'behind' && status !== 'identical') { + let prs = await pullRequestsFor(manifest, headers); fail( `${manifest.revision} is not on ${manifest.repository} main (compare status "${status}"). ` + + (prs.length ? `It is in ${prs.join(', ')}. ` : '') + `Merge the catalog change first, then re-pin to a commit on main (pnpm catalog:test-subset --bump).`, ); } From fbb9f6a8f812f21a83e3cf5cea6377fa9446e669 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 09:47:17 -0400 Subject: [PATCH 2/6] Tell a squash-merged catalog PR apart from an open one in the pin check Co-Authored-By: Claude Opus 5.5 --- packages/catalog/scripts/sync-test-subset.ts | 29 ++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/catalog/scripts/sync-test-subset.ts b/packages/catalog/scripts/sync-test-subset.ts index b168a59b037..5255083d6d2 100644 --- a/packages/catalog/scripts/sync-test-subset.ts +++ b/packages/catalog/scripts/sync-test-subset.ts @@ -535,21 +535,28 @@ function bump(manifest: Manifest) { return { ...manifest, revision: sha }; } -// For a commit main does not contain, GitHub lists only the open pull requests -// that carry it. The lookup only enriches a failure message, so an error -// answers no pull requests rather than masking that failure. +interface PullRequest { + html_url: string; + merged_at: string | null; + merge_commit_sha: string | null; +} + +// For a commit main does not contain, GitHub lists the open pull requests that +// carry it, and the merged one it came from when that pull request was +// squash-merged (a squash lands a new commit on main, never the head itself). +// The lookup only enriches a failure message, so an error answers no pull +// requests rather than masking that failure. async function pullRequestsFor( manifest: Manifest, headers: Record, -): Promise { +): Promise { let url = `https://api.github.com/repos/${manifest.repository}/commits/${manifest.revision}/pulls`; try { let response = await fetch(url, { headers }); if (!response.ok) { return []; } - let prs = (await response.json()) as { html_url: string }[]; - return prs.map((pr) => pr.html_url); + return (await response.json()) as PullRequest[]; } catch { return []; } @@ -575,10 +582,16 @@ async function checkPin(manifest: Manifest) { let { status } = (await response.json()) as { status: string }; if (status !== 'behind' && status !== 'identical') { let prs = await pullRequestsFor(manifest, headers); + let merged = prs.find((pr) => pr.merged_at); fail( `${manifest.revision} is not on ${manifest.repository} main (compare status "${status}"). ` + - (prs.length ? `It is in ${prs.join(', ')}. ` : '') + - `Merge the catalog change first, then re-pin to a commit on main (pnpm catalog:test-subset --bump).`, + (merged + ? `It is from ${merged.html_url}, which was merged as ${merged.merge_commit_sha}. ` + + `Re-pin to a commit on main (pnpm catalog:test-subset --bump).` + : (prs.length + ? `It is in ${prs.map((pr) => pr.html_url).join(', ')}. ` + : '') + + `Merge the catalog change first, then re-pin to a commit on main (pnpm catalog:test-subset --bump).`), ); } log(`${manifest.revision} is on ${manifest.repository} main`); From 660047d75f3d83438ec2b0d95637e6778fac51ab Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 10:29:40 -0400 Subject: [PATCH 3/6] Name the catalog PRs that changed a subset file since the pin Co-Authored-By: Claude Opus 5.5 --- packages/catalog/scripts/sync-test-subset.ts | 80 ++++++++++++++++---- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/catalog/scripts/sync-test-subset.ts b/packages/catalog/scripts/sync-test-subset.ts index 5255083d6d2..a84193edf50 100644 --- a/packages/catalog/scripts/sync-test-subset.ts +++ b/packages/catalog/scripts/sync-test-subset.ts @@ -541,25 +541,72 @@ interface PullRequest { merge_commit_sha: string | null; } +// The pull request lookups only enrich a failure message, so an error answers +// nothing rather than masking the failure they describe. +async function lookup( + url: string, + headers: Record, +): Promise { + try { + let response = await fetch(url, { headers }); + return response.ok ? ((await response.json()) as T) : undefined; + } catch { + return undefined; + } +} + // For a commit main does not contain, GitHub lists the open pull requests that // carry it, and the merged one it came from when that pull request was // squash-merged (a squash lands a new commit on main, never the head itself). -// The lookup only enriches a failure message, so an error answers no pull -// requests rather than masking that failure. +// For a commit on main, it lists the pull request that merged it. async function pullRequestsFor( - manifest: Manifest, + repository: string, + sha: string, headers: Record, ): Promise { - let url = `https://api.github.com/repos/${manifest.repository}/commits/${manifest.revision}/pulls`; - try { - let response = await fetch(url, { headers }); - if (!response.ok) { - return []; - } - return (await response.json()) as PullRequest[]; - } catch { + return ( + (await lookup( + `https://api.github.com/repos/${repository}/commits/${sha}/pulls`, + headers, + )) ?? [] + ); +} + +// The pull requests that changed `paths` on main since the pin: each path's +// history on main, narrowed to the commits main has and the pin lacks. +async function pullRequestsChanging( + manifest: Manifest, + paths: string[], + headers: Record, +): Promise { + let api = `https://api.github.com/repos/${manifest.repository}`; + let compare = await lookup<{ commits: { sha: string }[] }>( + `${api}/compare/${manifest.revision}...main`, + headers, + ); + if (!compare) { return []; } + let sincePin = new Set(compare.commits.map((c) => c.sha)); + let shas = new Set(); + for (let path of paths) { + let history = await lookup<{ sha: string }[]>( + `${api}/commits?sha=main&path=${encodeURIComponent(path)}&per_page=100`, + headers, + ); + for (let { sha } of history ?? []) { + if (sincePin.has(sha)) { + shas.add(sha); + } + } + } + let urls = new Set(); + for (let sha of shas) { + for (let pr of await pullRequestsFor(manifest.repository, sha, headers)) { + urls.add(pr.html_url); + } + } + return [...urls]; } // The deployed catalog realm serves boxel-catalog's main, so a pin that main @@ -581,7 +628,11 @@ async function checkPin(manifest: Manifest) { } let { status } = (await response.json()) as { status: string }; if (status !== 'behind' && status !== 'identical') { - let prs = await pullRequestsFor(manifest, headers); + let prs = await pullRequestsFor( + manifest.repository, + manifest.revision, + headers, + ); let merged = prs.find((pr) => pr.merged_at); fail( `${manifest.revision} is not on ${manifest.repository} main (compare status "${status}"). ` + @@ -618,8 +669,11 @@ async function checkPin(manifest: Manifest) { } } if (changed.length) { + let prs = await pullRequestsChanging(manifest, changed, headers); fail( - `${manifest.repository} main has changed ${changed.join(', ')} since ${manifest.revision}, so boxel's tests would run against definitions deployments no longer serve. ` + + `${manifest.repository} main has changed ${changed.join(', ')} since ${manifest.revision}` + + (prs.length ? ` (in ${prs.join(', ')})` : '') + + `, so boxel's tests would run against definitions deployments no longer serve. ` + `Re-pin to main (pnpm catalog:test-subset --bump), run the manifest's tests against it, and commit the new pin.`, ); } From 5cadcfae6a0b43dbc48a832c3f3f93e156b0e9e8 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 10:37:48 -0400 Subject: [PATCH 4/6] Name the paired boxel PR that re-pins when catalog main has moved past the pin Co-Authored-By: Claude Opus 5.5 --- packages/catalog/scripts/sync-test-subset.ts | 73 ++++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/catalog/scripts/sync-test-subset.ts b/packages/catalog/scripts/sync-test-subset.ts index a84193edf50..65cc0021f85 100644 --- a/packages/catalog/scripts/sync-test-subset.ts +++ b/packages/catalog/scripts/sync-test-subset.ts @@ -536,11 +536,15 @@ function bump(manifest: Manifest) { } interface PullRequest { + number: number; html_url: string; merged_at: string | null; merge_commit_sha: string | null; + head: { ref: string; sha: string }; } +const boxelRepository = 'cardstack/boxel'; + // The pull request lookups only enrich a failure message, so an error answers // nothing rather than masking the failure they describe. async function lookup( @@ -578,7 +582,7 @@ async function pullRequestsChanging( manifest: Manifest, paths: string[], headers: Record, -): Promise { +): Promise { let api = `https://api.github.com/repos/${manifest.repository}`; let compare = await lookup<{ commits: { sha: string }[] }>( `${api}/compare/${manifest.revision}...main`, @@ -600,15 +604,69 @@ async function pullRequestsChanging( } } } - let urls = new Set(); + let prs = new Map(); for (let sha of shas) { for (let pr of await pullRequestsFor(manifest.repository, sha, headers)) { - urls.add(pr.html_url); + prs.set(pr.html_url, pr); + } + } + return [...prs.values()]; +} + +// A catalog change and the boxel change that depends on it are made on +// branches of the same name, which is how boxel-catalog's Boxel Test Subset +// workflow pairs them. The open boxel pull requests on a catalog pull +// request's branch whose pin differs from this one carry that boxel side, +// re-pin included. The branch under check is left out, since the message is +// addressed to it. +async function pairedBoxelPullRequests( + manifest: Manifest, + catalogPrs: PullRequest[], + headers: Record, +): Promise { + let api = `https://api.github.com/repos/${boxelRepository}`; + let owner = boxelRepository.split('/')[0]; + let current = process.env.GITHUB_HEAD_REF || currentBranch(); + let manifestFile = relative(repoRoot, manifestPath); + let urls = new Set(); + for (let ref of new Set(catalogPrs.map((pr) => pr.head.ref))) { + if (ref === current) { + continue; + } + let prs = await lookup( + `${api}/pulls?state=open&head=${owner}:${encodeURIComponent(ref)}`, + headers, + ); + for (let pr of prs ?? []) { + let file = await lookup<{ content: string }>( + `${api}/contents/${manifestFile}?ref=${pr.head.sha}`, + headers, + ); + let revision = file + ? ( + JSON.parse( + Buffer.from(file.content, 'base64').toString('utf8'), + ) as Manifest + ).revision + : undefined; + if (revision && revision !== manifest.revision) { + urls.add(pr.html_url); + } } } return [...urls]; } +function currentBranch() { + try { + return execFileSync('git', ['-C', repoRoot, 'branch', '--show-current'], { + encoding: 'utf8', + }).trim(); + } catch { + return undefined; + } +} + // The deployed catalog realm serves boxel-catalog's main, so a pin that main // does not contain describes definitions no deployment has. A pin that main // contains can still be behind it: once main changes a subset file, boxel's @@ -670,11 +728,16 @@ async function checkPin(manifest: Manifest) { } if (changed.length) { let prs = await pullRequestsChanging(manifest, changed, headers); + let paired = await pairedBoxelPullRequests(manifest, prs, headers); fail( `${manifest.repository} main has changed ${changed.join(', ')} since ${manifest.revision}` + - (prs.length ? ` (in ${prs.join(', ')})` : '') + + (prs.length ? ` (in ${prs.map((pr) => pr.html_url).join(', ')})` : '') + `, so boxel's tests would run against definitions deployments no longer serve. ` + - `Re-pin to main (pnpm catalog:test-subset --bump), run the manifest's tests against it, and commit the new pin.`, + (paired.length + ? `The boxel side of that change, which re-pins, is in ${paired.join(', ')}: once that merges, merge main into this branch. ` + + `To move the pin without it, re-pin` + : `Re-pin`) + + ` to main (pnpm catalog:test-subset --bump), run the manifest's tests against it, and commit the new pin.`, ); } log( From b3c89b01ca91f38da9e693c0021b3e2dcf2fee06 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 10:44:05 -0400 Subject: [PATCH 5/6] Find the boxel pin that clears the failure by reading each open PR's manifest Co-Authored-By: Claude Opus 5.5 --- packages/catalog/scripts/sync-test-subset.ts | 199 ++++++++++++++----- 1 file changed, 146 insertions(+), 53 deletions(-) diff --git a/packages/catalog/scripts/sync-test-subset.ts b/packages/catalog/scripts/sync-test-subset.ts index 65cc0021f85..4c957615ca3 100644 --- a/packages/catalog/scripts/sync-test-subset.ts +++ b/packages/catalog/scripts/sync-test-subset.ts @@ -536,11 +536,9 @@ function bump(manifest: Manifest) { } interface PullRequest { - number: number; html_url: string; merged_at: string | null; merge_commit_sha: string | null; - head: { ref: string; sha: string }; } const boxelRepository = 'cardstack/boxel'; @@ -550,9 +548,10 @@ const boxelRepository = 'cardstack/boxel'; async function lookup( url: string, headers: Record, + init?: RequestInit, ): Promise { try { - let response = await fetch(url, { headers }); + let response = await fetch(url, { ...init, headers }); return response.ok ? ((await response.json()) as T) : undefined; } catch { return undefined; @@ -582,7 +581,7 @@ async function pullRequestsChanging( manifest: Manifest, paths: string[], headers: Record, -): Promise { +): Promise { let api = `https://api.github.com/repos/${manifest.repository}`; let compare = await lookup<{ commits: { sha: string }[] }>( `${api}/compare/${manifest.revision}...main`, @@ -604,57 +603,146 @@ async function pullRequestsChanging( } } } - let prs = new Map(); + let urls = new Set(); for (let sha of shas) { for (let pr of await pullRequestsFor(manifest.repository, sha, headers)) { - prs.set(pr.html_url, pr); + urls.add(pr.html_url); } } - return [...prs.values()]; + return [...urls]; } -// A catalog change and the boxel change that depends on it are made on -// branches of the same name, which is how boxel-catalog's Boxel Test Subset -// workflow pairs them. The open boxel pull requests on a catalog pull -// request's branch whose pin differs from this one carry that boxel side, -// re-pin included. The branch under check is left out, since the message is -// addressed to it. -async function pairedBoxelPullRequests( - manifest: Manifest, - catalogPrs: PullRequest[], - headers: Record, -): Promise { - let api = `https://api.github.com/repos/${boxelRepository}`; - let owner = boxelRepository.split('/')[0]; - let current = process.env.GITHUB_HEAD_REF || currentBranch(); - let manifestFile = relative(repoRoot, manifestPath); - let urls = new Set(); - for (let ref of new Set(catalogPrs.map((pr) => pr.head.ref))) { - if (ref === current) { - continue; +const boxelPinsQuery = ` + query ($owner: String!, $name: String!, $path: String!, $mainPath: String!, $after: String) { + repository(owner: $owner, name: $name) { + main: object(expression: $mainPath) { ... on Blob { text } } + pullRequests(states: OPEN, first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { + url + headRefName + commits(last: 1) { + nodes { commit { file(path: $path) { object { ... on Blob { text } } } } } + } + } + } + } + } +`; + +interface BoxelPinsAnswer { + data?: { + repository: { + main: { text?: string } | null; + pullRequests: { + pageInfo: { hasNextPage: boolean; endCursor: string | null }; + nodes: { + url: string; + headRefName: string; + commits: { + nodes: { + commit: { file: { object: { text?: string } | null } | null }; + }[]; + }; + }[]; + }; + }; + }; +} + +// The catalog revision boxel main pins, and the one each open boxel pull +// request pins at its head, read from their manifests with a GraphQL query +// per hundred pull requests. A pull request whose branch has no manifest, or +// one that doesn't parse, is left out. GraphQL takes no anonymous requests, so +// without GITHUB_TOKEN this answers nothing. +async function boxelPins(headers: Record) { + let [owner, name] = boxelRepository.split('/'); + let path = relative(repoRoot, manifestPath); + let revisionIn = (text: string | undefined) => { + try { + return text ? (JSON.parse(text) as Manifest).revision : undefined; + } catch { + return undefined; } - let prs = await lookup( - `${api}/pulls?state=open&head=${owner}:${encodeURIComponent(ref)}`, + }; + let main: string | undefined; + let pullRequests: { url: string; branch: string; revision: string }[] = []; + let after: string | null = null; + do { + let answer: BoxelPinsAnswer | undefined = await lookup( + 'https://api.github.com/graphql', headers, + { + method: 'POST', + body: JSON.stringify({ + query: boxelPinsQuery, + variables: { owner, name, path, mainPath: `main:${path}`, after }, + }), + }, ); - for (let pr of prs ?? []) { - let file = await lookup<{ content: string }>( - `${api}/contents/${manifestFile}?ref=${pr.head.sha}`, - headers, - ); - let revision = file - ? ( - JSON.parse( - Buffer.from(file.content, 'base64').toString('utf8'), - ) as Manifest - ).revision - : undefined; - if (revision && revision !== manifest.revision) { - urls.add(pr.html_url); + if (!answer?.data) { + break; + } + let { repository } = answer.data; + main ??= revisionIn(repository.main?.text); + for (let pr of repository.pullRequests.nodes) { + let revision = revisionIn(pr.commits.nodes[0]?.commit.file?.object?.text); + if (revision) { + pullRequests.push({ url: pr.url, branch: pr.headRefName, revision }); } } + let { hasNextPage, endCursor } = repository.pullRequests.pageInfo; + after = hasNextPage ? endCursor : null; + } while (after); + return { main, pullRequests }; +} + +// Where a pin that clears a changed-subset-file failure is already waiting: +// boxel main, or open boxel pull requests other than the branch under check, +// whose pin has every subset file as catalog main has it. That is the check's +// own condition, so merging one of them is what makes this branch pass. +async function pinsMatchingMain( + manifest: Manifest, + mainBlobs: Map, + headers: Record, +) { + let pins = await boxelPins(headers); + let matches = new Map>(); + let matchesMain = (revision: string) => { + if (!matches.has(revision)) { + matches.set( + revision, + Promise.all( + [...mainBlobs].map(async ([path, sha]) => { + let blob = await lookup<{ sha: string }>( + `https://api.github.com/repos/${manifest.repository}/contents/${path}?ref=${revision}`, + headers, + ); + return blob?.sha === sha; + }), + ).then((each) => each.every(Boolean)), + ); + } + return matches.get(revision)!; + }; + if ( + pins.main && + pins.main !== manifest.revision && + (await matchesMain(pins.main)) + ) { + return { main: true, pullRequests: [] }; } - return [...urls]; + let current = process.env.GITHUB_HEAD_REF || currentBranch(); + let candidates = pins.pullRequests.filter( + (pr) => pr.branch !== current && pr.revision !== manifest.revision, + ); + let matching = await Promise.all( + candidates.map((pr) => matchesMain(pr.revision)), + ); + return { + main: false, + pullRequests: candidates.filter((_, i) => matching[i]).map((pr) => pr.url), + }; } function currentBranch() { @@ -718,26 +806,31 @@ async function checkPin(manifest: Manifest) { } return ((await response.json()) as { sha: string }).sha; }; + let mainBlobs = new Map(); let changed: string[] = []; for (let { path } of manifest.files) { - if ( - (await blobSha(path, 'main')) !== (await blobSha(path, manifest.revision)) - ) { + let sha = await blobSha(path, 'main'); + mainBlobs.set(path, sha); + if (sha !== (await blobSha(path, manifest.revision))) { changed.push(path); } } if (changed.length) { let prs = await pullRequestsChanging(manifest, changed, headers); - let paired = await pairedBoxelPullRequests(manifest, prs, headers); + let waiting = await pinsMatchingMain(manifest, mainBlobs, headers); + let steps = `to main (pnpm catalog:test-subset --bump), run the manifest's tests against it, and commit the new pin.`; + let [one, ...more] = waiting.pullRequests; fail( `${manifest.repository} main has changed ${changed.join(', ')} since ${manifest.revision}` + - (prs.length ? ` (in ${prs.map((pr) => pr.html_url).join(', ')})` : '') + + (prs.length ? ` (in ${prs.join(', ')})` : '') + `, so boxel's tests would run against definitions deployments no longer serve. ` + - (paired.length - ? `The boxel side of that change, which re-pins, is in ${paired.join(', ')}: once that merges, merge main into this branch. ` + - `To move the pin without it, re-pin` - : `Re-pin`) + - ` to main (pnpm catalog:test-subset --bump), run the manifest's tests against it, and commit the new pin.`, + (waiting.main + ? `Boxel main already pins a revision that matches ${manifest.repository} main: merge main into this branch.` + : one + ? more.length + ? `${waiting.pullRequests.join(', ')} already pin a revision that matches ${manifest.repository} main: once one merges, merge main into this branch. To move the pin without them, re-pin ${steps}` + : `${one} already pins a revision that matches ${manifest.repository} main: once it merges, merge main into this branch. To move the pin without it, re-pin ${steps}` + : `Re-pin ${steps}`), ); } log( From f9505c6e449f4b12b579795b631c5fa284d6945c Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 10:45:48 -0400 Subject: [PATCH 6/6] Name the repo of every main the pin check's messages mention Co-Authored-By: Claude Opus 5.5 --- packages/catalog/scripts/sync-test-subset.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/catalog/scripts/sync-test-subset.ts b/packages/catalog/scripts/sync-test-subset.ts index 4c957615ca3..2af1b082ce5 100644 --- a/packages/catalog/scripts/sync-test-subset.ts +++ b/packages/catalog/scripts/sync-test-subset.ts @@ -527,7 +527,7 @@ function bump(manifest: Manifest) { ); let sha = output.split(/\s/)[0]; if (!/^[0-9a-f]{40}$/.test(sha)) { - fail(`could not resolve main of ${manifest.repository}`); + fail(`could not resolve ${manifest.repository} main`); } let raw = readFileSync(manifestPath, 'utf8'); writeFileSync(manifestPath, raw.replace(manifest.revision, sha)); @@ -784,11 +784,11 @@ async function checkPin(manifest: Manifest) { `${manifest.revision} is not on ${manifest.repository} main (compare status "${status}"). ` + (merged ? `It is from ${merged.html_url}, which was merged as ${merged.merge_commit_sha}. ` + - `Re-pin to a commit on main (pnpm catalog:test-subset --bump).` + `Re-pin to a commit on ${manifest.repository} main (pnpm catalog:test-subset --bump).` : (prs.length ? `It is in ${prs.map((pr) => pr.html_url).join(', ')}. ` : '') + - `Merge the catalog change first, then re-pin to a commit on main (pnpm catalog:test-subset --bump).`), + `Merge the ${manifest.repository} change first, then re-pin to a commit on ${manifest.repository} main (pnpm catalog:test-subset --bump).`), ); } log(`${manifest.revision} is on ${manifest.repository} main`); @@ -818,18 +818,18 @@ async function checkPin(manifest: Manifest) { if (changed.length) { let prs = await pullRequestsChanging(manifest, changed, headers); let waiting = await pinsMatchingMain(manifest, mainBlobs, headers); - let steps = `to main (pnpm catalog:test-subset --bump), run the manifest's tests against it, and commit the new pin.`; + let steps = `to ${manifest.repository} main (pnpm catalog:test-subset --bump), run the manifest's tests against it, and commit the new pin.`; let [one, ...more] = waiting.pullRequests; fail( `${manifest.repository} main has changed ${changed.join(', ')} since ${manifest.revision}` + (prs.length ? ` (in ${prs.join(', ')})` : '') + `, so boxel's tests would run against definitions deployments no longer serve. ` + (waiting.main - ? `Boxel main already pins a revision that matches ${manifest.repository} main: merge main into this branch.` + ? `${boxelRepository} main already pins a revision that matches ${manifest.repository} main: merge ${boxelRepository} main into this branch.` : one ? more.length - ? `${waiting.pullRequests.join(', ')} already pin a revision that matches ${manifest.repository} main: once one merges, merge main into this branch. To move the pin without them, re-pin ${steps}` - : `${one} already pins a revision that matches ${manifest.repository} main: once it merges, merge main into this branch. To move the pin without it, re-pin ${steps}` + ? `${waiting.pullRequests.join(', ')} already pin a revision that matches ${manifest.repository} main: once one merges, merge ${boxelRepository} main into this branch. To move the pin without them, re-pin ${steps}` + : `${one} already pins a revision that matches ${manifest.repository} main: once it merges, merge ${boxelRepository} main into this branch. To move the pin without it, re-pin ${steps}` : `Re-pin ${steps}`), ); }