Skip to content

Commit acbef37

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-pluvu5
One conflict, in showcase-d3-d4-capabilities.dogfood.test.ts: #25605 loosened the same contrast to the prefix "consulted only for an external member" on the absent record. Kept this branch's side, which grants on a real showcase_project to a staff member of its organization and pins "and the recipient is not one". Main's other incoming change (#25607) is scripts/release-pending-publish.mjs only. Claude-Session: https://claude.ai/code/session_014rFBGqoBLiroi35e82xEa7 Co-authored-by: Claude <noreply@anthropic.com>
2 parents 61e0a26 + 8801f2b commit acbef37

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

‎scripts/release-pending-publish.mjs‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,21 +541,62 @@ export function withLeadLine(text, line) {
541541
return { state: 'annotated', text: `${head}${eol}${line}${eol}${body.startsWith(eol) ? '' : eol}${body}` };
542542
}
543543

544+
/**
545+
* Orders two CLI versions (`X.Y.Z` or `X.Y.Z-tag.N`): negative when `a` is
546+
* older, positive when newer, 0 when equal. A prerelease sorts before its
547+
* release; prerelease identifiers compare numerically where both are numbers.
548+
*/
549+
export function compareVersions(a, b) {
550+
const split = (v) => {
551+
const [core, pre] = v.split('-', 2);
552+
return { core: core.split('.').map(Number), pre: pre === undefined ? null : pre.split('.') };
553+
};
554+
const x = split(a);
555+
const y = split(b);
556+
for (let i = 0; i < 3; i += 1) {
557+
const d = (x.core[i] ?? 0) - (y.core[i] ?? 0);
558+
if (d !== 0) return d;
559+
}
560+
if (x.pre === null || y.pre === null) return x.pre === y.pre ? 0 : x.pre === null ? 1 : -1;
561+
for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i += 1) {
562+
if (x.pre[i] === undefined) return -1;
563+
if (y.pre[i] === undefined) return 1;
564+
const nx = /^\d+$/.test(x.pre[i]);
565+
const ny = /^\d+$/.test(y.pre[i]);
566+
if (nx && ny && Number(x.pre[i]) !== Number(y.pre[i])) return Number(x.pre[i]) - Number(y.pre[i]);
567+
if (x.pre[i] !== y.pre[i]) return nx ? -1 : ny ? 1 : x.pre[i] < y.pre[i] ? -1 : 1;
568+
}
569+
return 0;
570+
}
571+
544572
/**
545573
* The last version commit on `head`'s first-parent chain, read WITHOUT
546574
* assuming full history: `found` needs the commit and its first parent both
547575
* present, because a graft-boundary commit has no parent and would read as the
548576
* version's introduction. `need-history` = the walk reached the shallow
549577
* boundary first; `none` = this history has no version commit, with the reason.
578+
*
579+
* A commit that moves the version BACK (a reverted cut: `git revert` of an
580+
* unpublished version commit, as 18.1.0's `74fa3cb6d` was by #25605) did not
581+
* introduce the version it returns to. The walk passes over it and over the
582+
* reverted cut's span, and keeps looking for the forward commit that did.
583+
* Read as a version commit, it made every changeset the revert restored look
584+
* shipped in the version main went back to.
550585
*/
551586
export function locateLastVersionCommit({ cwd, head }) {
552587
const shallow = gitOk(cwd, ['rev-parse', '--is-shallow-repository']).trim() !== 'false';
553588
const headSha = gitOk(cwd, ['rev-parse', '--verify', `${head}^{commit}`]).trim();
554589
const version = versionAt(cwd, headSha);
555590
if (version === null) return { state: 'none', headSha, reason: `${headSha.slice(0, 10)} carries no ${CLI_MANIFEST}` };
556591
const touching = gitOk(cwd, ['log', '--first-parent', '--format=%H', headSha, '--', CLI_MANIFEST]).split('\n').filter(Boolean);
592+
let reverted = false;
557593
for (const commit of touching) {
558594
const here = versionAt(cwd, commit);
595+
if (reverted) {
596+
// Inside a reverted cut's span: commits carrying the version it undid.
597+
if (here !== version) continue;
598+
reverted = false;
599+
}
559600
if (here !== version) {
560601
return {
561602
state: 'none',
@@ -574,6 +615,10 @@ export function locateLastVersionCommit({ cwd, head }) {
574615
if (before === null) {
575616
return { state: 'none', headSha, reason: `${CLI_MANIFEST} is created at ${commit.slice(0, 10)}, and its creation is not a release` };
576617
}
618+
if (compareVersions(before, version) > 0) {
619+
reverted = true;
620+
continue;
621+
}
577622
return { state: 'found', headSha, version, from: before, versionCommit: commit };
578623
}
579624
return shallow
@@ -1256,6 +1301,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
12561301
'annotate-shipped -> a second run writes nothing': 2,
12571302
'annotate-shipped -> a changeset whose first body line already says Shipped in is left byte-identical': 2,
12581303
'annotate-shipped -> outside pre mode, where the version pass deletes what it consumes': 2,
1304+
'annotate-shipped -> a reverted cut is not a version commit: the walk reads the forward cut before it': 3,
12591305
'annotate-shipped -> a depth-1 clone deepens itself to the version commit; with no reachable history it warns, annotates nothing and exits 0': 4,
12601306
'annotate-shipped -> no version commit, an unpublished or unreadable version, an error anywhere: a warning or a plain line, nothing written, exit 0': 5,
12611307
});
@@ -1645,6 +1691,49 @@ async function selfTest() {
16451691
'with the "release" form of the line',
16461692
);
16471693

1694+
battery('annotate-shipped -> a reverted cut is not a version commit: the walk reads the forward cut before it');
1695+
// The 18.1.0 shape: a 17.7.0 cut left y.md unconsumed (above); the next cut
1696+
// (17.8.0) consumed every pending changeset, was never published, and was
1697+
// reverted, which restored them all and moved the CLI back to 17.7.0.
1698+
const rv = fixture();
1699+
const rPut = (path) => {
1700+
mkdirSync(dirname(join(rv.dir, path)), { recursive: true });
1701+
writeFileSync(join(rv.dir, path), `---\n'${CLI_PACKAGE}': minor\n---\n\n${path}\n`);
1702+
};
1703+
rv.writeCli('17.6.0');
1704+
rPut('.changeset/x.md');
1705+
rv.commit('base: one changeset pending');
1706+
rv.g('checkout', '-q', '-b', 'version-pr');
1707+
rv.g('rm', '-q', '.changeset/x.md');
1708+
rv.writeCli('17.7.0');
1709+
rv.commit('chore: version packages');
1710+
rv.g('checkout', '-q', 'main');
1711+
rPut('.changeset/y.md');
1712+
rv.commit('a landing after the cut');
1713+
rv.g('merge', '-q', '--squash', 'version-pr');
1714+
const rForward = rv.commit('chore: version packages (landed behind main)');
1715+
rPut('.changeset/z.md');
1716+
rv.commit('a landing after 17.7.0');
1717+
rv.g('rm', '-q', '.changeset/y.md', '.changeset/z.md');
1718+
rv.writeCli('17.8.0');
1719+
const rCut = rv.commit('chore: version packages (17.8.0, never published)');
1720+
rv.g('revert', '--no-edit', rCut);
1721+
const rRevert = gitOk(rv.dir, ['rev-parse', 'HEAD']).trim();
1722+
const afterRevert = annotate(rv.dir);
1723+
check(
1724+
locateLastVersionCommit({ cwd: rv.dir, head: rRevert }).versionCommit === rForward && compareVersions('17.8.0', '17.7.0') > 0 &&
1725+
compareVersions('18.0.0-next.2', '18.0.0') < 0 && compareVersions('18.0.0-next.10', '18.0.0-next.2') > 0,
1726+
'the revert back to 17.7.0 is passed over with the reverted 17.8.0 cut; the 17.7.0 cut is the version commit, and versions order numerically',
1727+
);
1728+
check(
1729+
afterRevert.verdict === 'done' && afterRevert.versionCommit === rForward && afterRevert.annotated.join() === '.changeset/y.md',
1730+
'so only y.md, which 17.7.0 really left unconsumed, is annotated',
1731+
);
1732+
check(
1733+
!readFileSync(join(rv.dir, '.changeset/z.md'), 'utf8').includes(SHIPPED_MARK),
1734+
'and z.md, which the revert restored but 17.7.0 never carried, is not stamped as shipped in 17.7.0',
1735+
);
1736+
16481737
battery('annotate-shipped -> a depth-1 clone deepens itself to the version commit; with no reachable history it warns, annotates nothing and exits 0');
16491738
q.g('checkout', '-q', '--', '.changeset');
16501739
q.g('config', 'uploadpack.allowReachableSHA1InWant', 'true');

0 commit comments

Comments
 (0)