@arjunkhera/atlas 0.4.34 → 0.4.36
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/.claude-plugin/plugin.json +1 -1
- package/door/lib/gate.mjs +119 -3
- package/package.json +1 -1
- package/work/lib/numbers.mjs +35 -4
- package/work/lib/roundup.mjs +2 -2
- package/work/mcp.mjs +3 -0
package/door/lib/gate.mjs
CHANGED
|
@@ -252,7 +252,7 @@ export const gateRunTitle = (pr, sha) => `gate #${pr} at ${sha}`;
|
|
|
252
252
|
// The audit reads a merged pull request. GitHub then lists no pull request
|
|
253
253
|
// on its runs, so with `audit` the two pull request pins are left out; the
|
|
254
254
|
// file, the event, the head and the title still hold.
|
|
255
|
-
export function refusal(run, { name, pr, sha, baseRef, homes, sides = () => [], audit = false }) {
|
|
255
|
+
export function refusal(run, { name, pr, sha, baseRef, homes, sides = () => [], audit = false, baseChanged = () => '' }) {
|
|
256
256
|
if (!run) return 'no workflow run';
|
|
257
257
|
if (!homes.get(name)?.has(run.path)) return `from ${run.path ?? 'no file'}, not a base file that defines the check`;
|
|
258
258
|
const event = name === GATE_CHECK ? 'pull_request_target' : 'pull_request';
|
|
@@ -272,6 +272,12 @@ export function refusal(run, { name, pr, sha, baseRef, homes, sides = () => [],
|
|
|
272
272
|
const side = sides();
|
|
273
273
|
if (side === null) return 'no head branch to check for side pull requests';
|
|
274
274
|
if (side.length) return `the head branch also had #${side[0].number} into ${side[0].base?.ref ?? 'another branch'}`;
|
|
275
|
+
// A pull request can change its base. A run made while it pointed at a side
|
|
276
|
+
// branch used that branch's file, and the lists above no longer show it. So
|
|
277
|
+
// a run made before the last base change does not count (item 490bd791).
|
|
278
|
+
const changed = baseChanged();
|
|
279
|
+
if (changed === null) return `no events of #${pr} to check for a base change`;
|
|
280
|
+
if (changed && !(String(run.created_at ?? '') > changed)) return `made before the base of #${pr} last changed (${changed})`;
|
|
275
281
|
return null;
|
|
276
282
|
}
|
|
277
283
|
|
|
@@ -291,6 +297,77 @@ export function jobRefusal(job, check, run) {
|
|
|
291
297
|
return null;
|
|
292
298
|
}
|
|
293
299
|
|
|
300
|
+
// Every event of one pull request, oldest first, or null when GitHub gives
|
|
301
|
+
// none or more pages than `pages`. The newest event sits on the last page.
|
|
302
|
+
function eventsOf(gh, repo, pr, pages = 10) {
|
|
303
|
+
const all = [];
|
|
304
|
+
for (let page = 1; page <= pages; page += 1) {
|
|
305
|
+
const got = gh('GET', `repos/${repo}/issues/${pr}/events?per_page=100&page=${page}`);
|
|
306
|
+
if (!Array.isArray(got)) return null;
|
|
307
|
+
all.push(...got);
|
|
308
|
+
if (got.length < 100) return all;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// The merge lock (item a638ce35). Two pull requests, each READY on the same
|
|
314
|
+
// main tip, can merge seconds apart and turn the main branch red together
|
|
315
|
+
// (atlas#214 and atlas#215). So a READY takes a lock: the label MERGE_LABEL
|
|
316
|
+
// on its pull request. A lock lives LOCK_MS from its label event, a minute
|
|
317
|
+
// longer than a READY file, or until its pull request closes. A label makes
|
|
318
|
+
// no ref and starts no workflow. Times come from GitHub, not the local clock.
|
|
319
|
+
export const MERGE_LABEL = 'atlas-merging';
|
|
320
|
+
export const LOCK_MS = 11 * 60 * 1000;
|
|
321
|
+
|
|
322
|
+
// The newest event that put the label on or took it off, or null. GitHub
|
|
323
|
+
// lists the events oldest first.
|
|
324
|
+
const labelEvents = (events) => events
|
|
325
|
+
.filter((each) => (each?.event === 'labeled' || each?.event === 'unlabeled') && each.label?.name === MERGE_LABEL);
|
|
326
|
+
const lastLabelEvent = (events) => labelEvents(events).at(-1) ?? null;
|
|
327
|
+
|
|
328
|
+
// `lock.on` says whether the pull request carries the label now.
|
|
329
|
+
export function dropLock(gh, repo, pull, lock) {
|
|
330
|
+
if (!lock.on) return;
|
|
331
|
+
gh('DELETE', `repos/${repo}/issues/${pull.number}/labels/${MERGE_LABEL}`);
|
|
332
|
+
lock.on = false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Take the lock for `pull`. Returns null when it holds the lock, or why not;
|
|
336
|
+
// the caller then takes the label off. Each caller puts its label on first,
|
|
337
|
+
// then reads the others. Of two live labels, the older one wins, and the
|
|
338
|
+
// lower number breaks a tie. So of two ready calls in the same seconds,
|
|
339
|
+
// exactly one keeps its label.
|
|
340
|
+
export function takeLock(gh, repo, pull, lock) {
|
|
341
|
+
// GitHub can show its events late, and an old label event would make this
|
|
342
|
+
// lock look older. Events only grow, so the label events are counted first:
|
|
343
|
+
// the read after must hold each new one, and the newest must put it on.
|
|
344
|
+
const before = eventsOf(gh, repo, pull.number);
|
|
345
|
+
if (!before) return { why: `GitHub gave no events of #${pull.number}` };
|
|
346
|
+
const made = lock.on ? 2 : 1;
|
|
347
|
+
// A new label event restarts the lock, so an old label comes off first.
|
|
348
|
+
dropLock(gh, repo, pull, lock);
|
|
349
|
+
gh('POST', `repos/${repo}/issues/${pull.number}/labels`, { labels: [MERGE_LABEL] });
|
|
350
|
+
lock.on = true;
|
|
351
|
+
const mine = eventsOf(gh, repo, pull.number);
|
|
352
|
+
const last = mine && lastLabelEvent(mine);
|
|
353
|
+
if (!mine || labelEvents(mine).length < labelEvents(before).length + made || last?.event !== 'labeled') return { why: `GitHub does not show the new label event on #${pull.number} yet` };
|
|
354
|
+
const at = String(last.created_at ?? '');
|
|
355
|
+
const held = gh('GET', `repos/${repo}/issues?labels=${MERGE_LABEL}&state=open&per_page=100`);
|
|
356
|
+
if (!Array.isArray(held)) return { why: 'GitHub gave no list of locked pull requests' };
|
|
357
|
+
for (const other of held) {
|
|
358
|
+
if (!other?.pull_request || Number(other.number) === Number(pull.number)) continue;
|
|
359
|
+
const events = eventsOf(gh, repo, other.number);
|
|
360
|
+
const theirs = events && lastLabelEvent(events);
|
|
361
|
+
if (!theirs) return { why: `GitHub shows no label event on #${other.number}`, pr: other.number };
|
|
362
|
+
// The list can be late too: a label already taken off holds no lock.
|
|
363
|
+
if (theirs.event !== 'labeled') continue;
|
|
364
|
+
const since = String(theirs.created_at ?? '');
|
|
365
|
+
if (Date.parse(at) - Date.parse(since) >= LOCK_MS) continue;
|
|
366
|
+
if (since < at || (since === at && Number(other.number) < Number(pull.number))) return { why: `#${other.number} holds the merge lock since ${since}`, pr: other.number };
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
|
|
294
371
|
// Every pull request, open or closed, from the head branch into another
|
|
295
372
|
// branch. Read once, and only when a gate run needs it.
|
|
296
373
|
function sidesOf(gh, repo, head, baseRef) {
|
|
@@ -304,6 +381,18 @@ function sidesOf(gh, repo, head, baseRef) {
|
|
|
304
381
|
};
|
|
305
382
|
}
|
|
306
383
|
|
|
384
|
+
// The time of the last base change of one pull request, '' for none, or
|
|
385
|
+
// null when GitHub gives no events. Read once, when the first run needs it.
|
|
386
|
+
function baseChangedOf(gh, repo, pr) {
|
|
387
|
+
let changedAt;
|
|
388
|
+
return () => {
|
|
389
|
+
if (changedAt !== undefined) return changedAt;
|
|
390
|
+
const events = eventsOf(gh, repo, pr);
|
|
391
|
+
if (!events) return (changedAt = null);
|
|
392
|
+
return (changedAt = events.filter((each) => each?.event === 'base_ref_changed').map((each) => String(each.created_at ?? '')).sort().pop() ?? '');
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
307
396
|
// The workflow run of each check suite, read once.
|
|
308
397
|
function suiteRuns(gh, repo) {
|
|
309
398
|
const suites = new Map();
|
|
@@ -337,13 +426,14 @@ export function gateCopyRefusals(gh, repo, pull) {
|
|
|
337
426
|
export function pinnedRuns(gh, repo, { pr, sha, baseRef, head, checks, homesRef = baseRef, audit = false }) {
|
|
338
427
|
const homes = homesAt(gh, repo, homesRef, checks);
|
|
339
428
|
const sides = sidesOf(gh, repo, head, baseRef);
|
|
429
|
+
const baseChanged = audit ? () => '' : baseChangedOf(gh, repo, pr);
|
|
340
430
|
const runOf = suiteRuns(gh, repo);
|
|
341
431
|
const kept = [];
|
|
342
432
|
const refused = [];
|
|
343
433
|
for (const check of checkRuns(gh, repo, sha)) {
|
|
344
434
|
if (!checks.includes(check.name) || check.app?.id !== ACTIONS_APP_ID) continue;
|
|
345
435
|
const run = runOf(check.check_suite?.id);
|
|
346
|
-
const why = refusal(run, { name: check.name, pr, sha, baseRef, homes, sides, audit })
|
|
436
|
+
const why = refusal(run, { name: check.name, pr, sha, baseRef, homes, sides, audit, baseChanged })
|
|
347
437
|
?? jobRefusal(check.id === undefined ? null : gh('GET', `repos/${repo}/actions/jobs/${check.id}`), check, run);
|
|
348
438
|
if (why) refused.push({ name: check.name, why }); else kept.push(check);
|
|
349
439
|
}
|
|
@@ -374,6 +464,20 @@ export function readyCommand({ repo, pr, answer }, line, gh = ghApi) {
|
|
|
374
464
|
const pull = gh('GET', `repos/${repo}/pulls/${pr}`);
|
|
375
465
|
if (answer && typeof pull?.head?.sha === 'string') answer.sha = pull.head.sha;
|
|
376
466
|
if (pull?.state !== 'open') { line(`atlas gate ready: pull request #${pr} is ${pull?.merged ? 'merged' : pull?.state ?? 'not found'}. NOT READY.`); return 1; }
|
|
467
|
+
// Any answer but READY, an error too, takes the merge label off.
|
|
468
|
+
const lock = { on: (pull.labels ?? []).some((each) => each?.name === MERGE_LABEL) };
|
|
469
|
+
let code;
|
|
470
|
+
try {
|
|
471
|
+
code = readyOpen({ repo, pr, pull, lock }, line, gh);
|
|
472
|
+
return code;
|
|
473
|
+
} finally {
|
|
474
|
+
if (code !== 0) {
|
|
475
|
+
try { dropLock(gh, repo, pull, lock); } catch (error) { if (code !== undefined) throw error; }
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function readyOpen({ repo, pr, pull, lock }, line, gh) {
|
|
377
481
|
const sha = pull.head.sha;
|
|
378
482
|
let checks;
|
|
379
483
|
try { checks = checksAt(gh, repo, pull.base.ref); } catch (error) {
|
|
@@ -395,7 +499,19 @@ export function readyCommand({ repo, pr, answer }, line, gh = ghApi) {
|
|
|
395
499
|
if (!(behind >= 0)) throw new GateError(`GitHub gave no compare of ${pull.base.ref} and ${sha.slice(0, 7)}.`);
|
|
396
500
|
line(` ${behind === 0 ? '✓' : '✗'} ${(behind === 0 ? 'current' : 'behind').padEnd(8)} ${behind === 0 ? `the head holds every commit of ${pull.base.ref}` : `${pull.base.ref} has ${behind} commit(s) that the head does not hold`}`);
|
|
397
501
|
const green = rows.every((row) => row.state === 'green');
|
|
398
|
-
if (green && behind === 0) {
|
|
502
|
+
if (green && behind === 0) {
|
|
503
|
+
const held = takeLock(gh, repo, pull, lock);
|
|
504
|
+
if (held) {
|
|
505
|
+
line(` ✗ locked ${held.why}`);
|
|
506
|
+
line(held.pr
|
|
507
|
+
? `NOT READY: do not merge. Wait for #${held.pr} to merge or for its lock to end (${LOCK_MS / 60000} min). Then merge ${pull.base.ref} in if it moved, and run ready again.`
|
|
508
|
+
: 'NOT READY: do not merge. The merge lock could not be read; run ready again.');
|
|
509
|
+
return 1;
|
|
510
|
+
}
|
|
511
|
+
line(` ✓ lock this pull request holds the merge lock (label ${MERGE_LABEL}) for ${LOCK_MS / 60000} min`);
|
|
512
|
+
line('READY: every required check is green, on top of the current main branch. Merge with a merge commit, at this head.');
|
|
513
|
+
return 0;
|
|
514
|
+
}
|
|
399
515
|
line(behind > 0
|
|
400
516
|
? `NOT READY: do not merge. Merge ${pull.base.ref} in, wait for green, then run ready again.`
|
|
401
517
|
: 'NOT READY: do not merge. Wait for a pending check; fix a red or missing one.');
|
package/package.json
CHANGED
package/work/lib/numbers.mjs
CHANGED
|
@@ -35,11 +35,18 @@ export const SECOND_CHECK = 'passes-after';
|
|
|
35
35
|
// package.json or a runner config changes for many reasons, so it is not.
|
|
36
36
|
const TEST_GROUPS = Object.freeze(['unit-test', 'check-block', 'scenario-words', 'test-helper', 'kit-manifest']);
|
|
37
37
|
// The GitHub counts and the tracker counts of one row.
|
|
38
|
-
export const GITHUB_COUNTS = Object.freeze(['merges', 'not_on_green', 'reverts', 'unchecked_test_changes', 'signed_acts']);
|
|
38
|
+
export const GITHUB_COUNTS = Object.freeze(['merges', 'not_on_green', 'reverts', 'unchecked_test_changes', 'signed_acts', 'audit_unsigned_merges']);
|
|
39
39
|
export const GRAPH_COUNTS = Object.freeze(['reopens', 'step_ins', 'owner_messages', 'platform_messages']);
|
|
40
|
-
// Line 2: the
|
|
41
|
-
// shown as zero.
|
|
42
|
-
export const WAITING = Object.freeze({
|
|
40
|
+
// Line 2: the counts that wait on an item not built yet. Each is named, never
|
|
41
|
+
// shown as zero. None waits now: R14 line 3 shipped the unsigned-merge finding.
|
|
42
|
+
export const WAITING = Object.freeze({});
|
|
43
|
+
// Line 2: the audit job (R14 line 3) opens one issue for each merge that the
|
|
44
|
+
// gate sent to the owner with no signed act. GitHub Actions writes it, with
|
|
45
|
+
// the label atlas-job and this title.
|
|
46
|
+
export const AUDIT_LABEL = 'atlas-job';
|
|
47
|
+
const AUDIT_TITLE = /^atlas-job: gate-audit owner-unsigned:([0-9a-f]{40})$/;
|
|
48
|
+
const AUDIT_AUTHOR = 'github-actions[bot]';
|
|
49
|
+
const MAX_ISSUE_PAGES = 10;
|
|
43
50
|
|
|
44
51
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
45
52
|
const dayOf = (value) => String(value ?? '').slice(0, 10);
|
|
@@ -108,6 +115,26 @@ async function readAllFiles(repo, number, readGitHub) {
|
|
|
108
115
|
throw new Error(`pull request ${number} changed more than ${MAX_FILE_PAGES * PAGE} files`);
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
// Line 2: the merge commits that the audit job reported as unsigned, from
|
|
119
|
+
// its issues opened since the start of the period. Only an issue that GitHub
|
|
120
|
+
// Actions opened counts, so a person cannot add one by hand. A closed issue
|
|
121
|
+
// still counts: the merge happened. More than MAX_ISSUE_PAGES pages is
|
|
122
|
+
// refused, never cut.
|
|
123
|
+
export async function readUnsignedMerges(repo, readGitHub, sinceAt) {
|
|
124
|
+
const shas = new Set();
|
|
125
|
+
const since = new Date(sinceAt).toISOString();
|
|
126
|
+
for (let page = 1; page <= MAX_ISSUE_PAGES; page += 1) {
|
|
127
|
+
const rows = await readGitHub(`repos/${repo}/issues?labels=${AUDIT_LABEL}&state=all&since=${since}&per_page=${PAGE}&page=${page}`);
|
|
128
|
+
if (!Array.isArray(rows)) throw new Error('the audit issues are not a list');
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
const match = AUDIT_TITLE.exec(String(row?.title ?? ''));
|
|
131
|
+
if (match && row?.user?.login === AUDIT_AUTHOR && !row.pull_request) shas.add(match[1].toLowerCase());
|
|
132
|
+
}
|
|
133
|
+
if (rows.length < PAGE) return shas;
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`more than ${MAX_ISSUE_PAGES * PAGE} audit issues`);
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
// Line 2: the comment texts of one pull request, every page. More than
|
|
112
139
|
// MAX_COMMENT_PAGES pages is refused, never cut.
|
|
113
140
|
export async function readAllComments(repo, number, readGitHub) {
|
|
@@ -222,6 +249,7 @@ export async function readMerges({ repo, defaultBranch, period, readGitHub }) {
|
|
|
222
249
|
const text = await textAt('atlas.yaml', sha);
|
|
223
250
|
return text === null ? [] : deliveryFacts(text).required;
|
|
224
251
|
};
|
|
252
|
+
const unsigned = await readUnsignedMerges(repo, readGitHub, period.start);
|
|
225
253
|
return pool(merges.map((pull) => async () => {
|
|
226
254
|
const head = pull.head?.sha, merge = pull.merge_commit_sha;
|
|
227
255
|
if (!/^[0-9a-f]{40}$/i.test(String(head)) || !/^[0-9a-f]{40}$/i.test(String(merge))) throw new Error(`GitHub names no head or merge commit for pull request ${pull.number}`);
|
|
@@ -245,6 +273,7 @@ export async function readMerges({ repo, defaultBranch, period, readGitHub }) {
|
|
|
245
273
|
const signed = keys.length ? signedActOf({ repo, pull, texts: await readAllComments(repo, pull.number, readGitHub), keys }) : null;
|
|
246
274
|
return {
|
|
247
275
|
repo, number: pull.number, url: pull.html_url ?? `https://github.com/${repo}/pull/${pull.number}`, title: oneLine(pull.title ?? '', 200), merged_at: pull.merged_at,
|
|
276
|
+
audit_unsigned: unsigned.has(String(merge).toLowerCase()),
|
|
248
277
|
not_on_green: notOnGreenReasons({ required, runs, mergedAt: pull.merged_at }), revert: isRevert(pull),
|
|
249
278
|
changed_tests: tests, signed, unchecked: tests.length > 0 && !signed && !secondCheckPassed(runs, pull.merged_at),
|
|
250
279
|
};
|
|
@@ -320,6 +349,7 @@ export async function readNumbers({ product, repos, items, marks, period, readGi
|
|
|
320
349
|
if (merge.revert) events.push(['reverts', merge.merged_at]);
|
|
321
350
|
if (merge.unchecked) events.push(['unchecked_test_changes', merge.merged_at]);
|
|
322
351
|
if (merge.signed) events.push(['signed_acts', merge.merged_at]);
|
|
352
|
+
if (merge.audit_unsigned) events.push(['audit_unsigned_merges', merge.merged_at]);
|
|
323
353
|
}
|
|
324
354
|
for (const mark of reopens) if (itemRepos.get(mark.fields.item).includes(repo.id)) events.push(['reopens', mark.fields.marked_at]);
|
|
325
355
|
for (const pair of pairs) {
|
|
@@ -341,6 +371,7 @@ export async function readNumbers({ product, repos, items, marks, period, readGi
|
|
|
341
371
|
not_on_green: refused ? null : merges.filter((merge) => merge.not_on_green.length).map((merge) => ({ pr: merge.url, merged_at: merge.merged_at, reasons: merge.not_on_green })),
|
|
342
372
|
reverts: refused ? null : merges.filter((merge) => merge.revert).map((merge) => ({ pr: merge.url, title: merge.title })),
|
|
343
373
|
unchecked_test_changes: refused ? null : merges.filter((merge) => merge.unchecked).map((merge) => ({ pr: merge.url, tests: merge.changed_tests })),
|
|
374
|
+
audit_unsigned_merges: refused ? null : merges.filter((merge) => merge.audit_unsigned).map((merge) => ({ pr: merge.url, merged_at: merge.merged_at })),
|
|
344
375
|
signed_acts: refused ? null : merges.filter((merge) => merge.signed).map((merge) => ({ pr: merge.url, signed_at: merge.signed.signed_at, key: merge.signed.key })),
|
|
345
376
|
});
|
|
346
377
|
}
|
package/work/lib/roundup.mjs
CHANGED
|
@@ -266,7 +266,7 @@ function render(view) {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
// Part 6. A refused count reads "refused", never 0.
|
|
269
|
-
const COLUMNS = [['merges', 'merges'], ['not_on_green', 'not on green'], ['reverts', 'reverts'], ['unchecked_test_changes', 'unchecked test changes'], ['signed_acts', 'signed acts'], ['reopens', 'reopens'], ['step_ins', 'step-ins'], ['owner_messages', 'owner messages'], ['platform_messages', 'platform messages']];
|
|
269
|
+
const COLUMNS = [['merges', 'merges'], ['not_on_green', 'not on green'], ['reverts', 'reverts'], ['unchecked_test_changes', 'unchecked test changes'], ['signed_acts', 'signed acts'], ['audit_unsigned_merges', 'unsigned merges'], ['reopens', 'reopens'], ['step_ins', 'step-ins'], ['owner_messages', 'owner messages'], ['platform_messages', 'platform messages']];
|
|
270
270
|
const countsLine = (row) => COLUMNS.map(([key, word]) => `${row[key] === null ? 'refused' : row[key]} ${word}`).join(' · ');
|
|
271
271
|
function renderNumbers(numbers, more) {
|
|
272
272
|
const out = [`6. The numbers, ${numbers.from} to ${numbers.to} (UTC)`];
|
|
@@ -284,7 +284,7 @@ function renderNumbers(numbers, more) {
|
|
|
284
284
|
out.push(` input to live: ${numbers.delivered.length} delivered ${numbers.delivered.length === 1 ? 'item' : 'items'}${median === null ? '' : `, median ${median} days`}`);
|
|
285
285
|
for (const row of numbers.delivered.slice(0, SHOWN)) out.push(listLine(row.title, `${row.item.slice(0, 8)} · ${row.days ?? '?'} days · live ${dayOf(row.live_at)}`, ' '));
|
|
286
286
|
out.push(...more(numbers.delivered));
|
|
287
|
-
out.push(`
|
|
287
|
+
for (const [count, wait] of Object.entries(numbers.owner_acts ?? {})) out.push(` ${count.replace(/_/g, ' ')}: ${wait}`);
|
|
288
288
|
if (numbers.not_counted_sessions) out.push(` ${numbers.not_counted_sessions} sessions ran with the hooks off, so their messages were not counted`);
|
|
289
289
|
return out.join('\n');
|
|
290
290
|
}
|
package/work/mcp.mjs
CHANGED
|
@@ -227,6 +227,9 @@ export const GITHUB_READS = Object.freeze([
|
|
|
227
227
|
// and the owner key file at the first parent of the merge.
|
|
228
228
|
new RegExp(`^repos/${REPO}/issues/\\d{1,9}/comments\\?per_page=\\d{1,3}&page=\\d{1,3}$`),
|
|
229
229
|
new RegExp(`^repos/${REPO}/contents/atlas/owner-signers\\?ref=${SHA}$`),
|
|
230
|
+
// Roadmap R16 line 2: the audit issues of the period, for the merges that
|
|
231
|
+
// the audit job reported as unsigned.
|
|
232
|
+
new RegExp(`^repos/${REPO}/issues\\?labels=atlas-job&state=all&since=\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z&per_page=\\d{1,3}&page=\\d{1,3}$`),
|
|
230
233
|
]);
|
|
231
234
|
export function githubReader() {
|
|
232
235
|
return async (path) => {
|