@arjunkhera/atlas 0.4.43 → 0.4.45
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-check.mjs +105 -18
- package/door/lib/gate.mjs +4 -1
- package/owner-only.yaml +1 -0
- package/package.json +1 -1
- package/shape/check.mjs +39 -3
- package/shape/releases.json +5 -0
package/door/lib/gate-check.mjs
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
// the owner's signed act for this repo, this pull request and this head
|
|
25
25
|
// (roadmap R72, signed-act.mjs). The key comes from atlas/owner-signers on the
|
|
26
26
|
// base, never from the head. An act for an older head carries to this head
|
|
27
|
-
// when the new commits only merge the base in with no conflict (item 34b554e7)
|
|
27
|
+
// when the new commits only merge the base in with no conflict (item 34b554e7),
|
|
28
|
+
// or only rebuild the pages that atlas.yaml names under `generated` (item 4ee7cab3).
|
|
28
29
|
//
|
|
29
30
|
// Red wins over owner: the pull request must be fixed before the owner signs.
|
|
30
31
|
// Rules that hold everywhere here:
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
import { spawnSync } from 'node:child_process';
|
|
37
38
|
import { readFileSync, appendFileSync } from 'node:fs';
|
|
38
39
|
import { resolve } from 'node:path';
|
|
39
|
-
import { classify, changedPaths, compile, matcher, fakeSecret, repoFacts } from './owner-only.mjs';
|
|
40
|
+
import { classify, changedPaths, compile, matcher, fakeSecret, repoFacts, rulesAt, onList } from './owner-only.mjs';
|
|
40
41
|
import { MISSING, HIT, fileHits, hidePath, shippedPaths, ships, termsFromEnv } from './private-terms.mjs';
|
|
41
42
|
import { listChanged } from './changed-tests.mjs';
|
|
42
43
|
import { parseYaml, SECRET_PATTERNS } from '../../shape/check.mjs';
|
|
@@ -58,8 +59,8 @@ const CASE_GROUPS = new Set(['unit-test', 'check-block', 'scenario-words']);
|
|
|
58
59
|
|
|
59
60
|
export class GateCheckError extends Error {}
|
|
60
61
|
|
|
61
|
-
function git(root, args, { allowFail = false } = {}) {
|
|
62
|
-
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
62
|
+
function git(root, args, { allowFail = false, env = process.env } = {}) {
|
|
63
|
+
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, env });
|
|
63
64
|
if (r.error) throw new GateCheckError(`git cannot run: ${r.error.message}`);
|
|
64
65
|
if (r.status !== 0) {
|
|
65
66
|
if (allowFail) return null;
|
|
@@ -312,7 +313,7 @@ export function judge({ root, base, head, body = '', redMain = [], rules = null,
|
|
|
312
313
|
answer.rows.push(...secretRows(top, mergeBase, headSha, changes));
|
|
313
314
|
if (terms !== undefined) answer.rows.push(...termRows(top, baseSha, headSha, changes, terms));
|
|
314
315
|
answer.rows.push(...testRows({ root: top, mergeBase, head: headSha, body, redMain, rules: ruleRef, undo }));
|
|
315
|
-
if (signed) signedRows(top, baseSha, headSha, signed, answer);
|
|
316
|
+
if (signed) signedRows(top, baseSha, headSha, signed, answer, ruleRef);
|
|
316
317
|
return finish(answer);
|
|
317
318
|
}
|
|
318
319
|
|
|
@@ -323,7 +324,7 @@ export function judge({ root, base, head, body = '', redMain = [], rules = null,
|
|
|
323
324
|
// the base after the touch does not carry it (security review S1). A head job
|
|
324
325
|
// named like a check is never cleared: the owner's view of the gate could be
|
|
325
326
|
// that job's forgery (security review S1).
|
|
326
|
-
function signedRows(root, baseSha, headSha, { repo, pr, baseRef, texts }, answer) {
|
|
327
|
+
function signedRows(root, baseSha, headSha, { repo, pr, baseRef, texts }, answer, ruleRef = baseSha) {
|
|
327
328
|
if (!answer.rows.some((row) => row.kind === 'owner') || answer.rows.some((row) => row.kind === 'red')) return;
|
|
328
329
|
const note = (cause) => answer.rows.push({ kind: 'note', class: 'signed', cause });
|
|
329
330
|
if (texts === null) { note('the comments of the pull request could not be read, so no signed act counts'); return; }
|
|
@@ -336,7 +337,7 @@ function signedRows(root, baseSha, headSha, { repo, pr, baseRef, texts }, answer
|
|
|
336
337
|
let found = findAct(verified, { kind: 'merge', repo, pr: Number(pr), base: baseRef, merge_base: answer.merge_base, head: headSha });
|
|
337
338
|
let carried = null;
|
|
338
339
|
if (!found) {
|
|
339
|
-
carried = carryAct(root, baseSha, headSha, verified, { repo, pr, baseRef }, answer);
|
|
340
|
+
carried = carryAct(root, baseSha, headSha, verified, { repo, pr, baseRef, ruleRef }, answer);
|
|
340
341
|
if (carried?.found) found = carried.found;
|
|
341
342
|
}
|
|
342
343
|
if (!found) {
|
|
@@ -348,8 +349,9 @@ function signedRows(root, baseSha, headSha, { repo, pr, baseRef, texts }, answer
|
|
|
348
349
|
const locked = answer.rows.filter((row) => row.kind === 'owner' && row.class === 'gate-checks');
|
|
349
350
|
if (locked.length) { note('a signed act never clears a head job named like a check: remove that job'); return; }
|
|
350
351
|
for (const row of answer.rows) if (row.kind === 'owner') row.kind = 'signed';
|
|
351
|
-
answer.signed = { key: found.key, signed_at: found.signed_at, verified: found.verified, ...(carried?.found ? { carried_from: found.act.head } : {}) };
|
|
352
|
-
if (carried?.found) note(`the owner signed head ${found.act.head.slice(0, 7)} with key ${found.key} at ${found.signed_at}; this head only adds ${carried.merges}
|
|
352
|
+
answer.signed = { key: found.key, signed_at: found.signed_at, verified: found.verified, ...(carried?.found ? { carried_from: found.act.head } : {}), ...(carried?.pages?.length ? { pages: carried.pages, pages_checks: carried.checks } : {}) };
|
|
353
|
+
if (carried?.found && carried.pages?.length) note(`the owner signed head ${found.act.head.slice(0, 7)} with key ${found.key} at ${found.signed_at}; this head only adds ${carried.merges} merge(s) of the base and rebuilds ${carried.pages.length} generated page(s), which the required check(s) ${carried.checks.join(', ')} prove`);
|
|
354
|
+
else if (carried?.found) note(`the owner signed head ${found.act.head.slice(0, 7)} with key ${found.key} at ${found.signed_at}; this head only adds ${carried.merges} clean merge(s) of the base`);
|
|
353
355
|
else note(`the owner signed this head with key ${found.key} at ${found.signed_at}`);
|
|
354
356
|
}
|
|
355
357
|
|
|
@@ -377,7 +379,7 @@ const contains = (root, older, newer) => spawnSync('git', [MERGE_BASE, '--is-anc
|
|
|
377
379
|
// 5. Only the newest touch on this pull request carries, so a rollback to a
|
|
378
380
|
// head that the owner moved past does not clear.
|
|
379
381
|
// It fails closed: a commit or a merge that git cannot read stops the carry.
|
|
380
|
-
function carryAct(root, baseSha, headSha, verified, { repo, pr, baseRef }, answer) {
|
|
382
|
+
function carryAct(root, baseSha, headSha, verified, { repo, pr, baseRef, ruleRef = baseSha }, answer) {
|
|
381
383
|
const same = (a, b) => String(a).toLowerCase() === String(b).toLowerCase();
|
|
382
384
|
// Every valid merge act of this pull request, on any base branch and head.
|
|
383
385
|
const mine = [];
|
|
@@ -398,10 +400,13 @@ function carryAct(root, baseSha, headSha, verified, { repo, pr, baseRef }, answe
|
|
|
398
400
|
}).slice(0, CARRY_TRIES);
|
|
399
401
|
if (!candidates.length) return { why: 'the newest signed act of this pull request names another base branch or head' };
|
|
400
402
|
if (answer.rows.some((row) => row.kind === 'owner' && row.class === 'brake')) return { why: 'the brake is on, so the owner signs this head' };
|
|
403
|
+
const pages = generatedAt(root, baseSha);
|
|
404
|
+
for (const cause of pages.notes) answer.rows.push({ kind: 'note', class: 'signed', cause });
|
|
405
|
+
const built = pages.entries.length ? { entries: pages.entries, rules: rulesAt({ root, ref: ruleRef }).rules } : null;
|
|
401
406
|
let why = null;
|
|
402
407
|
for (const one of candidates) {
|
|
403
|
-
const result = carryOne(root, baseSha, headSha, answer.merge_base, one.act);
|
|
404
|
-
if (result.
|
|
408
|
+
const result = carryOne(root, baseSha, headSha, answer.merge_base, one.act, built);
|
|
409
|
+
if (result.ok) return { found: one, merges: result.merges, pages: result.pages, checks: result.checks };
|
|
405
410
|
why ??= result.why;
|
|
406
411
|
}
|
|
407
412
|
return { why: `${why} (${candidates.length} signed head(s) tried)` };
|
|
@@ -411,7 +416,7 @@ function carryAct(root, baseSha, headSha, verified, { repo, pr, baseRef }, answe
|
|
|
411
416
|
// gate makes depends on the commits alone.
|
|
412
417
|
const PINNED = { ...process.env, GIT_NO_REPLACE_OBJECTS: '1', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' };
|
|
413
418
|
|
|
414
|
-
function carryOne(root, baseSha, headSha, mergeBase, act) {
|
|
419
|
+
function carryOne(root, baseSha, headSha, mergeBase, act, built = null) {
|
|
415
420
|
const old = commitOf(root, act.head);
|
|
416
421
|
if (old !== act.head) return { why: `the signed head ${act.head.slice(0, 7)} is not a commit in this clone` };
|
|
417
422
|
if (commitOf(root, act.merge_base) !== act.merge_base) return { why: `the signed merge base ${act.merge_base.slice(0, 7)} is not a commit in this clone` };
|
|
@@ -424,15 +429,97 @@ function carryOne(root, baseSha, headSha, mergeBase, act) {
|
|
|
424
429
|
// commits after the signed merge base. A merge may bring in only one of
|
|
425
430
|
// them, never a middle commit of a merged pull request (finding 3).
|
|
426
431
|
const tips = new Set((git(root, ['rev-list', '--first-parent', baseSha, `^${act.merge_base}`], { allowFail: true }) ?? '').split('\n').filter(Boolean));
|
|
432
|
+
const pages = new Set();
|
|
433
|
+
const checks = new Set();
|
|
434
|
+
let merges = 0;
|
|
427
435
|
for (const sha of shas) {
|
|
428
|
-
const [tree, ...parents] = (git(root, ['show', '-s', '--format=%T %P', sha], { allowFail: true }) ?? '').trim().split(' ');
|
|
436
|
+
const [tree, ...parents] = (git(root, ['show', '-s', '--format=%T %P', sha], { allowFail: true, env: PINNED }) ?? '').trim().split(' ');
|
|
437
|
+
// Item 4ee7cab3: a commit on the branch that only rebuilds generated pages.
|
|
438
|
+
// No check names its parent. H0 comes into this head only down the first
|
|
439
|
+
// parents, because each second parent is a tip of the base, so the parent
|
|
440
|
+
// is H0 or a new commit (security review, finding 5).
|
|
441
|
+
if (parents.length === 1 && built) {
|
|
442
|
+
const off = offPages(root, `${parents[0]}^{tree}`, tree, built, pages, checks);
|
|
443
|
+
if (off) return { why: `commit ${sha.slice(0, 7)} is not a merge of two parents, and ${off}` };
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
429
446
|
if (parents.length !== 2) return { why: `commit ${sha.slice(0, 7)} is not a merge of two parents` };
|
|
430
447
|
if (!tips.has(parents[1])) return { why: `merge ${sha.slice(0, 7)} brings in ${parents[1].slice(0, 7)}, which was never a tip of the base after the touch` };
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
448
|
+
merges += 1;
|
|
449
|
+
const made = spawnSync('git', ['-c', 'merge.renames=true', '-c', 'merge.conflictStyle=merge', 'merge-tree', '--write-tree', '-z', '--name-only', '--no-messages', '--end-of-options', parents[0], parents[1]], { cwd: root, encoding: 'utf8', env: PINNED, timeout: 30_000 });
|
|
450
|
+
// 0 is a clean merge, 1 a merge with a conflict; anything else fails closed.
|
|
451
|
+
if (made.status !== 0 && made.status !== 1) return { why: `git cannot merge the parents of ${sha.slice(0, 7)}` };
|
|
452
|
+
const [madeTree, ...conflicted] = made.stdout.split('\0').filter(Boolean);
|
|
453
|
+
if (made.status === 0 && madeTree === tree) continue;
|
|
454
|
+
if (!built) return { why: made.status === 1 ? `merge ${sha.slice(0, 7)} has a conflict, so its fix needs a new touch` : `merge ${sha.slice(0, 7)} changes more than a clean merge of its parents` };
|
|
455
|
+
// A conflict counts only in a generated page, and the merge may differ
|
|
456
|
+
// from the tree that git makes only in generated pages.
|
|
457
|
+
const outside = conflicted.find((path) => !pageOf(built, path));
|
|
458
|
+
if (outside) return { why: `merge ${sha.slice(0, 7)} has a conflict in ${outside}, which no generated entry names, so its fix needs a new touch` };
|
|
459
|
+
const off = offPages(root, madeTree, tree, built, pages, checks);
|
|
460
|
+
if (off) return { why: `merge ${sha.slice(0, 7)} changes more than a clean merge of its parents: ${off}` };
|
|
461
|
+
}
|
|
462
|
+
return { ok: true, merges, pages: [...pages].sort(), checks: [...checks].sort() };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ------------------------------------------------------------------ generated pages
|
|
466
|
+
|
|
467
|
+
// Tracker item 4ee7cab3. A repo can name pages that a script builds from its
|
|
468
|
+
// code, and the required check that proves each page equals the build:
|
|
469
|
+
//
|
|
470
|
+
// generated:
|
|
471
|
+
// - path: docs/manual/reference/
|
|
472
|
+
// check: tests · unit
|
|
473
|
+
// why: node docs/manual/extract/run.mjs --check
|
|
474
|
+
//
|
|
475
|
+
// The list comes from the base, never from the head. An entry counts only
|
|
476
|
+
// when its check is a required check of the base and its path is a plain
|
|
477
|
+
// path that is not the repo root, .github/ or atlas.yaml. The gate never
|
|
478
|
+
// builds a page: nothing of the head runs. It only proves that a new commit
|
|
479
|
+
// changes a generated page and nothing else, and it names the check that
|
|
480
|
+
// proves the page. A merge needs every required check green, so a page that
|
|
481
|
+
// differs from the build stops the merge in that check.
|
|
482
|
+
const PAGE_PATH = /^(?!\.github(?:\/|$))(?!atlas\.yaml$)(?!atlas\/)[A-Za-z0-9_][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_][A-Za-z0-9_.-]*)*\/?$/;
|
|
483
|
+
export function generatedAt(root, ref) {
|
|
484
|
+
let facts;
|
|
485
|
+
try { facts = parseYaml(textAt(root, ref, 'atlas.yaml') ?? '') ?? {}; } catch { return { entries: [], notes: ['atlas.yaml on the base cannot be read, so no generated page counts'] }; }
|
|
486
|
+
if (facts.generated === undefined || facts.generated === null) return { entries: [], notes: [] };
|
|
487
|
+
if (!Array.isArray(facts.generated)) return { entries: [], notes: ['atlas.yaml generated is not a list, so no generated page counts'] };
|
|
488
|
+
const required = new Set(Array.isArray(facts.required_checks) ? facts.required_checks.map(String) : []);
|
|
489
|
+
const entries = [];
|
|
490
|
+
const notes = [];
|
|
491
|
+
facts.generated.forEach((entry, index) => {
|
|
492
|
+
const path = entry && typeof entry === 'object' ? entry.path : null;
|
|
493
|
+
const check = entry && typeof entry === 'object' ? entry.check : null;
|
|
494
|
+
if (typeof path !== 'string' || !PAGE_PATH.test(path) || path.split('/').some((part) => part === '..' || part === '.')) notes.push(`atlas.yaml generated entry ${index + 1} does not count: its path is not a plain path`);
|
|
495
|
+
else if (typeof check !== 'string' || !required.has(check)) notes.push(`atlas.yaml generated entry ${index + 1} does not count: its check is not a required check of the base`);
|
|
496
|
+
else entries.push({ path, check });
|
|
497
|
+
});
|
|
498
|
+
return { entries, notes };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const pageOf = (built, path) => built.entries.find((entry) => (entry.path.endsWith('/') ? path.startsWith(entry.path) : path === entry.path)) ?? null;
|
|
502
|
+
|
|
503
|
+
// Null when tree b differs from tree a only in generated pages that both trees
|
|
504
|
+
// hold as plain files, which the owner-only list does not name. A page that
|
|
505
|
+
// is added, removed or made a link counts as a change the owner did not see.
|
|
506
|
+
// Otherwise the first reason.
|
|
507
|
+
function offPages(root, a, b, built, pages, checks) {
|
|
508
|
+
const out = git(root, ['diff-tree', '-r', '-z', '--no-renames', '--no-textconv', '--no-ext-diff', a, b], { allowFail: true, env: PINNED });
|
|
509
|
+
if (out === null) return 'git cannot compare its trees';
|
|
510
|
+
const parts = out.split('\0');
|
|
511
|
+
const found = [];
|
|
512
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
513
|
+
const [oldMode, newMode, , , status] = parts[i].replace(/^:/, '').split(' ');
|
|
514
|
+
const path = parts[i + 1];
|
|
515
|
+
const entry = pageOf(built, path);
|
|
516
|
+
if (!entry) return `it changes ${path}, which no generated entry names`;
|
|
517
|
+
if (status !== 'M' || oldMode !== '100644' || newMode !== '100644') return `it adds, removes or retypes the generated page ${path}`;
|
|
518
|
+
if (onList(built.rules, path)) return `the generated page ${path} is on the owner-only list`;
|
|
519
|
+
found.push({ path, check: entry.check });
|
|
434
520
|
}
|
|
435
|
-
|
|
521
|
+
for (const one of found) { pages.add(one.path); checks.add(one.check); }
|
|
522
|
+
return null;
|
|
436
523
|
}
|
|
437
524
|
|
|
438
525
|
// Line 1: while the brake is on, every pull request of the repo comes to the
|
package/door/lib/gate.mjs
CHANGED
|
@@ -426,7 +426,10 @@ export function gateCopyRefusals(gh, repo, pull) {
|
|
|
426
426
|
export function pinnedRuns(gh, repo, { pr, sha, baseRef, head, checks, homesRef = baseRef, audit = false }) {
|
|
427
427
|
const homes = homesAt(gh, repo, homesRef, checks);
|
|
428
428
|
const sides = sidesOf(gh, repo, head, baseRef);
|
|
429
|
-
|
|
429
|
+
// The audit keeps the base-change pin: a merged pull request still lists
|
|
430
|
+
// its events, and a run made while it pointed at a side branch used that
|
|
431
|
+
// branch's file (security review of item 4ee7cab3, finding 1).
|
|
432
|
+
const baseChanged = baseChangedOf(gh, repo, pr);
|
|
430
433
|
const runOf = suiteRuns(gh, repo);
|
|
431
434
|
const kept = [];
|
|
432
435
|
const refused = [];
|
package/owner-only.yaml
CHANGED
package/package.json
CHANGED
package/shape/check.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export const sha256 = (text) => createHash('sha256').update(String(text ?? ''),
|
|
|
38
38
|
// Whether a copy is current or behind is not decided here. CI has no package
|
|
39
39
|
// and no network. The session verb decides it, from the release list that
|
|
40
40
|
// ships beside this file in the package, shape/releases.json.
|
|
41
|
-
export const CHECK_VERSION =
|
|
41
|
+
export const CHECK_VERSION = 9;
|
|
42
42
|
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
44
|
// The spec. One object per version. The check reads a version it has here and
|
|
@@ -1550,6 +1550,33 @@ export function counts(files) {
|
|
|
1550
1550
|
// check version 6 five cases switched the rule off and printed a warning that
|
|
1551
1551
|
// a person merges the file. Nothing switches it off now. The base copy scores,
|
|
1552
1552
|
// and the base kind and spec score both trees, so no case needs to.
|
|
1553
|
+
// A head whose atlas.yaml names a spec that this copy of the check cannot
|
|
1554
|
+
// read. A head with no spec, or with an atlas.yaml that does not read, is not
|
|
1555
|
+
// one: those stay errors.
|
|
1556
|
+
export function headSpecUnread(facts) {
|
|
1557
|
+
const spec = facts?.spec;
|
|
1558
|
+
return spec !== undefined && spec !== null && spec !== '' && !Object.hasOwn(SPECS, String(spec));
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// The head of a move to a newer spec: no part is scored, but every file the
|
|
1562
|
+
// newer spec can read for facts is swept for secrets. That is atlas.yaml, each
|
|
1563
|
+
// file under atlas/, and each facts file that atlas.yaml points to by a plain
|
|
1564
|
+
// path. The trust checks of the pattern list and the owner_only checks run as
|
|
1565
|
+
// in scan, so a newer spec cannot turn either of them from red into owner.
|
|
1566
|
+
export function sweepHead({ root, repoId = null, secretPatterns: patterns = secretPatterns() }) {
|
|
1567
|
+
const { facts } = readFacts(root);
|
|
1568
|
+
const errors = [...ownerOnlyErrors(facts)];
|
|
1569
|
+
const usable = Array.isArray(patterns) && patterns.every((pattern) => pattern instanceof RegExp) ? patterns : [];
|
|
1570
|
+
if (!usable.length) errors.push('the secret pattern list is empty or is not a list of patterns, so a credential value could not be caught; this check cannot be trusted');
|
|
1571
|
+
const missed = canaryMisses();
|
|
1572
|
+
if (missed.length) errors.push(`${missed.length} secret pattern(s) no longer catch their own canary: ${missed.join(', ')}; this check cannot be trusted`);
|
|
1573
|
+
const context = { root, secretPatterns: usable, errors };
|
|
1574
|
+
const files = facts?.files && typeof facts.files === 'object' && !Array.isArray(facts.files) ? facts.files : {};
|
|
1575
|
+
const pointed = Object.values(files).filter((path) => plainPath(path) && isFile(root, path));
|
|
1576
|
+
for (const path of new Set(['atlas.yaml', ...walk(root, 'atlas'), ...pointed])) secretIn(context, path, readText(root, path));
|
|
1577
|
+
return { ok: false, check_version: CHECK_VERSION, repo: repoId, kind: null, spec: facts?.spec ?? null, swept_only: true, files: [], by_reference: [], counts: counts([]), errors };
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1553
1580
|
export const OWNER_CASES = Object.freeze(['check-file', 'workflow', 'kind', 'spec', 'moved', 'first-run']);
|
|
1554
1581
|
|
|
1555
1582
|
const fileHash = (root, path) => { const text = readText(root, path); return text === null ? null : sha256(text); };
|
|
@@ -1633,8 +1660,9 @@ export function noLoss({ headRoot, baseRoot, baseSha = null, baseCommit = null,
|
|
|
1633
1660
|
if (String(out.head_facts.kind) !== String(scoreAs.kind)) {
|
|
1634
1661
|
owner('kind', `atlas.yaml names kind "${scoreAs.kind}" at the base and "${out.head_facts.kind}" at the head; this pull request changes which parts the repo needs`);
|
|
1635
1662
|
}
|
|
1663
|
+
const unread = Object.hasOwn(SPECS, String(scoreAs.spec)) && headSpecUnread(headRead.facts);
|
|
1636
1664
|
if (String(out.head_facts.spec) !== String(scoreAs.spec)) {
|
|
1637
|
-
owner('spec', `atlas.yaml names spec "${scoreAs.spec}" at the base and "${out.head_facts.spec}" at the head; this pull request changes what the spec version means`);
|
|
1665
|
+
owner('spec', `atlas.yaml names spec "${scoreAs.spec}" at the base and "${out.head_facts.spec}" at the head; this pull request changes what the spec version means${unread ? `; this copy of the check, check version ${CHECK_VERSION}, cannot read spec "${out.head_facts.spec}", so it did not score the head and only swept it for secrets` : ''}`);
|
|
1638
1666
|
}
|
|
1639
1667
|
|
|
1640
1668
|
// 3. Does the diff change the gate itself? The base copy judges, so the
|
|
@@ -1645,6 +1673,12 @@ export function noLoss({ headRoot, baseRoot, baseSha = null, baseCommit = null,
|
|
|
1645
1673
|
}
|
|
1646
1674
|
}
|
|
1647
1675
|
|
|
1676
|
+
// A head on a spec this copy cannot read is a move to a newer spec. This
|
|
1677
|
+
// copy has no yardstick for the head, so it compares nothing. The spec row
|
|
1678
|
+
// above sends it to the owner, and the gate still sweeps the head for
|
|
1679
|
+
// secrets (roadmap R62: the base copy judged the move to spec 3 red).
|
|
1680
|
+
if (unread) return out;
|
|
1681
|
+
|
|
1648
1682
|
// 4. Score both trees, by the same yardstick, and compare part by part. The
|
|
1649
1683
|
// head is scored twice: at its own pointers, and at the base's.
|
|
1650
1684
|
const base = scan({ root: baseRoot, repoId, secretPatterns: patterns, scoreAs });
|
|
@@ -1988,7 +2022,9 @@ export function gateFromGit({ git, baseCommit, headCommit, repoId = null, trees
|
|
|
1988
2022
|
}
|
|
1989
2023
|
mkdirSync(baseRoot, { recursive: true });
|
|
1990
2024
|
mkdirSync(headRoot, { recursive: true });
|
|
1991
|
-
const
|
|
2025
|
+
const baseFacts = readFacts(baseRoot).facts;
|
|
2026
|
+
const swept = baseFacts && Object.hasOwn(SPECS, String(baseFacts.spec)) && headSpecUnread(readFacts(headRoot).facts);
|
|
2027
|
+
const report = swept ? sweepHead({ root: headRoot, repoId, secretPatterns: patterns }) : scan({ root: headRoot, repoId, secretPatterns: patterns });
|
|
1992
2028
|
const state = noLoss({ headRoot, baseRoot, baseCommit: mergeBase, repoId, secretPatterns: patterns });
|
|
1993
2029
|
// A link or a submodule that the head adds or changes is never read, so the
|
|
1994
2030
|
// check cannot judge it. The owner reads it.
|
package/shape/releases.json
CHANGED
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
"check_version": 8,
|
|
48
48
|
"sha256": "8cf521f6e8f89ae65a257c521c583281e0c3c27ef968b4bd96eb336a8d095795",
|
|
49
49
|
"workflow_sha256": "fcddebef030e6564f97e1a5fb81c8ad1b3084ebef9622167c6565698312dcdd7"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"check_version": 9,
|
|
53
|
+
"sha256": "87896abfc09afd22724cfa645bbb5ef029d83887caf70f38b88d00b21cddef02",
|
|
54
|
+
"workflow_sha256": "fcddebef030e6564f97e1a5fb81c8ad1b3084ebef9622167c6565698312dcdd7"
|
|
50
55
|
}
|
|
51
56
|
]
|
|
52
57
|
}
|