@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
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('atlas.yaml lists no required_checks. Nothing was changed.');
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
- const checks = checksAt(gh, repo, pull.base.ref);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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