@arjunkhera/atlas 0.4.18 → 0.4.19
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/cli.mjs +6 -1
- package/door/lib/gate.mjs +34 -3
- package/package.json +1 -1
- package/skills/lead/SKILL.md +5 -0
package/door/cli.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import { changedCommand as testsChanged } from './lib/changed-tests.mjs';
|
|
|
38
38
|
import { beforeCommand as testsBefore } from './lib/fails-before.mjs';
|
|
39
39
|
import { afterCommand as testsAfter } from './lib/passes-after.mjs';
|
|
40
40
|
import { upkeepCommand } from './lib/upkeep.mjs';
|
|
41
|
-
import { gateCommand } from './lib/gate.mjs';
|
|
41
|
+
import { gateCommand, mergeList } from './lib/gate.mjs';
|
|
42
42
|
import { judgeCommand } from './lib/judge.mjs';
|
|
43
43
|
|
|
44
44
|
export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -290,6 +290,8 @@ function statusCommand(chosen) {
|
|
|
290
290
|
line(` ${row.why}`);
|
|
291
291
|
if (row.copy) line(` copy sha256 ${row.copy.sha256.slice(0, 12)}… check version ${row.copy.check_version ?? '?'}`);
|
|
292
292
|
if (row.packaged) line(` package sha256 ${row.packaged.sha256.slice(0, 12)}… check version ${row.packaged.check_version}${row.packaged.released ? '' : ' (not released)'}`);
|
|
293
|
+
const list = mergeList(row.root);
|
|
294
|
+
if (list) line(` ${list.line}`);
|
|
293
295
|
line('');
|
|
294
296
|
}
|
|
295
297
|
line(`Decided by the release list in Atlas ${ATLAS_VERSION}, never by git history.`);
|
|
@@ -461,6 +463,9 @@ function checkCommand(chosen) {
|
|
|
461
463
|
const report = scan({ root, repoId: chosen.repo ?? null });
|
|
462
464
|
const decision = verdict(report, chosen['fail-on'] ?? 'error');
|
|
463
465
|
process.stdout.write(toText(report));
|
|
466
|
+
// A report, never a reason for red: the list is an owner-only key.
|
|
467
|
+
const list = mergeList(root);
|
|
468
|
+
if (list && list.state !== 'listed') line(list.line);
|
|
464
469
|
line(`${decision.green ? 'GREEN' : 'RED'}: ${decision.why}`);
|
|
465
470
|
return decision.green ? 0 : 1;
|
|
466
471
|
}
|
package/door/lib/gate.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// The plan is built by pure functions, so a test reads it without GitHub. The
|
|
16
16
|
// gh calls sit in one place, behind `gh`, which a test replaces.
|
|
17
17
|
import { execFileSync } from 'node:child_process';
|
|
18
|
-
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync } from 'node:fs';
|
|
18
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, renameSync, readdirSync } from 'node:fs';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { dirname, join } from 'node:path';
|
|
21
21
|
import { childEnvironment } from '../../work/lib/child-env.mjs';
|
|
@@ -38,16 +38,40 @@ export function gateFacts(root) {
|
|
|
38
38
|
return factsOf(readFileSync(path, 'utf8'));
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// A repo with no list cannot merge through the door: ready reads the list,
|
|
42
|
+
// and the door never makes one up (item 3d675dc7).
|
|
43
|
+
export const NO_LIST = 'atlas.yaml lists no required_checks.';
|
|
44
|
+
export const LIST_FIX = 'Fix: add required_checks to atlas.yaml, with the names of the CI jobs that run on every pull request. The key is owner-only, so the owner merges that pull request by hand.';
|
|
45
|
+
|
|
41
46
|
function factsOf(text) {
|
|
42
47
|
const facts = parseYaml(text) ?? {};
|
|
43
48
|
const checks = facts.required_checks;
|
|
44
49
|
if (!Array.isArray(checks) || checks.length === 0 || checks.some((name) => typeof name !== 'string' || !name.trim())) {
|
|
45
|
-
throw new GateError(
|
|
50
|
+
throw Object.assign(new GateError(`${NO_LIST} Nothing was changed.`), { noList: true });
|
|
46
51
|
}
|
|
47
52
|
if (facts.up_to_date !== undefined && typeof facts.up_to_date !== 'boolean') throw new GateError('atlas.yaml up_to_date must be true or false. Nothing was changed.');
|
|
48
53
|
return { checks: [...checks], upToDate: facts.up_to_date === true };
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
// Can this repo merge through the door? The answer does not read the repo
|
|
57
|
+
// kind: each kind merges on the same list. `atlas status` and `atlas check`
|
|
58
|
+
// print it.
|
|
59
|
+
// listed atlas.yaml names the checks
|
|
60
|
+
// missing CI runs, but atlas.yaml names no checks
|
|
61
|
+
// no-ci no workflow runs, so no list can exist yet
|
|
62
|
+
export function mergeList(root) {
|
|
63
|
+
const path = join(root, 'atlas.yaml');
|
|
64
|
+
if (!existsSync(path)) return null;
|
|
65
|
+
let checks = null;
|
|
66
|
+
try { checks = factsOf(readFileSync(path, 'utf8')).checks; } catch (error) { if (!error.noList) return { state: 'missing', checks: [], line: `merge door: atlas.yaml cannot be read (${error.message}). ${LIST_FIX}` }; }
|
|
67
|
+
if (checks) return { state: 'listed', checks, line: `merge door: ${checks.length} required check(s) in atlas.yaml.` };
|
|
68
|
+
const flows = join(root, '.github', 'workflows');
|
|
69
|
+
const ci = existsSync(flows) && readdirSync(flows).some((name) => /\.ya?ml$/.test(name));
|
|
70
|
+
return ci
|
|
71
|
+
? { state: 'missing', checks: [], line: `merge door: ${NO_LIST} ${LIST_FIX}` }
|
|
72
|
+
: { state: 'no-ci', checks: [], line: 'merge door: no list, because no CI workflow runs here. Merges stay with the owner.' };
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
// The two ruleset bodies, as the GitHub rulesets API takes them.
|
|
52
76
|
export function rulesets({ checks, upToDate }, enforcement = 'active') {
|
|
53
77
|
return [
|
|
@@ -317,7 +341,14 @@ export function readyCommand({ repo, pr, answer }, line, gh = ghApi) {
|
|
|
317
341
|
if (answer && typeof pull?.head?.sha === 'string') answer.sha = pull.head.sha;
|
|
318
342
|
if (pull?.state !== 'open') { line(`atlas gate ready: pull request #${pr} is ${pull?.merged ? 'merged' : pull?.state ?? 'not found'}. NOT READY.`); return 1; }
|
|
319
343
|
const sha = pull.head.sha;
|
|
320
|
-
|
|
344
|
+
let checks;
|
|
345
|
+
try { checks = checksAt(gh, repo, pull.base.ref); } catch (error) {
|
|
346
|
+
if (!error.noList) throw error;
|
|
347
|
+
line(`atlas gate ready: #${pr} at ${sha.slice(0, 7)}: atlas.yaml lists no required_checks on ${pull.base.ref}.`);
|
|
348
|
+
line(` ${LIST_FIX}`);
|
|
349
|
+
line('NOT READY: do not merge. Until the list exists, merges in this repo stay with the owner.');
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
321
352
|
const { kept, refused } = pinnedRuns(gh, repo, { pr, sha, baseRef: pull.base.ref, head: pull.head, checks });
|
|
322
353
|
const rows = checkStates(checks, kept);
|
|
323
354
|
line(`atlas gate ready: #${pr} at ${sha.slice(0, 7)}, ${checks.length} required check(s) from atlas.yaml on ${pull.base.ref}.`);
|
package/package.json
CHANGED
package/skills/lead/SKILL.md
CHANGED
|
@@ -117,6 +117,11 @@ set. A test keeps the two lists equal.
|
|
|
117
117
|
A person merges every file that `atlas tooling` writes.
|
|
118
118
|
A person merges every file that `atlas tests write` writes.
|
|
119
119
|
|
|
120
|
+
When a repo joins Atlas, its first pull request lists `required_checks` in
|
|
121
|
+
`atlas.yaml`: the CI jobs that run on every pull request. The owner merges
|
|
122
|
+
that pull request by hand. With no list, the merge check says NOT READY,
|
|
123
|
+
and merges stay with the owner.
|
|
124
|
+
|
|
120
125
|
## 7. The first reply of a cloud session
|
|
121
126
|
|
|
122
127
|
A cloud session can start from an old setup and run an old Atlas. So in a
|