Skip to content

Commit 2964518

Browse files
committed
feat!: align hot-path session memory with context-mode
1 parent 21b36f4 commit 2964518

20 files changed

Lines changed: 1271 additions & 271 deletions

.github/scripts/version.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
applyBump,
1212
calculateVersion,
1313
findReleaseAs,
14+
hasBreakingChangeBody,
1415
hasNonTestChanges,
1516
parseSemver,
1617
} from "./version.ts";
@@ -45,6 +46,7 @@ describe("analyzeCommits", () => {
4546

4647
it("returns 'major' for breaking change with ! suffix", () => {
4748
assertEquals(analyzeCommits(["feat!: breaking change"]), "major");
49+
assertEquals(analyzeCommits(["fix!: breaking fix"]), "major");
4850
});
4951

5052
it("returns 'major' for breaking change with BREAKING CHANGE in subject", () => {
@@ -61,6 +63,15 @@ describe("analyzeCommits", () => {
6163
);
6264
});
6365

66+
it("returns 'major' when a commit body contains BREAKING CHANGE", () => {
67+
assertEquals(
68+
analyzeCommits(["feat: keep subject normal"], [
69+
"BREAKING CHANGE: api changed",
70+
]),
71+
"major",
72+
);
73+
});
74+
6475
it("returns highest bump when mixed commits (feat + fix → minor)", () => {
6576
const subjects = [
6677
"fix: bug fix",
@@ -116,6 +127,24 @@ describe("analyzeCommits", () => {
116127
});
117128
});
118129

130+
describe("hasBreakingChangeBody", () => {
131+
it("returns true for semantic-release style breaking change bodies", () => {
132+
assertEquals(
133+
hasBreakingChangeBody([
134+
"Some text\n\nBREAKING CHANGE: changed output format",
135+
]),
136+
true,
137+
);
138+
});
139+
140+
it("returns false when commit bodies do not include the breaking footer", () => {
141+
assertEquals(
142+
hasBreakingChangeBody(["Regular body", "Another body"]),
143+
false,
144+
);
145+
});
146+
});
147+
119148
describe("findReleaseAs", () => {
120149
it("returns undefined for empty array", () => {
121150
assertEquals(findReleaseAs([]), undefined);
@@ -349,6 +378,16 @@ describe("calculateVersion", () => {
349378
assertEquals(result, { skip: false, version: "2.0.0", tag: "latest" });
350379
});
351380

381+
it("creates release version for BREAKING CHANGE in commit body", () => {
382+
const result = calculateVersion({
383+
...baseOpts,
384+
subjects: ["feat: keep subject stable"],
385+
bodies: ["BREAKING CHANGE: api changed"],
386+
eventName: "push",
387+
});
388+
assertEquals(result, { skip: false, version: "2.0.0", tag: "latest" });
389+
});
390+
352391
it("skips when no triggering commits", () => {
353392
const result = calculateVersion({
354393
...baseOpts,
@@ -468,6 +507,20 @@ describe("calculateVersion", () => {
468507
});
469508
});
470509

510+
it("creates canary for BREAKING CHANGE in commit body", () => {
511+
const result = calculateVersion({
512+
...baseOpts,
513+
subjects: ["fix: preserve subject format"],
514+
bodies: ["BREAKING CHANGE: cache schema changed"],
515+
eventName: "pull_request",
516+
});
517+
assertEquals(result, {
518+
skip: false,
519+
version: "2.0.0-canary.abc123d.20260212091429",
520+
tag: "canary",
521+
});
522+
});
523+
471524
it("shortens commit SHA to 7 characters", () => {
472525
const result = calculateVersion({
473526
...baseOpts,
@@ -618,5 +671,20 @@ describe("calculateVersion", () => {
618671
tag: "canary",
619672
});
620673
});
674+
675+
it("creates a 0.x canary minor bump from BREAKING CHANGE in body", () => {
676+
const result = calculateVersion({
677+
...baseOpts,
678+
currentVersion: "0.1.12",
679+
subjects: ["feat: keep subject stable"],
680+
bodies: ["BREAKING CHANGE: overhaul session-memory semantics"],
681+
eventName: "pull_request",
682+
});
683+
assertEquals(result, {
684+
skip: false,
685+
version: "0.2.0-canary.abc123d.20260212091429",
686+
tag: "canary",
687+
});
688+
});
621689
});
622690
});

.github/scripts/version.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,31 @@ export type VersionResult =
1919
| { skip: false; version: string; tag: "latest" | "canary" };
2020

2121
/**
22-
* Analyze conventional commit subjects and return the highest bump type.
22+
* Returns true when any commit body contains a semantic-release style breaking
23+
* change footer/header such as `BREAKING CHANGE: details`.
24+
*/
25+
export function hasBreakingChangeBody(bodies: string[]): boolean {
26+
return bodies.some((body) => /^BREAKING CHANGE:/im.test(body));
27+
}
28+
29+
/**
30+
* Analyze conventional commits and return the highest bump type.
31+
*
32+
* Supported formats:
33+
* - `feat: add feature` -> minor
34+
* - `fix: resolve bug` / `perf: speed up path` -> patch
35+
* - `feat!: breaking api change` / `fix!: breaking bugfix` -> major
36+
* - `BREAKING CHANGE: explanation` in a commit body -> major
37+
* - `Release-As: x.y.z` is handled separately as an exact override
2338
*
24-
* Rules:
25-
* - `BREAKING CHANGE` in body or `type!:` → major
26-
* - `feat:` → minor
27-
* - `fix:` / `perf:` → patch
28-
* - Anything else → none
39+
* In `0.x`, a major bump resolves to the next minor version.
2940
*/
30-
export function analyzeCommits(subjects: string[]): Bump {
41+
export function analyzeCommits(
42+
subjects: string[],
43+
bodies: string[] = [],
44+
): Bump {
45+
if (hasBreakingChangeBody(bodies)) return "major";
46+
3147
let bump: Bump = "none";
3248

3349
for (const msg of subjects) {
@@ -121,7 +137,7 @@ export function calculateVersion(opts: {
121137
currentVersion: string;
122138
/** Conventional commit subjects since last release. */
123139
subjects: string[];
124-
/** Commit bodies (for Release-As detection). */
140+
/** Commit bodies (for Release-As and BREAKING CHANGE detection). */
125141
bodies: string[];
126142
/** Whether this is a "push" (release) or "pull_request" (canary). */
127143
eventName: "push" | "pull_request";
@@ -151,8 +167,8 @@ export function calculateVersion(opts: {
151167
return { skip: false, version, tag } as const;
152168
}
153169

154-
// Analyze commits
155-
let bump = analyzeCommits(opts.subjects);
170+
// Analyze commits using subjects plus semantic-release style body footers.
171+
let bump = analyzeCommits(opts.subjects, opts.bodies);
156172

157173
// When no git tags, default to patch bump from npm baseline
158174
if (opts.noGitTags && bump === "none") {

0 commit comments

Comments
 (0)