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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel-platform",
3
- "version": "0.21.0",
3
+ "version": "0.25.0",
4
4
  "description": "Shared CI/deploy workflows, composite toolchain action, npm config package, Renovate preset, and operator runbook templates.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,7 +44,7 @@
44
44
  "provenance": true
45
45
  },
46
46
  "dependencies": {
47
- "mandrel": "^1.81.0"
47
+ "mandrel": "^1.83.0"
48
48
  },
49
49
  "scripts": {
50
50
  "typecheck": "node --input-type=module --eval 'process.exit(0)'",
@@ -979,18 +979,61 @@ export function hasDrift(report) {
979
979
  return report.results.some((r) => r.error || r.drift);
980
980
  }
981
981
 
982
+ /**
983
+ * Is EVERY configured consumer an `error` row (M11)? This is the signature of a
984
+ * dead cross-repo credential: a PIN_DRIFT_TOKEN that was *provided* but has
985
+ * expired (fine-grained PATs always expire) can no longer read ANY consumer, so
986
+ * every `fetchConsumerWorkflows` call fails closed to an `error` row. Contrast
987
+ * the not-yet-provisioned bootstrap case: there the token is *absent*, the run
988
+ * legitimately can't read the private consumers, and that benign state must keep
989
+ * its current exit-0 behavior. The caller distinguishes the two by whether the
990
+ * token was provided (see `runCli`'s `tokenProvided`); this predicate only
991
+ * answers "did every row error", which — given a provided token — means the
992
+ * credential died rather than "no drift".
993
+ *
994
+ * Requires at least one consumer (an empty registry is vacuously not a
995
+ * dead-credential signal).
996
+ *
997
+ * @param {{ results: Array<{ error?: string }> }} report
998
+ * @returns {boolean}
999
+ */
1000
+ export function allConsumersErrored(report) {
1001
+ const results = Array.isArray(report.results) ? report.results : [];
1002
+ return results.length > 0 && results.every((r) => Boolean(r.error));
1003
+ }
1004
+
982
1005
  // ---------------------------------------------------------------------------
983
1006
  // CLI entry
984
1007
  // ---------------------------------------------------------------------------
985
1008
 
1009
+ /**
1010
+ * Whether the cross-repo `PIN_DRIFT_TOKEN` was PROVIDED (non-empty) to the run
1011
+ * (M11). The scheduled/dispatch workflow reads consumers over the `gh` CLI with
1012
+ * `GH_TOKEN: ${{ secrets.PIN_DRIFT_TOKEN || github.token }}` — the built-in
1013
+ * `github.token` fallback can only read THIS repo, so cross-repo reads fail
1014
+ * closed to `error` rows both when the token is absent (bootstrap) AND when it
1015
+ * is provided-but-dead (expired PAT). This env var is the only signal that
1016
+ * distinguishes the two: the workflow sets it to the raw secret so an empty
1017
+ * value ⇒ absent (bootstrap, benign) and a non-empty value ⇒ provided (a
1018
+ * total error sweep then means the credential died).
1019
+ *
1020
+ * @param {Record<string, string | undefined>} env
1021
+ * @returns {boolean}
1022
+ */
1023
+ export function pinDriftTokenProvided(env) {
1024
+ return typeof env.PIN_DRIFT_TOKEN === "string" && env.PIN_DRIFT_TOKEN.length > 0;
1025
+ }
1026
+
986
1027
  /**
987
1028
  * @param {{
988
1029
  * argv?: string[],
989
1030
  * cwd?: string,
990
1031
  * stdout?: { write: (s: string) => void },
991
1032
  * stderr?: { write: (s: string) => void },
1033
+ * env?: Record<string, string | undefined>,
992
1034
  * runGh?: (args: string[]) => string,
993
1035
  * summaryPath?: string | undefined,
1036
+ * tokenProvided?: boolean,
994
1037
  * nowMs?: number,
995
1038
  * }} [opts]
996
1039
  * @returns {number} exit code
@@ -1000,8 +1043,10 @@ export function runCli({
1000
1043
  cwd = process.cwd(),
1001
1044
  stdout = process.stdout,
1002
1045
  stderr = process.stderr,
1046
+ env = process.env,
1003
1047
  runGh = defaultGhRunner,
1004
1048
  summaryPath = process.env.GITHUB_STEP_SUMMARY,
1049
+ tokenProvided = pinDriftTokenProvided(env),
1005
1050
  nowMs = Date.now(),
1006
1051
  } = {}) {
1007
1052
  const { config: configRel, json, strict } = parseArgv(argv);
@@ -1025,9 +1070,19 @@ export function runCli({
1025
1070
 
1026
1071
  const report = buildReport(config, runGh, nowMs);
1027
1072
  const drift = hasDrift(report);
1073
+ // M11: a PROVIDED-but-dead PIN_DRIFT_TOKEN (expired PAT) can no longer read
1074
+ // ANY consumer, so every row fails closed to `error`. That is a credential
1075
+ // failure, NOT the benign not-yet-provisioned bootstrap (token absent) — fail
1076
+ // the run unconditionally (even without --strict, which the scheduled path
1077
+ // can never pass) so the death is loud instead of a green no-op. The absent-
1078
+ // token bootstrap keeps its current behavior: tokenProvided is false, so this
1079
+ // branch never fires and the run exits per the drift/--strict rules below.
1080
+ const deadCredential = tokenProvided && allConsumersErrored(report);
1028
1081
 
1029
1082
  if (json) {
1030
- stdout.write(`${JSON.stringify({ kind: "pin-drift-report", drift, ...report }, null, 2)}\n`);
1083
+ stdout.write(
1084
+ `${JSON.stringify({ kind: "pin-drift-report", drift, deadCredential, ...report }, null, 2)}\n`,
1085
+ );
1031
1086
  } else {
1032
1087
  const text = renderReport(report);
1033
1088
  stdout.write(`${text}\n`);
@@ -1042,6 +1097,16 @@ export function runCli({
1042
1097
  }
1043
1098
  }
1044
1099
 
1100
+ if (deadCredential) {
1101
+ stderr.write(
1102
+ "::error::[pin-drift] PIN_DRIFT_TOKEN was provided but every cross-repo " +
1103
+ "consumer read errored — the credential is dead (likely an expired " +
1104
+ "fine-grained PAT), not a not-yet-provisioned bootstrap. Rotate the " +
1105
+ "token. See docs/runbooks/pin-drift-dashboard.md.\n",
1106
+ );
1107
+ return 1;
1108
+ }
1109
+
1045
1110
  if (strict && drift) {
1046
1111
  stderr.write(`[pin-drift] ❌ drift detected (--strict)\n`);
1047
1112
  return 1;
@@ -20,6 +20,7 @@ import { join } from "node:path";
20
20
  import { test } from "node:test";
21
21
 
22
22
  import {
23
+ allConsumersErrored,
23
24
  buildReport,
24
25
  classifyNpmPin,
25
26
  classifyStaleLiterals,
@@ -34,6 +35,7 @@ import {
34
35
  isWithinReleaseAgeWindow,
35
36
  parseDurationMs,
36
37
  parseSemver,
38
+ pinDriftTokenProvided,
37
39
  renderReport,
38
40
  resolveLatestRelease,
39
41
  runCli,
@@ -883,3 +885,126 @@ test("runCli --strict DOES exit 1 once the held release ages out", () => {
883
885
  rmSync(cfgDir, { recursive: true, force: true });
884
886
  }
885
887
  });
888
+
889
+ // ---------------------------------------------------------------------------
890
+ // M11 — provided-but-dead PIN_DRIFT_TOKEN (expired PAT) vs. not-yet-provisioned
891
+ // bootstrap. A dead credential can read NO consumer, so every row fails closed
892
+ // to an `error` row; that must hard-fail EVEN without --strict (the scheduled
893
+ // path can never pass --strict). The absent-token bootstrap — identical row
894
+ // shape but token unset — must keep its benign exit-0.
895
+ // (temp/audits/workflow-robustness-review-2026-07-05.md M11)
896
+ // ---------------------------------------------------------------------------
897
+
898
+ const DEAD_CRED_CONFIG = {
899
+ platformRepo: PLATFORM,
900
+ consumers: [
901
+ { name: "c1", repo: "o/c1", branch: "main" },
902
+ { name: "c2", repo: "o/c2", branch: "main" },
903
+ ],
904
+ };
905
+
906
+ /**
907
+ * A gh runner where the platform's own release resolution succeeds (that read
908
+ * uses the built-in token, which CAN read this repo) but EVERY cross-repo
909
+ * consumer workflow-listing call fails with a non-404 (auth/transport) — the
910
+ * exact shape of a dead cross-repo PAT. Every consumer therefore becomes an
911
+ * `error` row.
912
+ */
913
+ function makeAllConsumersFailRunGh() {
914
+ return (args) => {
915
+ const path = args[1];
916
+ if (path === `repos/${PLATFORM}/releases/latest`) {
917
+ return JSON.stringify({ tag_name: TAG });
918
+ }
919
+ if (path === `repos/${PLATFORM}/git/ref/tags/${TAG}`) {
920
+ return JSON.stringify({ object: { sha: LATEST_SHA, type: "commit" } });
921
+ }
922
+ if (/\/contents\/\.github\/workflows/.test(path)) {
923
+ // 403/5xx (not 404) → fail-closed to an `error` row, mirroring an expired
924
+ // PAT that can no longer read the private consumer repo.
925
+ throw ghHttpError(403, "Forbidden");
926
+ }
927
+ throw new Error(`unexpected gh api path: ${path}`);
928
+ };
929
+ }
930
+
931
+ test("pinDriftTokenProvided: non-empty ⇒ true, empty/absent ⇒ false", () => {
932
+ assert.equal(pinDriftTokenProvided({ PIN_DRIFT_TOKEN: "ghp_live" }), true);
933
+ assert.equal(pinDriftTokenProvided({ PIN_DRIFT_TOKEN: "" }), false);
934
+ assert.equal(pinDriftTokenProvided({}), false);
935
+ });
936
+
937
+ test("allConsumersErrored: true only when every row errored and ≥1 consumer", () => {
938
+ const report = buildReport(DEAD_CRED_CONFIG, makeAllConsumersFailRunGh());
939
+ assert.equal(allConsumersErrored(report), true);
940
+ // A single clean row flips it back to false — that is drift/partial, not a
941
+ // dead credential.
942
+ assert.equal(
943
+ allConsumersErrored({ results: [{ error: "x" }, { drift: true }] }),
944
+ false,
945
+ );
946
+ assert.equal(allConsumersErrored({ results: [] }), false);
947
+ });
948
+
949
+ test("runCli: PROVIDED-but-dead PIN_DRIFT_TOKEN (all rows error) exits 1 with ::error:: even without --strict", () => {
950
+ cfgDir = mkdtempSync(join(tmpdir(), "pin-drift-dead-"));
951
+ const p = join(cfgDir, "consumers.json");
952
+ writeFileSync(p, JSON.stringify(DEAD_CRED_CONFIG));
953
+ const stderr = capture();
954
+ try {
955
+ const code = runCli({
956
+ argv: ["--config", p], // NOTE: no --strict.
957
+ env: { PIN_DRIFT_TOKEN: "ghp_expired" },
958
+ runGh: makeAllConsumersFailRunGh(),
959
+ stdout: capture(),
960
+ stderr,
961
+ summaryPath: undefined,
962
+ });
963
+ assert.equal(code, 1);
964
+ assert.match(stderr.text(), /::error::/);
965
+ assert.match(stderr.text(), /credential is dead/);
966
+ } finally {
967
+ rmSync(cfgDir, { recursive: true, force: true });
968
+ }
969
+ });
970
+
971
+ test("runCli: dead-credential also surfaces in the --json envelope (deadCredential:true)", () => {
972
+ cfgDir = mkdtempSync(join(tmpdir(), "pin-drift-dead-json-"));
973
+ const p = join(cfgDir, "consumers.json");
974
+ writeFileSync(p, JSON.stringify(DEAD_CRED_CONFIG));
975
+ const stdout = capture();
976
+ try {
977
+ const code = runCli({
978
+ argv: ["--config", p, "--json"],
979
+ env: { PIN_DRIFT_TOKEN: "ghp_expired" },
980
+ runGh: makeAllConsumersFailRunGh(),
981
+ stdout,
982
+ stderr: capture(),
983
+ summaryPath: undefined,
984
+ });
985
+ assert.equal(code, 1);
986
+ const envelope = JSON.parse(stdout.text());
987
+ assert.equal(envelope.deadCredential, true);
988
+ } finally {
989
+ rmSync(cfgDir, { recursive: true, force: true });
990
+ }
991
+ });
992
+
993
+ test("runCli: ABSENT PIN_DRIFT_TOKEN bootstrap (all rows error, token unset) stays exit 0 — benign", () => {
994
+ cfgDir = mkdtempSync(join(tmpdir(), "pin-drift-bootstrap-"));
995
+ const p = join(cfgDir, "consumers.json");
996
+ writeFileSync(p, JSON.stringify(DEAD_CRED_CONFIG));
997
+ try {
998
+ const code = runCli({
999
+ argv: ["--config", p], // no --strict, no token.
1000
+ env: {}, // PIN_DRIFT_TOKEN absent → not-yet-provisioned bootstrap.
1001
+ runGh: makeAllConsumersFailRunGh(),
1002
+ stdout: capture(),
1003
+ stderr: capture(),
1004
+ summaryPath: undefined,
1005
+ });
1006
+ assert.equal(code, 0);
1007
+ } finally {
1008
+ rmSync(cfgDir, { recursive: true, force: true });
1009
+ }
1010
+ });