Skip to content

Commit 2b81152

Browse files
fix: support consumed release changesets (#4)
* docs: design release lifecycle regression fix * fix: support consumed release changesets
1 parent 99c7b2c commit 2b81152

4 files changed

Lines changed: 141 additions & 5 deletions

File tree

‎.changeset/tender-symbols-beg.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
3+
---
4+
5+
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Release PR Lifecycle Test Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Make release configuration tests pass both before and after Changesets consumes the initial release file.
6+
7+
**Architecture:** Extract the lifecycle assertion into a small helper that reads a supplied package and changeset directory. Test the helper against temporary pre-release and versioned fixtures, then use it for the repository-level assertion.
8+
9+
**Tech Stack:** TypeScript, Node.js test runner, pnpm, Changesets
10+
11+
## Global Constraints
12+
13+
- Preserve the permanent Changesets configuration assertions.
14+
- Require the seed changeset at version `0.0.0`.
15+
- Require the seed changeset to be absent after versioning.
16+
- Do not modify `README.md` or runtime package behavior.
17+
18+
---
19+
20+
### Task 1: Make the release test lifecycle-aware
21+
22+
**Files:**
23+
- Modify: `test/release-config.test.ts`
24+
- Create: `.changeset/<generated-empty-changeset>.md`
25+
26+
**Interfaces:**
27+
- Consumes: `package.json` version and `.changeset/initial-public-release.md`
28+
- Produces: `assertInitialReleaseLifecycle(root: string): void`
29+
30+
- [ ] **Step 1: Add failing fixture coverage**
31+
32+
Add temporary fixtures proving that version `0.0.0` requires a matching seed,
33+
and a versioned package requires the seed to be absent.
34+
35+
- [ ] **Step 2: Run the focused test and verify failure**
36+
37+
Run: `node --test test/release-config.test.ts`
38+
39+
Expected: the versioned fixture fails because the current assertion reads the
40+
seed unconditionally.
41+
42+
- [ ] **Step 3: Implement the lifecycle assertion**
43+
44+
Read `package.json` relative to the supplied root. At version `0.0.0`, assert
45+
that the seed exists and matches `INITIAL_RELEASE`; otherwise assert that the
46+
seed does not exist. Call the helper from the repository configuration test.
47+
48+
- [ ] **Step 4: Verify focused and full checks**
49+
50+
Run: `node --test test/release-config.test.ts`
51+
52+
Expected: PASS.
53+
54+
Run: `pnpm check`
55+
56+
Expected: all lint, type, contract, example, and unit checks pass.
57+
58+
- [ ] **Step 5: Record the CI-only fix and open a pull request**
59+
60+
Create an empty Changeset decision, commit with Conventional Commits, push the
61+
branch, and open a pull request against `main`. Confirm required checks pass.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Release PR Lifecycle Test Design
2+
3+
## Problem
4+
5+
The generated Changesets release pull request consumes
6+
`.changeset/initial-public-release.md`. The release configuration test reads
7+
that transient file unconditionally, so every valid release pull request fails
8+
CI after versioning.
9+
10+
## Design
11+
12+
Keep the permanent Changesets configuration assertions unconditional. Make the
13+
initial-release assertion depend on the package lifecycle:
14+
15+
- While `package.json` is version `0.0.0`, the seed changeset must exist and
16+
request a minor release for `@brkn-labs/judge`.
17+
- After the package version changes from `0.0.0`, the seed changeset must no
18+
longer exist because Changesets has consumed it.
19+
20+
This preserves the original bootstrap guard without requiring an ephemeral
21+
file forever. No runtime package behavior, release workflow, or README content
22+
changes.
23+
24+
## Verification
25+
26+
Exercise both lifecycle states in isolated temporary fixtures, then run the
27+
repository's focused release-config test and complete `pnpm check` gate.

‎test/release-config.test.ts‎

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@ import {
88
readFileSync,
99
rmSync,
1010
statSync,
11+
writeFileSync,
1112
} from "node:fs";
1213
import { tmpdir } from "node:os";
1314
import { join } from "node:path";
1415
import test from "node:test";
1516

1617
const COMMITLINT_CONFIG = /@commitlint\/config-conventional/u;
1718
const INITIAL_RELEASE = /"@brkn-labs\/judge": minor/u;
19+
const INITIAL_RELEASE_FILE = ".changeset/initial-public-release.md";
1820
const INVALID_COMMIT_MESSAGE = /type may not be empty/u;
1921
const MIT_COPYRIGHT = /Copyright \(c\) 2026 Dhruv Tiwari/u;
2022
const MIT_GRANT =
2123
/Permission is hereby granted, free of charge, to any person obtaining a copy/u;
24+
const UNRELEASED_VERSION = "0.0.0";
2225
const EXECUTABLE_DIVISORS = [64, 8, 1] as const;
2326

2427
const hasExecutableBit = (file: string): boolean => {
@@ -38,6 +41,19 @@ const runCommitlint = (message: string) =>
3841
const readJson = (file: string): Record<string, unknown> =>
3942
JSON.parse(readFileSync(file, "utf8")) as Record<string, unknown>;
4043

44+
const assertInitialReleaseLifecycle = (root: string): void => {
45+
const packageJson = readJson(join(root, "package.json"));
46+
const initialReleaseFile = join(root, INITIAL_RELEASE_FILE);
47+
48+
if (packageJson.version === UNRELEASED_VERSION) {
49+
assert.equal(existsSync(initialReleaseFile), true);
50+
assert.match(readFileSync(initialReleaseFile, "utf8"), INITIAL_RELEASE);
51+
return;
52+
}
53+
54+
assert.equal(existsSync(initialReleaseFile), false);
55+
};
56+
4157
test("package is public and exposes release commands", () => {
4258
const packageJson = readJson("package.json");
4359
const scripts = packageJson.scripts as Record<string, string>;
@@ -72,16 +88,43 @@ test("package is public and exposes release commands", () => {
7288

7389
test("Changesets targets public main releases", () => {
7490
const config = readJson(".changeset/config.json");
75-
const initialChangeset = readFileSync(
76-
".changeset/initial-public-release.md",
77-
"utf8"
78-
);
7991

8092
assert.equal(config.baseBranch, "main");
8193
assert.equal(config.access, "public");
8294
assert.equal(config.commit, false);
8395
assert.equal(config.changelog, "@changesets/cli/changelog");
84-
assert.match(initialChangeset, INITIAL_RELEASE);
96+
assertInitialReleaseLifecycle(".");
97+
});
98+
99+
test("initial release seed follows the package lifecycle", () => {
100+
const fixture = mkdtempSync(join(tmpdir(), "judge-release-lifecycle-"));
101+
102+
try {
103+
mkdirSync(join(fixture, ".changeset"));
104+
writeFileSync(
105+
join(fixture, "package.json"),
106+
JSON.stringify({ name: "@brkn-labs/judge", version: UNRELEASED_VERSION })
107+
);
108+
109+
assert.throws(() => assertInitialReleaseLifecycle(fixture));
110+
111+
writeFileSync(
112+
join(fixture, INITIAL_RELEASE_FILE),
113+
'---\n"@brkn-labs/judge": minor\n---\n\nInitial public release.\n'
114+
);
115+
assert.doesNotThrow(() => assertInitialReleaseLifecycle(fixture));
116+
117+
writeFileSync(
118+
join(fixture, "package.json"),
119+
JSON.stringify({ name: "@brkn-labs/judge", version: "0.1.0" })
120+
);
121+
assert.throws(() => assertInitialReleaseLifecycle(fixture));
122+
123+
rmSync(join(fixture, INITIAL_RELEASE_FILE));
124+
assert.doesNotThrow(() => assertInitialReleaseLifecycle(fixture));
125+
} finally {
126+
rmSync(fixture, { force: true, recursive: true });
127+
}
85128
});
86129

87130
test("local hooks enforce quality and Conventional Commits", () => {

0 commit comments

Comments
 (0)