mandrel-platform 0.21.0 → 0.25.0

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.
@@ -11,6 +11,8 @@ import {
11
11
  probeUrl,
12
12
  runSmoke,
13
13
  resolveFailedFile,
14
+ defaultDeriveSubdomain,
15
+ writeRollbackState,
14
16
  } from "./deploy-boot-smoke.mjs";
15
17
 
16
18
  // ---------------------------------------------------------------------------
@@ -29,26 +31,67 @@ test("parseSmokePaths enforces a leading slash", () => {
29
31
  });
30
32
 
31
33
  // ---------------------------------------------------------------------------
32
- // extractSubdomain
34
+ // extractSubdomain — parses the Cloudflare REST subdomain response (M14)
33
35
  // ---------------------------------------------------------------------------
34
36
 
35
- test("extractSubdomain finds the first workers.dev slug in whoami output", () => {
36
- const whoami = [
37
- "Getting User settings...",
38
- "👋 You are logged in!",
39
- "┌──────────────┬──────────────────────────┐",
40
- "│ Account Name │ dsj1984's Account │",
41
- "│ Subdomain │ dsj1984.workers.dev │",
42
- "└──────────────┴──────────────────────────┘",
43
- ].join("\n");
44
- assert.equal(extractSubdomain(whoami), "dsj1984");
37
+ test("extractSubdomain reads result.name from the REST subdomain response", () => {
38
+ assert.equal(
39
+ extractSubdomain('{"success":true,"errors":[],"messages":[],"result":{"name":"dsj1984"}}'),
40
+ "dsj1984"
41
+ );
42
+ });
43
+
44
+ test("extractSubdomain strips a trailing .workers.dev when the API echoes the host", () => {
45
+ assert.equal(extractSubdomain('{"success":true,"result":{"name":"dsj1984.workers.dev"}}'), "dsj1984");
45
46
  });
46
47
 
47
- test("extractSubdomain returns null when no slug is present", () => {
48
- assert.equal(extractSubdomain("no subdomain here"), null);
48
+ test("extractSubdomain returns null for unsuccessful, malformed, or missing bodies", () => {
49
+ assert.equal(extractSubdomain('{"success":false,"result":null}'), null);
50
+ assert.equal(extractSubdomain('{"result":{"name":""}}'), null);
51
+ assert.equal(extractSubdomain('{"result":{"name":42}}'), null);
52
+ assert.equal(extractSubdomain('{"result":{}}'), null);
53
+ assert.equal(extractSubdomain('{"result":"dsj1984"}'), null);
54
+ assert.equal(extractSubdomain("<html>not json</html>"), null);
49
55
  assert.equal(extractSubdomain(""), null);
50
56
  });
51
57
 
58
+ // ---------------------------------------------------------------------------
59
+ // defaultDeriveSubdomain — the REST GET /accounts/{id}/workers/subdomain call
60
+ // ---------------------------------------------------------------------------
61
+
62
+ test("defaultDeriveSubdomain calls the workers/subdomain endpoint with a bearer token", async () => {
63
+ let seenUrl;
64
+ let seenOptions;
65
+ const fetchImpl = async (url, options) => {
66
+ seenUrl = url;
67
+ seenOptions = options;
68
+ return { ok: true, text: async () => '{"success":true,"result":{"name":"dsj1984"}}' };
69
+ };
70
+ const slug = await defaultDeriveSubdomain({ accountId: "acct-123", apiToken: "tok-abc" }, fetchImpl);
71
+ assert.equal(slug, "dsj1984");
72
+ assert.equal(
73
+ seenUrl,
74
+ "https://api.cloudflare.com/client/v4/accounts/acct-123/workers/subdomain"
75
+ );
76
+ assert.equal(seenOptions.method, "GET");
77
+ assert.equal(seenOptions.headers.Authorization, "Bearer tok-abc");
78
+ });
79
+
80
+ test("defaultDeriveSubdomain returns null without creds, on non-2xx, and on network error", async () => {
81
+ assert.equal(await defaultDeriveSubdomain({ accountId: "", apiToken: "tok" }, async () => ({})), null);
82
+ assert.equal(await defaultDeriveSubdomain({ accountId: "a", apiToken: "" }, async () => ({})), null);
83
+ assert.equal(
84
+ await defaultDeriveSubdomain({ accountId: "a", apiToken: "t" }, async () => ({ ok: false, text: async () => "" })),
85
+ null
86
+ );
87
+ assert.equal(
88
+ await defaultDeriveSubdomain({ accountId: "a", apiToken: "t" }, async () => {
89
+ throw new Error("ECONNRESET");
90
+ }),
91
+ null
92
+ );
93
+ });
94
+
52
95
  // ---------------------------------------------------------------------------
53
96
  // parseVersionField — the jq/JSON.parse replacement for grep-for-"version"
54
97
  // ---------------------------------------------------------------------------
@@ -315,21 +358,30 @@ test("runSmoke fails without a rollback list when no subdomain is derivable", as
315
358
  const { log, lines } = collectLogs();
316
359
  const result = await runSmoke(
317
360
  { DEPLOYED_WORKERS: "api", SMOKE_PATHS: "/health" },
318
- { log, whoami: () => "not logged in", probe: async () => ({ status: 200, body: "{}" }) }
361
+ { log, deriveSubdomain: async () => null, probe: async () => ({ status: 200, body: "{}" }) }
319
362
  );
320
363
  assert.equal(result.exitCode, 1);
321
364
  assert.deepEqual(result.failedWorkers, []);
322
365
  assert.ok(lines.some((l) => l.includes("Could not derive workers.dev subdomain")));
323
366
  });
324
367
 
325
- test("runSmoke derives the subdomain from whoami output when not provided", async () => {
368
+ test("runSmoke derives the subdomain via the REST endpoint when not provided", async () => {
326
369
  const { log } = collectLogs();
327
370
  const probed = [];
371
+ let seenCreds;
328
372
  const result = await runSmoke(
329
- { DEPLOYED_WORKERS: "api", SMOKE_PATHS: "/health" },
373
+ {
374
+ DEPLOYED_WORKERS: "api",
375
+ SMOKE_PATHS: "/health",
376
+ CLOUDFLARE_ACCOUNT_ID: "acct-123",
377
+ CLOUDFLARE_API_TOKEN: "tok-abc",
378
+ },
330
379
  {
331
380
  log,
332
- whoami: () => "│ Subdomain │ dsj1984.workers.dev │",
381
+ deriveSubdomain: async (creds) => {
382
+ seenCreds = creds;
383
+ return "dsj1984";
384
+ },
333
385
  probe: async (url) => {
334
386
  probed.push(url);
335
387
  return { status: 200, body: "{}" };
@@ -338,6 +390,8 @@ test("runSmoke derives the subdomain from whoami output when not provided", asyn
338
390
  );
339
391
  assert.equal(result.exitCode, 0);
340
392
  assert.deepEqual(probed, ["https://api.dsj1984.workers.dev/health"]);
393
+ // The REST creds are threaded through from the environment.
394
+ assert.deepEqual(seenCreds, { accountId: "acct-123", apiToken: "tok-abc" });
341
395
  });
342
396
 
343
397
  // ---------------------------------------------------------------------------
@@ -379,3 +433,70 @@ test("resolveFailedFile creates a private temp dir when SMOKE_FAILED_FILE is uns
379
433
  assert.equal(path, "/var/folders/xyz/deploy-boot-smoke-Zzz999/smoke-failed-workers.txt");
380
434
  assert.notEqual(path, "/tmp/smoke-failed-workers.txt");
381
435
  });
436
+
437
+ // ---------------------------------------------------------------------------
438
+ // writeRollbackState — the shared terminal writer (crash-path fix, M2)
439
+ // ---------------------------------------------------------------------------
440
+
441
+ test("writeRollbackState writes the sorted list and the smoke_failed flag", () => {
442
+ const writes = [];
443
+ const appends = [];
444
+ writeRollbackState("/tmp/failed.txt", ["api", "worker-cron"], {
445
+ githubEnv: "/tmp/gh-env",
446
+ writeFile: (path, data) => writes.push({ path, data }),
447
+ appendFile: (path, data) => appends.push({ path, data }),
448
+ });
449
+ assert.deepEqual(writes, [{ path: "/tmp/failed.txt", data: "api\nworker-cron\n" }]);
450
+ assert.deepEqual(appends, [{ path: "/tmp/gh-env", data: "smoke_failed=true\n" }]);
451
+ });
452
+
453
+ test("writeRollbackState is a no-op for an empty worker list", () => {
454
+ let wrote = false;
455
+ let appended = false;
456
+ writeRollbackState("/tmp/failed.txt", [], {
457
+ githubEnv: "/tmp/gh-env",
458
+ writeFile: () => {
459
+ wrote = true;
460
+ },
461
+ appendFile: () => {
462
+ appended = true;
463
+ },
464
+ });
465
+ assert.equal(wrote, false);
466
+ assert.equal(appended, false);
467
+ });
468
+
469
+ test("writeRollbackState skips the GITHUB_ENV append when githubEnv is unset (still writes the list)", () => {
470
+ const writes = [];
471
+ let appended = false;
472
+ writeRollbackState("/tmp/failed.txt", ["api"], {
473
+ writeFile: (path, data) => writes.push({ path, data }),
474
+ appendFile: () => {
475
+ appended = true;
476
+ },
477
+ });
478
+ assert.deepEqual(writes, [{ path: "/tmp/failed.txt", data: "api\n" }]);
479
+ assert.equal(appended, false);
480
+ });
481
+
482
+ // ---------------------------------------------------------------------------
483
+ // Crash-path terminal write (M2): runSmoke throwing must still mark every
484
+ // deployed worker for rollback. main() catches and calls writeRollbackState
485
+ // with uniqueSorted(parseCsv(DEPLOYED_WORKERS)); this asserts the exact
486
+ // derivation main() feeds the writer on the crash path.
487
+ // ---------------------------------------------------------------------------
488
+
489
+ test("crash-path derives the full deployed-worker rollback set (uniqueSorted + parseCsv)", () => {
490
+ // Mirrors main()'s catch block: on an unhandled error, EVERY deployed
491
+ // worker is marked (no per-worker attribution survives a crash).
492
+ const deployed = uniqueSorted(parseCsv("worker-cron, api ,worker-cron"));
493
+ const writes = [];
494
+ const appends = [];
495
+ writeRollbackState("/tmp/failed.txt", deployed, {
496
+ githubEnv: "/tmp/gh-env",
497
+ writeFile: (path, data) => writes.push({ path, data }),
498
+ appendFile: (path, data) => appends.push({ path, data }),
499
+ });
500
+ assert.deepEqual(writes, [{ path: "/tmp/failed.txt", data: "api\nworker-cron\n" }]);
501
+ assert.deepEqual(appends, [{ path: "/tmp/gh-env", data: "smoke_failed=true\n" }]);
502
+ });
@@ -68,7 +68,12 @@ import { tmpdir } from "node:os";
68
68
  import { dirname, join, resolve } from "node:path";
69
69
  import { fileURLToPath } from "node:url";
70
70
 
71
- import { buildReport, isFullSha } from "./check-pin-drift.mjs";
71
+ import {
72
+ allConsumersErrored,
73
+ buildReport,
74
+ isFullSha,
75
+ pinDriftTokenProvided,
76
+ } from "./check-pin-drift.mjs";
72
77
  import { defaultGhRunner } from "./lib/gh-json.mjs";
73
78
  import { parseSemver } from "./lib/semver-duration.mjs";
74
79
 
@@ -602,6 +607,13 @@ export function runRepair({
602
607
  }) {
603
608
  // Reuse the detector to classify every consumer (single SSOT for drift).
604
609
  const report = buildReport(config, runGh, nowMs);
610
+ // M11: mirror check-pin-drift's dead-credential signal. When EVERY consumer
611
+ // row errored the detector could read no repo at all — the signature of a
612
+ // provided-but-dead PIN_DRIFT_TOKEN (an expired PAT). The caller pairs this
613
+ // with `tokenProvided` to fail the run instead of silently reporting a green
614
+ // "no repairable drift" (which is what an all-error sweep degrades to, since
615
+ // every error row classifies `repairable: false, reason: "error"`).
616
+ const allErrored = allConsumersErrored(report);
605
617
  const latestTag = report.latestRelease?.tag ?? null;
606
618
  const targetSha = report.latestRelease?.sha ?? null;
607
619
  // The pin target is the latest release tag (so the `# <ref>` annotation reads
@@ -648,7 +660,14 @@ export function runRepair({
648
660
  }
649
661
  }
650
662
 
651
- return { ref: effectiveRef, targetSha, dryRun, hasToken: Boolean(token), rows };
663
+ return {
664
+ ref: effectiveRef,
665
+ targetSha,
666
+ dryRun,
667
+ hasToken: Boolean(token),
668
+ allErrored,
669
+ rows,
670
+ };
652
671
  }
653
672
 
654
673
  // ---------------------------------------------------------------------------
@@ -743,6 +762,23 @@ export function runCli({
743
762
  }
744
763
  }
745
764
  }
765
+
766
+ // M11: the repair loop reads consumers with the SAME cross-repo credential the
767
+ // dashboard uses (PIN_DRIFT_TOKEN → GH_TOKEN). When that token was PROVIDED
768
+ // but every detector row errored, the credential is dead (expired PAT) rather
769
+ // than not-yet-provisioned — every consumer degraded to `error` / repairable
770
+ // false, which otherwise renders a reassuring green "no repairable drift". Fail
771
+ // the run loudly so the dead credential is fixed. The absent-token bootstrap
772
+ // (pinDriftTokenProvided false) keeps its exit-0 read-only behavior.
773
+ if (pinDriftTokenProvided(env) && report.allErrored) {
774
+ stderr.write(
775
+ "::error::[platform-repair] PIN_DRIFT_TOKEN was provided but every " +
776
+ "cross-repo consumer read errored — the credential is dead (likely an " +
777
+ "expired fine-grained PAT), not a not-yet-provisioned bootstrap. Rotate " +
778
+ "the token. See docs/runbooks/pin-drift-dashboard.md.\n",
779
+ );
780
+ return 1;
781
+ }
746
782
  return 0;
747
783
  }
748
784
 
@@ -21,6 +21,9 @@
21
21
  */
22
22
 
23
23
  import assert from "node:assert/strict";
24
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import { join } from "node:path";
24
27
  import { test } from "node:test";
25
28
 
26
29
  import {
@@ -32,6 +35,7 @@ import {
32
35
  parsePrNumberFromUrl,
33
36
  renderRepairPrBody,
34
37
  renderRepairReport,
38
+ runCli,
35
39
  runRepair,
36
40
  } from "./platform-repair.mjs";
37
41
 
@@ -269,6 +273,12 @@ function makeGh({ consumerWorkflow, npmVersion, openPrs = {}, calls }) {
269
273
 
270
274
  const noopGit = () => "";
271
275
 
276
+ /** A minimal write-sink that records everything written, for stdout/stderr. */
277
+ function capture() {
278
+ const chunks = [];
279
+ return { write: (s) => chunks.push(s), text: () => chunks.join("") };
280
+ }
281
+
272
282
  function laggingConfig() {
273
283
  return {
274
284
  platformRepo: PLATFORM_REPO,
@@ -456,3 +466,106 @@ test("renderRepairReport tabulates outcomes and a repaired section", () => {
456
466
  assert.ok(text.includes("Repaired (1)"));
457
467
  assert.ok(text.includes("#101"));
458
468
  });
469
+
470
+ // ---------------------------------------------------------------------------
471
+ // M11 — provided-but-dead read credential vs. not-yet-provisioned bootstrap.
472
+ // The repair loop reads consumers with the SAME cross-repo PAT the dashboard
473
+ // uses (PIN_DRIFT_TOKEN → GH_TOKEN). When that PAT is provided-but-dead every
474
+ // detector row errors → every consumer classifies `error`/repairable-false,
475
+ // which otherwise renders a reassuring green "no repairable drift". runCli must
476
+ // hard-fail on that when the token was provided, and stay exit-0 when it was
477
+ // absent (bootstrap). Mirrors scripts/check-runner-health.mjs error-row
478
+ // handling. (temp/audits/workflow-robustness-review-2026-07-05.md M11)
479
+ // ---------------------------------------------------------------------------
480
+
481
+ /**
482
+ * A gh runner where the platform's own release resolution succeeds but EVERY
483
+ * cross-repo consumer read fails non-404 (auth/transport) — the shape of a dead
484
+ * cross-repo PAT. No PR surface is reached because every consumer errors out
485
+ * before repair.
486
+ */
487
+ function makeAllConsumersFailGh() {
488
+ return (args) => {
489
+ const path = args[1];
490
+ if (args[0] !== "api") {
491
+ throw new Error(`unexpected non-api gh call under dead credential: ${args.join(" ")}`);
492
+ }
493
+ if (path === `repos/${PLATFORM_REPO}/releases/latest`) {
494
+ return JSON.stringify({ tag_name: "mandrel-platform-v1.2.3", published_at: "2020-01-01T00:00:00Z" });
495
+ }
496
+ if (path === `repos/${PLATFORM_REPO}/git/ref/tags/mandrel-platform-v1.2.3`) {
497
+ return JSON.stringify({ object: { sha: LATEST_SHA, type: "commit" } });
498
+ }
499
+ if (/\/contents\/\.github\/workflows/.test(path)) {
500
+ // 403 (not 404) → fail-closed error row, mirroring an expired PAT.
501
+ const err = new Error("gh: Forbidden (HTTP 403)");
502
+ err.stderr = "gh: Forbidden (HTTP 403)\n";
503
+ throw err;
504
+ }
505
+ if (/^repos\/[^/]+\/[^/]+$/.test(path)) {
506
+ return JSON.stringify({ default_branch: "main" });
507
+ }
508
+ throw new Error(`unexpected gh api path: ${path}`);
509
+ };
510
+ }
511
+
512
+ function deadCredConfigFile() {
513
+ const dir = mkdtempSync(join(tmpdir(), "platform-repair-dead-"));
514
+ const p = join(dir, "consumers.json");
515
+ writeFileSync(
516
+ p,
517
+ JSON.stringify({
518
+ platformRepo: PLATFORM_REPO,
519
+ consumers: [
520
+ { name: "domio", repo: "dsj1984/domio" },
521
+ { name: "athportal", repo: "dsj1984/athportal" },
522
+ ],
523
+ }),
524
+ );
525
+ return { dir, p };
526
+ }
527
+
528
+ test("runCli: PROVIDED-but-dead PIN_DRIFT_TOKEN (every consumer read errors) exits 1 with ::error::", () => {
529
+ const { dir, p } = deadCredConfigFile();
530
+ const stderr = capture();
531
+ try {
532
+ const code = runCli({
533
+ argv: ["--config", p],
534
+ env: { PIN_DRIFT_TOKEN: "ghp_expired" },
535
+ runGh: makeAllConsumersFailGh(),
536
+ runGit: noopGit,
537
+ runSync: () => {
538
+ throw new Error("must not sync under a dead credential");
539
+ },
540
+ stdout: capture(),
541
+ stderr,
542
+ summaryPath: undefined,
543
+ });
544
+ assert.equal(code, 1);
545
+ assert.match(stderr.text(), /::error::/);
546
+ assert.match(stderr.text(), /credential is dead/);
547
+ } finally {
548
+ rmSync(dir, { recursive: true, force: true });
549
+ }
550
+ });
551
+
552
+ test("runCli: ABSENT PIN_DRIFT_TOKEN bootstrap (every consumer read errors, token unset) stays exit 0", () => {
553
+ const { dir, p } = deadCredConfigFile();
554
+ try {
555
+ const code = runCli({
556
+ argv: ["--config", p],
557
+ env: {}, // PIN_DRIFT_TOKEN absent → not-yet-provisioned bootstrap.
558
+ runGh: makeAllConsumersFailGh(),
559
+ runGit: noopGit,
560
+ runSync: () => {
561
+ throw new Error("must not sync during bootstrap");
562
+ },
563
+ stdout: capture(),
564
+ stderr: capture(),
565
+ summaryPath: undefined,
566
+ });
567
+ assert.equal(code, 0);
568
+ } finally {
569
+ rmSync(dir, { recursive: true, force: true });
570
+ }
571
+ });
@@ -570,22 +570,49 @@ test("--check-ruleset requires --consumer-repo", () => {
570
570
  }, /consumer-repo/);
571
571
  });
572
572
 
573
- test("apply materializes the canonical deploy-staging.yml workflow caller template", () => {
573
+ test("apply materializes the canonical deploy-staging dispatcher + run templates (Story #272)", () => {
574
574
  const out = JSON.parse(run([]));
575
- const stub = join(consumer, ".github", "workflows", "deploy-staging.yml");
576
- assert.ok(existsSync(stub));
575
+ const dispatcher = join(consumer, ".github", "workflows", "deploy-staging.yml");
576
+ const runner = join(consumer, ".github", "workflows", "deploy-staging-run.yml");
577
+ assert.ok(existsSync(dispatcher), "deploy-staging.yml (dispatcher) materialized");
578
+ assert.ok(existsSync(runner), "deploy-staging-run.yml (deploy) materialized");
577
579
  assert.ok(
578
580
  out.workflowStubs.created.some((f) => f.endsWith("deploy-staging.yml")),
579
- "deploy-staging.yml reported as created"
581
+ "deploy-staging.yml (dispatcher) reported as created"
582
+ );
583
+ assert.ok(
584
+ out.workflowStubs.created.some((f) => f.endsWith("deploy-staging-run.yml")),
585
+ "deploy-staging-run.yml (deploy) reported as created"
586
+ );
587
+ const dispatcherBody = readFileSync(dispatcher, "utf8");
588
+ const runnerBody = readFileSync(runner, "utf8");
589
+ // Both halves carry the never-clobber template marker.
590
+ assert.ok(
591
+ dispatcherBody.includes("Canonical staging-deploy caller template"),
592
+ "dispatcher carries the template marker"
593
+ );
594
+ assert.ok(
595
+ runnerBody.includes("Canonical staging-deploy caller template"),
596
+ "run template carries the template marker"
597
+ );
598
+ // The dispatcher fires on CI-green (workflow_run) and DISPATCHES the run
599
+ // workflow — it must NOT call the deploy directly, since a workflow_run
600
+ // deploy skips every environment: job (Story #272).
601
+ assert.ok(
602
+ dispatcherBody.includes("workflow_run") &&
603
+ dispatcherBody.includes("gh workflow run deploy-staging-run.yml"),
604
+ "dispatcher fires on workflow_run and dispatches deploy-staging-run.yml"
580
605
  );
581
- const body = readFileSync(stub, "utf8");
582
606
  assert.ok(
583
- body.includes("Canonical staging-deploy caller template"),
584
- "materialized workflow carries the template marker"
607
+ !dispatcherBody.includes("dsj1984/mandrel-platform/.github/workflows/deploy-cloudflare.yml"),
608
+ "dispatcher does NOT uses: deploy-cloudflare.yml directly (that would skip environment: jobs on workflow_run)"
585
609
  );
610
+ // The deploy half runs on workflow_dispatch (where environment: jobs execute)
611
+ // and uses the shared reusable workflow.
586
612
  assert.ok(
587
- body.includes("dsj1984/mandrel-platform/.github/workflows/deploy-cloudflare.yml"),
588
- "template calls the shared deploy-cloudflare.yml reusable workflow"
613
+ runnerBody.includes("workflow_dispatch") &&
614
+ runnerBody.includes("dsj1984/mandrel-platform/.github/workflows/deploy-cloudflare.yml"),
615
+ "run template deploys on workflow_dispatch via the shared deploy-cloudflare.yml"
589
616
  );
590
617
  });
591
618
 
@@ -0,0 +1,20 @@
1
+ {
2
+ "$comment": "Data-driven expected roster for scripts/check-runner-health.mjs (Story #258). Each entry is one repo whose self-hosted runner fleet the scheduled runner-fleet-health.yml workflow monitors. Adding/removing a runner needs only an edit here — the checker reads GET /repos/{owner}/{repo}/actions/runners and compares live status against `expectedCount` + `labels`. All nine runners (including Beestera/swarm-os's three) are co-resident on one operator Mac (2026-07-03 runner audit, repo-ops matrix §1a); if that host sleeps, reboots, fills its disk, or a launchd service dies, every listed repo's CI silently queues with no alert until this monitor catches it.",
3
+ "$comment_swarm_os": "Beestera/swarm-os is deliberately NOT listed: no dsj1984-owned PAT can read another org's runner API (fine-grained PATs are bound to one resource owner; the Beestera org rejects classic PATs), so its row would permanently false-positive as 0/3 degraded. Host-level coverage is retained regardless — its runners share the Mac with the rows below, so a wedged host still trips domio/athportal. What is NOT covered: swarm-os's individual launchd services dying while the host stays healthy, and its stale-queue check. Re-add the entry if a Beestera-owned credential (fine-grained PAT or GitHub App) plus per-repo token support ever lands.",
4
+ "$comment_staleQueuedMinutes": "A queued/waiting workflow run older than this many minutes with no online runner matching its labels is flagged as a queue-staleness signal (optional per-repo override via `staleQueuedMinutes`).",
5
+ "defaultStaleQueuedMinutes": 20,
6
+ "repos": [
7
+ {
8
+ "name": "domio",
9
+ "repo": "dsj1984/domio",
10
+ "expectedCount": 3,
11
+ "labels": ["self-hosted", "macOS", "ARM64", "domio-runner"]
12
+ },
13
+ {
14
+ "name": "athportal",
15
+ "repo": "dsj1984/athportal",
16
+ "expectedCount": 3,
17
+ "labels": ["self-hosted", "macOS", "ARM64", "athportal-runner"]
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,169 @@
1
+ # Hash-pinned lockfile for the pr-quality SAST (semgrep) step.
2
+ #
3
+ # Target platform : Linux x86_64 / CPython 3.12 (GitHub Actions ubuntu-latest)
4
+ # Tool : semgrep 1.97.0 (+ complete transitive closure)
5
+ # Consumed via : pip install --require-hashes -r scripts/semgrep-requirements.txt
6
+ #
7
+ # Every requirement below is pinned with `==` and carries at least one
8
+ # sha256 hash, as `--require-hashes` demands. The closure was resolved for
9
+ # the linux/cp312 target specifically (via `pip download` with explicit
10
+ # --platform manylinux/musllinux + --python-version 3.12 --abi cp312), NOT
11
+ # from the local interpreter, so the native wheels (semgrep, protobuf,
12
+ # rpds-py, wrapt, charset-normalizer, ruamel.yaml.clib) are the linux x86_64
13
+ # cp312-compatible artifacts.
14
+ #
15
+ # setuptools is intentionally included: the workflow installs it alongside
16
+ # semgrep so opentelemetry's transitive `pkg_resources` import works on
17
+ # Python >=3.12 (which no longer ships setuptools by default).
18
+ #
19
+ # Regenerate with `pip download semgrep==<ver> setuptools --only-binary=:all:
20
+ # --platform manylinux2014_x86_64 --platform manylinux_2_17_x86_64
21
+ # --platform any --python-version 3.12 --implementation cp --abi cp312`.
22
+
23
+ attrs==26.1.0 \
24
+ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309
25
+
26
+ boltons==21.0.0 \
27
+ --hash=sha256:b9bb7b58b2b420bbe11a6025fdef6d3e5edc9f76a42fb467afe7ca212ef9948b
28
+
29
+ bracex==3.0 \
30
+ --hash=sha256:3833e61c2f092d5aa0468fa2e6c6e990a306185abf763b6d122f0158e59c58a5
31
+
32
+ certifi==2026.6.17 \
33
+ --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
34
+
35
+ charset-normalizer==3.4.7 \
36
+ --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116
37
+
38
+ click==8.4.2 \
39
+ --hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76
40
+
41
+ click-option-group==0.5.9 \
42
+ --hash=sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080
43
+
44
+ colorama==0.4.6 \
45
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
46
+
47
+ defusedxml==0.7.1 \
48
+ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
49
+
50
+ deprecated==1.3.1 \
51
+ --hash=sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f
52
+
53
+ exceptiongroup==1.2.2 \
54
+ --hash=sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b
55
+
56
+ face==26.0.1 \
57
+ --hash=sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d
58
+
59
+ glom==22.1.0 \
60
+ --hash=sha256:5339da206bf3532e01a83a35aca202960ea885156986d190574b779598e9e772
61
+
62
+ googleapis-common-protos==1.75.0 \
63
+ --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed
64
+
65
+ idna==3.18 \
66
+ --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2
67
+
68
+ importlib-metadata==7.1.0 \
69
+ --hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570
70
+
71
+ jsonschema==4.26.0 \
72
+ --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
73
+
74
+ jsonschema-specifications==2025.9.1 \
75
+ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe
76
+
77
+ markdown-it-py==4.2.0 \
78
+ --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
79
+
80
+ mdurl==0.1.2 \
81
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8
82
+
83
+ opentelemetry-api==1.25.0 \
84
+ --hash=sha256:757fa1aa020a0f8fa139f8959e53dec2051cc26b832e76fa839a6d76ecefd737
85
+
86
+ opentelemetry-exporter-otlp-proto-common==1.25.0 \
87
+ --hash=sha256:15637b7d580c2675f70246563363775b4e6de947871e01d0f4e3881d1848d693
88
+
89
+ opentelemetry-exporter-otlp-proto-http==1.25.0 \
90
+ --hash=sha256:2eca686ee11b27acd28198b3ea5e5863a53d1266b91cda47c839d95d5e0541a6
91
+
92
+ opentelemetry-instrumentation==0.46b0 \
93
+ --hash=sha256:89cd721b9c18c014ca848ccd11181e6b3fd3f6c7669e35d59c48dc527408c18b
94
+
95
+ opentelemetry-instrumentation-requests==0.46b0 \
96
+ --hash=sha256:a8c2472800d8686f3f286cd524b8746b386154092e85a791ba14110d1acc9b81
97
+
98
+ opentelemetry-proto==1.25.0 \
99
+ --hash=sha256:f07e3341c78d835d9b86665903b199893befa5e98866f63d22b00d0b7ca4972f
100
+
101
+ opentelemetry-sdk==1.25.0 \
102
+ --hash=sha256:d97ff7ec4b351692e9d5a15af570c693b8715ad78b8aafbec5c7100fe966b4c9
103
+
104
+ opentelemetry-semantic-conventions==0.46b0 \
105
+ --hash=sha256:6daef4ef9fa51d51855d9f8e0ccd3a1bd59e0e545abe99ac6203804e36ab3e07
106
+
107
+ opentelemetry-util-http==0.46b0 \
108
+ --hash=sha256:8dc1949ce63caef08db84ae977fdc1848fe6dc38e6bbaad0ae3e6ecd0d451629
109
+
110
+ packaging==26.2 \
111
+ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
112
+
113
+ peewee==3.19.0 \
114
+ --hash=sha256:de220b94766e6008c466e00ce4ba5299b9a832117d9eb36d45d0062f3cfd7417
115
+
116
+ protobuf==4.25.9 \
117
+ --hash=sha256:438c636de8fb706a0de94a12a268ef1ae8f5ba5ae655a7671fcda5968ba3c9be
118
+
119
+ pygments==2.20.0 \
120
+ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
121
+
122
+ referencing==0.37.0 \
123
+ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231
124
+
125
+ requests==2.34.2 \
126
+ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0
127
+
128
+ rich==13.5.3 \
129
+ --hash=sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9
130
+
131
+ rpds-py==2026.6.3 \
132
+ --hash=sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6
133
+
134
+ ruamel.yaml==0.17.40 \
135
+ --hash=sha256:b16b6c3816dff0a93dca12acf5e70afd089fa5acb80604afd1ffa8b465b7722c
136
+
137
+ ruamel.yaml.clib==0.2.15 \
138
+ --hash=sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9
139
+
140
+ semgrep==1.97.0 \
141
+ --hash=sha256:996fe0b2bfac3a4d4511e470fdf5f3bca96b1f794f398e0336c8388802c218de
142
+
143
+ # PINNED to 80.9.0 (the last release that still SHIPS `pkg_resources`):
144
+ # setuptools 81+ removed the vendored `pkg_resources` module, but
145
+ # semgrep 1.97.0's transitive `opentelemetry-instrumentation==0.46b0` imports
146
+ # it at load. A py3.12 venv seeds no setuptools, so the lockfile must supply a
147
+ # pkg_resources-bearing one — 83.0.0 broke the SAST step at runtime with
148
+ # `ModuleNotFoundError: No module named 'pkg_resources'`. Do NOT bump past 80.x
149
+ # without confirming pkg_resources is present (or bumping opentelemetry off it).
150
+ setuptools==80.9.0 \
151
+ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922
152
+
153
+ tomli==2.0.2 \
154
+ --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38
155
+
156
+ typing-extensions==4.16.0 \
157
+ --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
158
+
159
+ urllib3==2.7.0 \
160
+ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
161
+
162
+ wcmatch==8.5.2 \
163
+ --hash=sha256:17d3ad3758f9d0b5b4dedc770b65420d4dac62e680229c287bf24c9db856a478
164
+
165
+ wrapt==1.17.3 \
166
+ --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828
167
+
168
+ zipp==4.1.0 \
169
+ --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f