Skip to content

Commit 6ce30c3

Browse files
committed
fix(release-notes): let Artur sync acknowledge known-in-source drift
1 parent 6499dc7 commit 6ce30c3

3 files changed

Lines changed: 152 additions & 3 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"commit": "6cf7474224eaeb97aca9e77a6a4bedb26045df01",
4+
"reason": "Arthur PR #23 (fix(worker): support investigate keyword schema) removes maxItems from KEYWORDS_SCHEMA in apps/worker/src/workflows/blocks/investigate.ts. Source main already omits maxItems from that schema (verified 2026-08-17), so the drift guard's patch-id comparison is a false positive: overwriting Arthur with the source snapshot preserves the change rather than reverting it. Tracked under AIW-281."
5+
}
6+
]

scripts/release-notes/cli.test.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "node:assert/strict";
2-
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
2+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
33
import os from "node:os";
44
import path from "node:path";
55
import test from "node:test";
@@ -278,3 +278,122 @@ test("sync-artur CLI blocks an unbackported destination commit", async () => {
278278
);
279279
assert.equal(synchronized, false);
280280
});
281+
282+
test("sync-artur CLI proceeds when drift is acknowledged in source main", async () => {
283+
const acknowledgedCommit = "6cf7474224eaeb97aca9e77a6a4bedb26045df01";
284+
const sourceMain = await mkdtemp(path.join(os.tmpdir(), "artur-sync-ack-src-"));
285+
await mkdir(path.join(sourceMain, "scripts/release-notes"), { recursive: true });
286+
await writeFile(
287+
path.join(sourceMain, "scripts/release-notes/acknowledged-drift.json"),
288+
JSON.stringify([{ commit: acknowledgedCommit, reason: "already present in source" }]),
289+
);
290+
const output = await mkdtemp(path.join(os.tmpdir(), "artur-sync-ack-out-"));
291+
const approvalPath = path.join(output, "approved.json");
292+
const resultPath = path.join(output, "result.json");
293+
await writeFile(
294+
approvalPath,
295+
JSON.stringify({
296+
version: "2026.08.0",
297+
previousSourceCommit: "a".repeat(40),
298+
targetSourceCommit: "b".repeat(40),
299+
notesPath: "docs/releases/artur/2026.08.0.md",
300+
releaseNotesPullRequest: 193,
301+
releaseNotesApprovedBy: ["zak"],
302+
}),
303+
);
304+
const expected = {
305+
version: "2026.08.0",
306+
sourceCommit: "b".repeat(40),
307+
destinationBaseCommit: "c".repeat(40),
308+
notesPath: "docs/releases/artur/2026.08.0.md",
309+
added: [],
310+
modified: [],
311+
deleted: [],
312+
preserved: [],
313+
driftCommits: [],
314+
};
315+
let synchronized = false;
316+
await runCli(
317+
[
318+
"sync-artur",
319+
"--version",
320+
"2026.08.0",
321+
"--approval",
322+
approvalPath,
323+
"--source-main",
324+
sourceMain,
325+
"--source-snapshot",
326+
"/source-snapshot",
327+
"--destination",
328+
"/destination",
329+
"--previous-destination-ref",
330+
"baseline",
331+
"--output",
332+
resultPath,
333+
],
334+
{
335+
findDrift: async () => [acknowledgedCommit],
336+
sync: async () => {
337+
synchronized = true;
338+
return expected;
339+
},
340+
},
341+
);
342+
assert.equal(synchronized, true);
343+
const record = JSON.parse(await readFile(resultPath, "utf8"));
344+
assert.deepEqual(record.driftCommits, []);
345+
assert.deepEqual(record.acknowledgedDrift, [acknowledgedCommit]);
346+
});
347+
348+
test("sync-artur CLI still blocks drift that is not acknowledged", async () => {
349+
const sourceMain = await mkdtemp(path.join(os.tmpdir(), "artur-sync-ack-neg-"));
350+
await mkdir(path.join(sourceMain, "scripts/release-notes"), { recursive: true });
351+
await writeFile(
352+
path.join(sourceMain, "scripts/release-notes/acknowledged-drift.json"),
353+
JSON.stringify([{ commit: "e".repeat(40), reason: "unrelated" }]),
354+
);
355+
const output = await mkdtemp(path.join(os.tmpdir(), "artur-sync-ack-neg-out-"));
356+
const approvalPath = path.join(output, "approved.json");
357+
await writeFile(
358+
approvalPath,
359+
JSON.stringify({
360+
version: "2026.08.0",
361+
previousSourceCommit: "a".repeat(40),
362+
targetSourceCommit: "b".repeat(40),
363+
notesPath: "docs/releases/artur/2026.08.0.md",
364+
releaseNotesPullRequest: 193,
365+
releaseNotesApprovedBy: ["zak"],
366+
}),
367+
);
368+
let synchronized = false;
369+
await assert.rejects(
370+
runCli(
371+
[
372+
"sync-artur",
373+
"--version",
374+
"2026.08.0",
375+
"--approval",
376+
approvalPath,
377+
"--source-main",
378+
sourceMain,
379+
"--source-snapshot",
380+
"/source-snapshot",
381+
"--destination",
382+
"/destination",
383+
"--previous-destination-ref",
384+
"baseline",
385+
"--output",
386+
path.join(output, "result.json"),
387+
],
388+
{
389+
findDrift: async () => ["f".repeat(40)],
390+
sync: async () => {
391+
synchronized = true;
392+
throw new Error("must not run");
393+
},
394+
},
395+
),
396+
/not backported.*ffff/i,
397+
);
398+
assert.equal(synchronized, false);
399+
});

scripts/release-notes/cli.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,13 @@ const approvedSourceSchema = z.object({
216216
releaseNotesApprovedBy: z.array(z.string().min(1)).min(1),
217217
});
218218

219+
const acknowledgedDriftSchema = z.array(
220+
z.object({
221+
commit: z.string().regex(/^[0-9a-f]{40}$/),
222+
reason: z.string().min(1),
223+
}),
224+
);
225+
219226
interface CliDeps {
220227
validate?: typeof validateApprovedSourceRelease;
221228
findDrift?: typeof findUnbackportedDestinationCommits;
@@ -330,6 +337,18 @@ async function guardArturCommand(argv: string[]): Promise<unknown> {
330337
return { version, available: true };
331338
}
332339

340+
async function readAcknowledgedDrift(sourceMainDir: string): Promise<Set<string>> {
341+
const file = path.join(sourceMainDir, "scripts/release-notes/acknowledged-drift.json");
342+
let raw: string;
343+
try {
344+
raw = await readFile(file, "utf8");
345+
} catch (error) {
346+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return new Set();
347+
throw error;
348+
}
349+
return new Set(acknowledgedDriftSchema.parse(JSON.parse(raw)).map((entry) => entry.commit));
350+
}
351+
333352
async function syncArturCommand(argv: string[], deps: CliDeps): Promise<SyncResult> {
334353
const version = parseVersion(requiredArg(argv, "version"));
335354
const approval = approvedSourceSchema.parse(
@@ -342,13 +361,16 @@ async function syncArturCommand(argv: string[], deps: CliDeps): Promise<SyncResu
342361
const sourceSnapshotDir = path.resolve(requiredArg(argv, "source-snapshot"));
343362
const destinationDir = path.resolve(requiredArg(argv, "destination"));
344363
const previousDestinationRef = requiredArg(argv, "previous-destination-ref");
345-
const driftCommits = await (deps.findDrift ?? findUnbackportedDestinationCommits)({
364+
const foundDrift = await (deps.findDrift ?? findUnbackportedDestinationCommits)({
346365
sourceSnapshotDir,
347366
destinationDir,
348367
previousSourceCommit: approval.previousSourceCommit,
349368
targetSourceCommit: approval.targetSourceCommit,
350369
previousDestinationRef,
351370
});
371+
const acknowledgedDrift = await readAcknowledgedDrift(sourceMainDir);
372+
const acknowledged = foundDrift.filter((commit) => acknowledgedDrift.has(commit));
373+
const driftCommits = foundDrift.filter((commit) => !acknowledgedDrift.has(commit));
352374
if (driftCommits.length > 0) {
353375
throw new Error(
354376
`Artur contains application commits that are not backported to the selected source snapshot: ${driftCommits.join(", ")}`,
@@ -363,7 +385,9 @@ async function syncArturCommand(argv: string[], deps: CliDeps): Promise<SyncResu
363385
});
364386
const outputPath = path.resolve(requiredArg(argv, "output"));
365387
await mkdir(path.dirname(outputPath), { recursive: true });
366-
await writeFile(outputPath, `${JSON.stringify({ ...result, driftCommits }, null, 2)}\n`);
388+
const record: Record<string, unknown> = { ...result, driftCommits };
389+
if (acknowledged.length > 0) record.acknowledgedDrift = acknowledged;
390+
await writeFile(outputPath, `${JSON.stringify(record, null, 2)}\n`);
367391
return { ...result, driftCommits };
368392
}
369393

0 commit comments

Comments
 (0)