@arjunkhera/atlas 0.4.17 → 0.4.18

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.17",
3
+ "version": "0.4.18",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
@@ -292,7 +292,9 @@ export function freshTree(top, tree, sha) {
292
292
  // commit has a fixed author, date and message, so it holds no history and names neither real commit.
293
293
  export function gitTree(tree) {
294
294
  const env = { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: tree, GIT_CONFIG_NOSYSTEM: '1', GIT_AUTHOR_NAME: 'atlas', GIT_AUTHOR_EMAIL: 'atlas@invalid', GIT_COMMITTER_NAME: 'atlas', GIT_COMMITTER_EMAIL: 'atlas@invalid', GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z', GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z' };
295
- for (const args of [['init', '-q', '-b', 'main'], ['add', '-A'], ['commit', '-q', '--no-verify', '--no-gpg-sign', '-m', 'tree']]) {
295
+ // No auto gc or background maintenance: it can still write in .git while
296
+ // the run removes the tree (ENOTEMPTY on atlas#223).
297
+ for (const args of [['init', '-q', '-b', 'main'], ['config', 'gc.auto', '0'], ['config', 'maintenance.auto', 'false'], ['add', '-A'], ['commit', '-q', '--no-verify', '--no-gpg-sign', '-m', 'tree']]) {
296
298
  const r = spawnSync('git', args, { cwd: tree, env, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
297
299
  if (r.status !== 0) throw new Error(`git ${args[0]} in the run tree failed: ${String(r.stderr).trim().split('\n')[0]}`);
298
300
  }
@@ -322,7 +324,7 @@ export function runSide({ top, changed, plans, side, work = join(tmpdir(), 'atla
322
324
  }
323
325
  }
324
326
  gitTree(tree);
325
- for (const dir of ['home', 'tmp', 'report']) { rmSync(join(work, dir), { recursive: true, force: true }); mkdirSync(join(work, dir), { recursive: true }); }
327
+ for (const dir of ['home', 'tmp', 'report']) { rmSync(join(work, dir), { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); mkdirSync(join(work, dir), { recursive: true }); }
326
328
  const command = commandOf(p.runner, p.path, report);
327
329
  if (python && p.runner === 'pytest' && spawnSync('python3', ['-B', '-c', 'import pytest'], { cwd: tree, env }).status !== 0) {
328
330
  out.files[p.path] = { command, cases: null, why: 'pytest is not installed', none: true };
@@ -26,7 +26,7 @@
26
26
  import { spawnSync } from 'node:child_process';
27
27
  import { readFileSync, appendFileSync } from 'node:fs';
28
28
  import { resolve } from 'node:path';
29
- import { classify, changedPaths, compile, matcher } from './owner-only.mjs';
29
+ import { classify, changedPaths, compile, matcher, fakeSecret } from './owner-only.mjs';
30
30
  import { listChanged } from './changed-tests.mjs';
31
31
  import { parseYaml, SECRET_PATTERNS } from '../../shape/check.mjs';
32
32
  import { brakeState, undoOf, tripText } from './brake.mjs';
@@ -101,7 +101,7 @@ export function secretRows(root, base, head, changes) {
101
101
  for (const added of addedLines(root, base, head, change.path)) {
102
102
  const found = patterns.find((pattern) => {
103
103
  const hit = pattern.regex.exec(added.text);
104
- return hit && !(pattern.id === 'url-credential' && EXAMPLE_HOST.test(hit[0]));
104
+ return hit && !fakeSecret(pattern.id, hit[0]) && !(pattern.id === 'url-credential' && EXAMPLE_HOST.test(hit[0]));
105
105
  });
106
106
  if (found) rows.push({ kind: 'red', class: 'secret', path: change.path, line: added.line, cause: `secret pattern ${found.id}` });
107
107
  }
package/door/lib/gate.mjs CHANGED
@@ -237,14 +237,12 @@ export function refusal(run, { name, pr, sha, baseRef, homes, sides = () => [] }
237
237
  // the side branch's file, so it does not count.
238
238
  if (pulls.some((each) => each.base?.ref !== baseRef)) return `also of a pull request into ${pulls.find((each) => each.base?.ref !== baseRef).base?.ref ?? 'another branch'}`;
239
239
  // GitHub builds that list when it is read, from the open pull requests of
240
- // the head branch. Once a side pull request closes, its gate run names only
241
- // this one, and its title can be set to match. So the gate also refuses a
242
- // head branch that ever had a pull request into another branch.
243
- if (name === GATE_CHECK) {
244
- const side = sides();
245
- if (side === null) return 'no head branch to check for side pull requests';
246
- if (side.length) return `the head branch also had #${side[0].number} into ${side[0].base?.ref ?? 'another branch'}`;
247
- }
240
+ // the head branch. Once a side pull request closes, its run names only this
241
+ // one. So no run counts when the head branch ever had a pull request into
242
+ // another branch (atlas#212 for the gate; item 043f8c00 for every check).
243
+ const side = sides();
244
+ if (side === null) return 'no head branch to check for side pull requests';
245
+ if (side.length) return `the head branch also had #${side[0].number} into ${side[0].base?.ref ?? 'another branch'}`;
248
246
  return null;
249
247
  }
250
248
 
@@ -268,7 +266,7 @@ export function jobRefusal(job, check, run) {
268
266
  function pinnedRuns(gh, repo, { pr, sha, baseRef, head, checks }) {
269
267
  const homes = homesAt(gh, repo, baseRef, checks);
270
268
  // Every pull request, open or closed, from the head branch into another
271
- // branch. Read once, and only when a gate run needs it.
269
+ // branch. Read once, when the first run needs it.
272
270
  let sideList;
273
271
  const sides = () => {
274
272
  if (sideList !== undefined) return sideList;
@@ -498,6 +498,38 @@ const newest = (paths) => paths.reduce((best, path) => (best === null || newer(p
498
498
 
499
499
  // `rules` names the commit whose list holds; the default is the base. The
500
500
  // gate replay passes today's main branch, to ask what today's list would do.
501
+ // Item b6e264a9, from the 30-day replay: the loose pattern "named-value"
502
+ // (token = ..., secret: ...) matched test code in most of its hits, such as
503
+ // a fake environment value. It does not count in a test file or under
504
+ // archive/. The patterns of a known shape still count there.
505
+ export const LOOSE_PATTERN = 'named-value';
506
+ // A spec/ folder is not on the list: a schema repo keeps product text there.
507
+ export const TEST_PATH = /(^|\/)(test|tests|__tests__)\/|\.(test|spec)\.[^/]+$|(^|\/)test_[^/]*\.py$|_test\.(py|go)$|^archive\//;
508
+ // A value that says it is fake, such as "sk-fake-model-key-...", is no key.
509
+ export const FAKE_VALUE = /fake|dummy|placeholder|example/i;
510
+ // The part of a hit that holds the value: the user and password of a URL
511
+ // credential, the text after ":" or "=" of a named value, else the whole
512
+ // token. The word test reads only that part, so a host such as
513
+ // db.examplecorp.io or a word after the value does not hide a real secret
514
+ // (verifier, atlas#221).
515
+ export function secretValue(id, text) {
516
+ if (id === 'url-credential') return text.slice(text.indexOf('://') + 3, text.lastIndexOf('@'));
517
+ if (id === LOOSE_PATTERN) return (/[:=]\s*(.*)$/s.exec(text)?.[1] ?? text).replace(/["',;})\]].*$/s, '');
518
+ return text;
519
+ }
520
+ export const fakeSecret = (id, text) => FAKE_VALUE.test(secretValue(id, text));
521
+ // The first secret pattern that one added line of this path shows, or null.
522
+ export function secretIn(patterns, path, lines) {
523
+ for (const pattern of patterns) {
524
+ if (pattern.id === LOOSE_PATTERN && TEST_PATH.test(path)) continue;
525
+ for (const line of lines) {
526
+ const hit = pattern.regex.exec(line);
527
+ if (hit && !fakeSecret(pattern.id, hit[0])) return { pattern, hit };
528
+ }
529
+ }
530
+ return null;
531
+ }
532
+
501
533
  export function classify({ root, base, head, rules: rulesRef = null, classes = loadClasses() }) {
502
534
  const { rules, product, tree } = rulesAt({ root, ref: rulesRef ?? base, classes });
503
535
  const baseTree = rulesRef ? treeAt(root, base) : tree;
@@ -544,7 +576,7 @@ export function classify({ root, base, head, rules: rulesRef = null, classes = l
544
576
  if (kitRelease && sign.version_key) continue;
545
577
  if (sign.type === 'path' || sign.type === 'any_change') hits.push([sign.class, cause]);
546
578
  else if (sign.type === 'secret') {
547
- const found = secrets.find((pattern) => changesOf().added.some((line) => pattern.regex.test(line)));
579
+ const found = secretIn(secrets, path, changesOf().added)?.pattern;
548
580
  if (found) hits.push([sign.class, `${cause} (${found.id})`]);
549
581
  } else if (sign.type === 'added_line') {
550
582
  const regex = compile(sign.pattern);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.4.17",
3
+ "version": "0.4.18",
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",