@afokapu/atdd-bun 0.10.71 → 0.10.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/integrity.json +4 -4
- package/package.json +1 -1
- package/src/ci.ts +12 -1
- package/src/hooks.ts +37 -10
- package/templates/github/atdd-bun.yml +1 -0
package/integrity.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.10.
|
|
2
|
+
"version": "0.10.73",
|
|
3
3
|
"files": {
|
|
4
4
|
"HOOK_AUDIT.md": "5329d840db37671b1918f688ead26865473b87db73dbc75f7c8b2a8bbe8d6d43",
|
|
5
5
|
"PLANNER_PORT.md": "fb5935bac8b7ac18994de21e43ace3a5ef8cd55f85b0e3349fca261280054f11",
|
|
@@ -1257,11 +1257,11 @@
|
|
|
1257
1257
|
"planner-schemas/wmbt.schema.json": "7a3a5693bfcf01340667c2be4a3c48e74f34b1af7e8dc13372c0de06b7f4fe12",
|
|
1258
1258
|
"relationships.yaml": "5860a308ebc3ee7fa8172fc2c6633984000ab3003f0ae4b6fa28db48879eda22",
|
|
1259
1259
|
"src/agent.ts": "6d6c6e75908af8fbab13432f0001b0374bb25cf3dee46ba218dfd67e0090cb56",
|
|
1260
|
-
"src/ci.ts": "
|
|
1260
|
+
"src/ci.ts": "2a5e5052c4b99eb5c7282c7db2af3846ad6f3ca7d1eaefe924441116f765f37b",
|
|
1261
1261
|
"src/cli.ts": "995e41b3cb44c04f65f4d599f0fc51b93b17b7558b7c7dcfabed64de3d455250",
|
|
1262
1262
|
"src/docs-capability.ts": "bdeeb414649049b1cd4fa8008cada3a576318be51c43bec91bfa46a8e250c270",
|
|
1263
1263
|
"src/enforce.ts": "7e6b3a1fc7c90f8e1836ab3e4655778fb569a63be5e7c199de44024fc003e6be",
|
|
1264
|
-
"src/hooks.ts": "
|
|
1264
|
+
"src/hooks.ts": "03eadd10e97cb4c369b89668b86be6d3e7f50b1402c1e1293d822c414581a30c",
|
|
1265
1265
|
"src/index.ts": "4081f8b08d1d3ee9e15a9964143e53d9cfb94a5616ab3e58a180d24cc16dc381",
|
|
1266
1266
|
"src/integrity.ts": "f5ad22759eec675454df82cdcf791b1f081951f08ca5084ebdcb8cd82aa1ecef",
|
|
1267
1267
|
"src/journey-docs.ts": "994d229376244c84212cff951032823068a76565dd2fd427b9d44e341bd14853",
|
|
@@ -1279,6 +1279,6 @@
|
|
|
1279
1279
|
"templates/agents/atdd-bun.integrity.test.ts": "dec6f6e5f65a08d9512fc703b367c163f8c9da1f6aa7fd466835c1ec30348a41",
|
|
1280
1280
|
"templates/docs/site.css": "482b3427e3a3cb02a85dc85cfbeac80592dfd53abf42fc92d6ea7071f22afc95",
|
|
1281
1281
|
"templates/github/atdd-bun-release.yml": "d9547e9e6ef3ae010d53d55314f6a54567f88bd900f7e50d994cd308601d2684",
|
|
1282
|
-
"templates/github/atdd-bun.yml": "
|
|
1282
|
+
"templates/github/atdd-bun.yml": "53d77e2d0dc8fa7f8700ac1d4d5c7522f960b423e894d78e1d8bfc63d68b95c7"
|
|
1283
1283
|
}
|
|
1284
1284
|
}
|
package/package.json
CHANGED
package/src/ci.ts
CHANGED
|
@@ -6,6 +6,17 @@ import { policy } from "./hooks";
|
|
|
6
6
|
const root = resolve(import.meta.dir, "..");
|
|
7
7
|
const template = join(root, "templates/github/atdd-bun.yml");
|
|
8
8
|
const version = (await Bun.file(join(root, "package.json")).json() as { version: string }).version;
|
|
9
|
+
/** A repository that depends on Playwright needs its browser binary on the runner, or `bun test` fails with a missing
|
|
10
|
+
* executable. The step is emitted only for a direct dependency; an unreadable package.json yields no step. */
|
|
11
|
+
async function usesPlaywright(repo: string) {
|
|
12
|
+
const path = join(repo, "package.json");
|
|
13
|
+
if (!existsSync(path)) return false;
|
|
14
|
+
try {
|
|
15
|
+
const manifest = JSON.parse(await readFile(path, "utf8")) as { dependencies?: unknown; devDependencies?: unknown } | null;
|
|
16
|
+
return [manifest?.dependencies, manifest?.devDependencies].some(group => group !== null && typeof group === "object" && ["@playwright/test", "playwright"].some(name => Object.hasOwn(group, name)));
|
|
17
|
+
} catch { return false; }
|
|
18
|
+
}
|
|
19
|
+
const playwrightStep = " - run: ./node_modules/.bin/playwright install --with-deps chromium\n";
|
|
9
20
|
/** The workflow for this repository: pushes to its protected branches run the post-merge checks, so a repository
|
|
10
21
|
* whose base branch is not main or master is covered too. The integrity check compares against this same rendering.
|
|
11
22
|
* Each branch is written as a JSON string, a YAML scalar a branch name can never break out of. */
|
|
@@ -16,7 +27,7 @@ export async function renderWorkflow(repo = process.cwd()) {
|
|
|
16
27
|
const config = Bun.YAML.parse(await readFile(configPath, "utf8"));
|
|
17
28
|
rejectNew = Boolean(config && typeof config === "object" && !Array.isArray(config) && (config as { ratchet?: { mode?: unknown } }).ratchet?.mode === "reject-new");
|
|
18
29
|
} catch { /* policy(repo) below gives its existing parse diagnostic */ }
|
|
19
|
-
return (await readFile(template, "utf8")).replace("{{VERSION}}", version).replace("{{PROTECTED_BRANCHES}}", (await policy(repo)).protected_branches.map(branch => JSON.stringify(branch)).join(", ")).replace("{{ENFORCEMENT}}", rejectNew ? 'bun run atdd-bun all --ratchet --base "$ATDD_BASE_REF" --ratchet-activate-profiles' : "bun run atdd-bun all");
|
|
30
|
+
return (await readFile(template, "utf8")).replace("{{PLAYWRIGHT}}\n", await usesPlaywright(repo) ? playwrightStep : "").replace("{{VERSION}}", version).replace("{{PROTECTED_BRANCHES}}", (await policy(repo)).protected_branches.map(branch => JSON.stringify(branch)).join(", ")).replace("{{ENFORCEMENT}}", rejectNew ? 'bun run atdd-bun all --ratchet --base "$ATDD_BASE_REF" --ratchet-activate-profiles' : "bun run atdd-bun all");
|
|
20
31
|
}
|
|
21
32
|
const githubPackagesRegistry = /^\s*@[\w.-]+:registry\s*=\s*https:\/\/npm\.pkg\.github\.com\/?\s*$/im;
|
|
22
33
|
const githubPackagesAuth = /^\s*\/\/npm\.pkg\.github\.com\/:_authToken\s*=\s*(.*?)\s*$/gim;
|
package/src/hooks.ts
CHANGED
|
@@ -114,13 +114,13 @@ async function strictPushGate(root: string, candidate: string, range: string) {
|
|
|
114
114
|
try {
|
|
115
115
|
await checkoutCommit(root, candidateRoot, candidate, "strict pre-push candidate");
|
|
116
116
|
const changed = await files(candidateRoot, ["diff", "--name-only", range]);
|
|
117
|
-
return validation(candidateRoot, changed, true);
|
|
117
|
+
return await validation(candidateRoot, changed, true);
|
|
118
118
|
} catch (error) { return bad(`strict pre-push gate failed closed: ${String(error)}`); }
|
|
119
119
|
finally { await git(root, ["worktree", "remove", "--force", candidateRoot]); await rm(candidateRoot, { recursive: true, force: true }); }
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** A brand-new remote branch is ratcheted against the one protected remote-tracking tip that is an exact ancestor of the candidate. */
|
|
123
|
-
async function
|
|
123
|
+
async function protectedAncestors(root: string, candidate: string, protectedBranches: string[], remote: string | undefined): Promise<{ ancestors: string[] } | { error: string }> {
|
|
124
124
|
const listed = await git(root, ["for-each-ref", "--format=%(refname) %(objectname)", "refs/remotes/"]);
|
|
125
125
|
if (listed.code) return { error: `could not list protected branch tips for the new remote branch: ${listed.err || listed.out}` };
|
|
126
126
|
const remotes = (await git(root, ["remote"])).out.split("\n").filter(Boolean), pushRemote = remote && remotes.includes(remote) ? remote : undefined;
|
|
@@ -130,9 +130,24 @@ async function newBranchBase(root: string, candidate: string, protectedBranches:
|
|
|
130
130
|
});
|
|
131
131
|
const ancestors = new Set<string>();
|
|
132
132
|
for (const tip of new Set(tips)) if ((await git(root, ["merge-base", "--is-ancestor", tip, candidate])).code === 0) ancestors.add(tip);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
return { ancestors: [...ancestors] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function newBranchBase(root: string, candidate: string, protectedBranches: string[], remote: string | undefined): Promise<{ base: string } | { error: string }> {
|
|
137
|
+
const found = await protectedAncestors(root, candidate, protectedBranches, remote);
|
|
138
|
+
if ("error" in found) return found;
|
|
139
|
+
if (!found.ancestors.length) return { error: "ratchet new remote branch requires a protected branch tip that is an exact ancestor of the pushed candidate; none found" };
|
|
140
|
+
if (found.ancestors.length > 1) return { error: `ratchet new remote branch base is ambiguous: ${found.ancestors.length} protected branch tips qualify with different commits` };
|
|
141
|
+
return { base: found.ancestors[0]! };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** For an existing remote branch the protected ancestor tip also counts as carried: CI judges against the protected base. */
|
|
145
|
+
async function existingBranchProtectedBase(root: string, candidate: string, remoteBase: string, protectedBranches: string[], remote: string | undefined): Promise<{ base: string | null } | { error: string }> {
|
|
146
|
+
const found = await protectedAncestors(root, candidate, protectedBranches, remote);
|
|
147
|
+
if ("error" in found) return found;
|
|
148
|
+
if (found.ancestors.length > 1) return { error: `ratchet existing remote branch protected base is ambiguous: ${found.ancestors.length} protected branch tips qualify with different commits` };
|
|
149
|
+
const tip = found.ancestors[0];
|
|
150
|
+
return { base: tip && tip !== remoteBase.toLowerCase() ? tip : null };
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
async function ratchetPushGate(root: string, remoteBase: string, candidate: string, protectedBranches: string[], remote: string | undefined): Promise<{ ok: boolean; message: string } | null> {
|
|
@@ -140,7 +155,7 @@ async function ratchetPushGate(root: string, remoteBase: string, candidate: stri
|
|
|
140
155
|
if (!/^[0-9a-f]{40}$/i.test(candidate)) return bad("ratchet reject-new requires an exact local candidate commit");
|
|
141
156
|
const resolvedCandidate = await git(root, ["rev-parse", "--verify", `${candidate}^{commit}`]);
|
|
142
157
|
if (resolvedCandidate.code || resolvedCandidate.out.toLowerCase() !== candidate.toLowerCase()) return bad(`ratchet candidate ${candidate} is unavailable or not a full exact commit`);
|
|
143
|
-
const baseRoot = await mkdtemp(join(tmpdir(), "atdd-ratchet-push-base-")), candidateRoot = await mkdtemp(join(tmpdir(), "atdd-ratchet-push-candidate-"));
|
|
158
|
+
const baseRoot = await mkdtemp(join(tmpdir(), "atdd-ratchet-push-base-")), protectedRoot = await mkdtemp(join(tmpdir(), "atdd-ratchet-push-protected-")), candidateRoot = await mkdtemp(join(tmpdir(), "atdd-ratchet-push-candidate-"));
|
|
144
159
|
try {
|
|
145
160
|
await checkoutCommit(root, candidateRoot, candidate, "ratchet candidate");
|
|
146
161
|
const candidateConfig = await configAt(candidateRoot);
|
|
@@ -148,6 +163,8 @@ async function ratchetPushGate(root: string, remoteBase: string, candidate: stri
|
|
|
148
163
|
try { policy = parseRatchetPolicy(candidateConfig); }
|
|
149
164
|
catch (error) { return bad(`ratchet pushed candidate policy is invalid: ${String(error)}`); }
|
|
150
165
|
if (!policy || policy.mode !== "reject-new") return null;
|
|
166
|
+
let protectedBase: string | null = null;
|
|
167
|
+
if (!/^0+$/.test(remoteBase)) { const found = await existingBranchProtectedBase(root, candidate, remoteBase, protectedBranches, remote); if ("error" in found) return bad(found.error); protectedBase = found.base; }
|
|
151
168
|
if (/^0+$/.test(remoteBase)) { const found = await newBranchBase(root, candidate, protectedBranches, remote); if ("error" in found) return bad(found.error); base = found.base; }
|
|
152
169
|
if (!/^[0-9a-f]{40}$/i.test(base)) return bad("ratchet reject-new requires an exact non-zero remote commit base");
|
|
153
170
|
const resolvedBase = await git(root, ["rev-parse", "--verify", `${base}^{commit}`]);
|
|
@@ -169,17 +186,27 @@ async function ratchetPushGate(root: string, remoteBase: string, candidate: stri
|
|
|
169
186
|
const unchanged = baseProfiles.join(",") === candidateProfiles.join(",") && (!basePolicyProfiles || basePolicyProfiles.join(",") === baseProfiles.join(",")) && contextDigest(baseConfig, baseProfiles) === contextDigest(candidateConfig, candidateProfiles);
|
|
170
187
|
if (policyProfiles.join(",") !== candidateProfiles.join(",") || (!activation && !unchanged)) return bad("ratchet remote base and pushed candidate profile/context differ");
|
|
171
188
|
const normalize = (findings: Awaited<ReturnType<typeof enforce>>, directory: string) => findings.map(finding => ({ ...finding, file: canonicalFindingPath(directory, finding.file) }));
|
|
172
|
-
|
|
189
|
+
let protectedUsed: string | null = null;
|
|
190
|
+
if (protectedBase) {
|
|
191
|
+
await checkoutCommit(root, protectedRoot, protectedBase, "ratchet protected base");
|
|
192
|
+
const protectedConfig = await configAt(protectedRoot);
|
|
193
|
+
// Only a protected tip judged under the same profile/context as the candidate is comparable; otherwise the remote tip alone decides.
|
|
194
|
+
if (contextDigest(protectedConfig, candidateProfiles) === contextDigest(candidateConfig, candidateProfiles)) protectedUsed = protectedBase;
|
|
195
|
+
}
|
|
196
|
+
const [baseFindings, candidateFindings, protectedFindings] = await Promise.all([
|
|
173
197
|
enforce({ root: baseRoot, profiles: candidateProfiles as any }),
|
|
174
198
|
enforce({ root: candidateRoot, profiles: candidateProfiles as any }),
|
|
199
|
+
protectedUsed ? enforce({ root: protectedRoot, profiles: candidateProfiles as any }) : Promise.resolve([]),
|
|
175
200
|
]);
|
|
176
|
-
const
|
|
177
|
-
|
|
201
|
+
const carriedBase = [...normalize(baseFindings, baseRoot), ...normalize(protectedFindings, protectedRoot)];
|
|
202
|
+
const delta = compareFindings(carriedBase, normalize(candidateFindings, candidateRoot));
|
|
203
|
+
return { ok: delta.new.length === 0, message: JSON.stringify({ schema: "atdd-bun.ratchet-report/v1", mode: policy.mode, base, ...(protectedUsed ? { protected_base: protectedUsed } : {}), candidate, profiles: candidateProfiles, context: contextDigest(candidateConfig, candidateProfiles), ...delta }) };
|
|
178
204
|
} catch (error) { return bad(`ratchet pre-push gate failed closed: ${String(error)}`); }
|
|
179
205
|
finally {
|
|
180
206
|
await git(root, ["worktree", "remove", "--force", baseRoot]);
|
|
207
|
+
await git(root, ["worktree", "remove", "--force", protectedRoot]);
|
|
181
208
|
await git(root, ["worktree", "remove", "--force", candidateRoot]);
|
|
182
|
-
await Promise.all([rm(baseRoot, { recursive: true, force: true }), rm(candidateRoot, { recursive: true, force: true })]);
|
|
209
|
+
await Promise.all([rm(baseRoot, { recursive: true, force: true }), rm(protectedRoot, { recursive: true, force: true }), rm(candidateRoot, { recursive: true, force: true })]);
|
|
183
210
|
}
|
|
184
211
|
}
|
|
185
212
|
|