forgedock 1.36.0 → 1.37.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.
Files changed (112) hide show
  1. package/bin/bot-token-status.mjs +26 -0
  2. package/bin/engine/events.mjs +10 -1
  3. package/bin/engine/events.test.mjs +15 -1
  4. package/bin/engine/outcome-codes.mjs +39 -1
  5. package/bin/engine/phases.mjs +65 -5
  6. package/bin/engine/telemetry-seam.mjs +1 -1
  7. package/bin/engine/telemetry-seam.test.mjs +4 -0
  8. package/bin/engine-cli.mjs +268 -37
  9. package/bin/engine.mjs +3 -1
  10. package/bin/forgedock.mjs +108 -5
  11. package/bin/labels.json +50 -0
  12. package/bin/lean/batch.mjs +10 -4
  13. package/bin/lean/batch.test.mjs +75 -1
  14. package/bin/lean/conformance.e2e.test.mjs +1 -1
  15. package/bin/lean/conformance.mjs +125 -11
  16. package/bin/lean/conformance.test.mjs +122 -1
  17. package/bin/lean/decisions.mjs +181 -0
  18. package/bin/lean/decisions.test.mjs +71 -0
  19. package/bin/lean/effects.mjs +29 -14
  20. package/bin/lean/execution.mjs +60 -16
  21. package/bin/lean/execution.test.mjs +59 -2
  22. package/bin/lean/gatewatch.mjs +31 -0
  23. package/bin/lean/gatewatch.test.mjs +59 -0
  24. package/bin/lean/guardgate.mjs +170 -0
  25. package/bin/lean/guardgate.test.mjs +153 -0
  26. package/bin/lean/guards.mjs +243 -0
  27. package/bin/lean/guards.test.mjs +177 -0
  28. package/bin/lean/heavy.mjs +54 -8
  29. package/bin/lean/heavy.test.mjs +74 -1
  30. package/bin/lean/hostkill.mjs +1 -1
  31. package/bin/lean/integration.mjs +276 -0
  32. package/bin/lean/integration.test.mjs +189 -0
  33. package/bin/lean/lean.mjs +134 -28
  34. package/bin/lean/lean.test.mjs +239 -22
  35. package/bin/lean/lifecycle.mjs +129 -0
  36. package/bin/lean/lifecycle.test.mjs +118 -0
  37. package/bin/lean/light.mjs +1 -1
  38. package/bin/lean/milestonesync.mjs +130 -0
  39. package/bin/lean/milestonesync.test.mjs +133 -0
  40. package/bin/lean/orchestrate.mjs +83 -15
  41. package/bin/lean/parked-sweep.mjs +45 -0
  42. package/bin/lean/parked-sweep.test.mjs +71 -0
  43. package/bin/lean/postdeploy-check.mjs +38 -6
  44. package/bin/lean/postdeploy-check.test.mjs +62 -2
  45. package/bin/lean/postdeploy-monitor.mjs +12 -2
  46. package/bin/lean/postdeploy-monitor.test.mjs +27 -1
  47. package/bin/lean/prefilter.mjs +83 -23
  48. package/bin/lean/prefilter.test.mjs +73 -1
  49. package/bin/lean/probe.mjs +2 -2
  50. package/bin/lean/probe.test.mjs +1 -1
  51. package/bin/lean/prompts.mjs +15 -9
  52. package/bin/lean/resume.test.mjs +1 -1
  53. package/bin/lean/retry.mjs +28 -0
  54. package/bin/lean/sandbox-provider.contract.test.mjs +8 -3
  55. package/bin/lean/sandbox-remote.mjs +4 -4
  56. package/bin/lean/schedd.mjs +53 -9
  57. package/bin/lean/schedd.test.mjs +77 -5
  58. package/bin/lean/sequence.mjs +24 -0
  59. package/bin/lean/sequence.test.mjs +31 -1
  60. package/bin/lean/signoff.mjs +66 -0
  61. package/bin/lean/signoff.test.mjs +102 -0
  62. package/bin/lean/train.mjs +177 -0
  63. package/bin/lean/train.test.mjs +113 -0
  64. package/bin/pool/cli.mjs +2 -0
  65. package/bin/pool/health.mjs +30 -0
  66. package/bin/review-context.mjs +298 -0
  67. package/bin/runner.mjs +26 -3
  68. package/bin/stats/compounding.mjs +157 -0
  69. package/bin/stats/compounding.test.mjs +74 -0
  70. package/bin/stats/index.mjs +21 -2
  71. package/commands/bootstrap.md +10 -8
  72. package/commands/deploy-info.md +3 -0
  73. package/commands/issue.md +2 -0
  74. package/commands/milestone.md +43 -54
  75. package/commands/orchestrate/phase-4-execution.md +2 -0
  76. package/commands/orchestrate.md +8 -1
  77. package/commands/pipeline-status.md +20 -0
  78. package/commands/review-pr.md +1 -0
  79. package/commands/work-on/build/implement.md +1 -1
  80. package/commands/work-on/build.md +4 -2
  81. package/commands/work-on/investigate.md +3 -1
  82. package/commands/work-on/remediate.md +1 -1
  83. package/docs/CONFIG.md +71 -2
  84. package/docs/EVENTS.md +1 -4
  85. package/docs/OUTCOME-CODES.md +9 -1
  86. package/docs/STATS.md +20 -0
  87. package/docs/site/start-a-new-project.md +1 -1
  88. package/docs/spec/label-state-machine.md +6 -0
  89. package/package.json +1 -1
  90. package/pool/install/install-agent.sh +4 -2
  91. package/pool/install/install-router.sh +4 -2
  92. package/pool/install/install-schedd.sh +28 -0
  93. package/pool/install/unit-hardening.sh +34 -0
  94. package/scripts/acceptance-red-green.sh +19 -2
  95. package/scripts/acceptance-red-green.test.sh +2 -0
  96. package/scripts/bootstrap-create.mjs +3 -2
  97. package/scripts/bootstrap-validate.mjs +31 -4
  98. package/scripts/file-deferred-followups.sh +285 -77
  99. package/scripts/file-deferred-followups.test.sh +111 -4
  100. package/scripts/hotspots.sh +23 -2
  101. package/scripts/hotspots.test.sh +9 -0
  102. package/scripts/plan-validate-live.mjs +111 -0
  103. package/scripts/readiness.mjs +141 -0
  104. package/scripts/review-prepare.sh +17 -1
  105. package/scripts/review-prepare.test.sh +17 -0
  106. package/scripts/settle-scope.mjs +74 -0
  107. package/scripts/systemd/forgedock-bot-token-refresh.service +7 -0
  108. package/scripts/systemd/forgedock-bot-token-refresh.timer +11 -0
  109. package/scripts/wait-ci-green.sh +14 -3
  110. package/scripts/wait-ci-green.test.sh +3 -1
  111. package/templates/bootstrap/needs-human-issue.md +4 -1
  112. package/templates/github-actions/migration-journal-guard.test.mjs +26 -0
@@ -71,11 +71,13 @@ jitter() { awk -v s="$1" -v p="$JITTER_PCT" -v r="$RANDOM" 'BEGIN{ v=s*(1-p/100+
71
71
  # classify_gh <text> -> prints one class, or nothing when the text is unrecognised:
72
72
  # limited real rate-limit evidence (x-ratelimit-remaining: 0, HTTP 429, "API rate limit exceeded" or GraphQL "API rate limit already exceeded", secondary rate limit,
73
73
  # abuse detection, too many requests): a wait, bounded by the whole window
74
- # auth a bare 401/403 (bad credentials, missing scope): permanent, never waited on
74
+ # unauth a bare 401 (expired token, forge#4662): the refresh timer may fix it, so a small bounded retry, then an explicit auth error
75
+ # auth a bare 403 (missing scope): permanent, never waited on
75
76
  # transient 5xx / gateway / network failures: a small bounded retry (FORGE_CI_TRANSIENT_RETRIES), never the long wait
76
77
  classify_gh() {
77
78
  if printf '%s' "$1" | grep -Eqi 'api rate limit (already )?exceeded|secondary (rate )?limit|abuse detection|x-ratelimit-remaining: *0|HTTP 429|status 429|too many requests'; then echo limited
78
- elif printf '%s' "$1" | grep -Eqi 'HTTP (401|403)|status (401|403)|bad credentials|requires authentication'; then echo auth
79
+ elif printf '%s' "$1" | grep -Eqi 'HTTP 401|status 401|bad credentials|requires authentication'; then echo unauth
80
+ elif printf '%s' "$1" | grep -Eqi 'HTTP 403|status 403'; then echo auth
79
81
  elif printf '%s' "$1" | grep -Eqi 'HTTP 5[0-9][0-9]|status 5[0-9][0-9]|bad gateway|service unavailable|gateway time-?out|timed? ?out|i/o timeout|connection (reset|refused)|could not resolve host|unexpected EOF|network is unreachable|TLS handshake'; then echo transient
80
82
  fi
81
83
  }
@@ -94,6 +96,10 @@ while :; do
94
96
  START_SHA=""; _hc="$(classify_gh "$(cat "$_herr" 2>/dev/null)")"
95
97
  case "$_hc" in
96
98
  auth) emit ERROR; echo "cannot read PR #$PR head in $REPO (HTTP 401/403 without rate-limit evidence)"; exit 2 ;;
99
+ unauth)
100
+ _hfail=$((_hfail + 1))
101
+ if [ "$_hfail" -gt 2 ]; then emit ERROR; echo "cannot read PR #$PR head in $REPO: HTTP 401 (bot token expired or revoked?) ${_hfail} times in a row; run scripts/refresh-bot-token.sh"; exit 2; fi
102
+ sleep "$(jitter "$INTERVAL")" ;;
97
103
  limited)
98
104
  _hlim=$((_hlim + 1))
99
105
  if [ $(( $(date +%s) - _hstart )) -ge "$TIMEOUT" ]; then emit RATE_LIMITED; echo "PR #$PR: GitHub rate-limited for the whole ${TIMEOUT}s window reading the PR head; retry later"; exit 4; fi
@@ -117,8 +123,13 @@ while :; do
117
123
  NC_OUT="$(gh pr checks "$PR" -R "$REPO" 2>&1 || true)"
118
124
  if printf '%s' "$NC_OUT" | grep -qi 'no checks reported'; then JSON='[]'
119
125
  elif GH_CLASS="$(classify_gh "$NC_OUT")"; [ "$GH_CLASS" = auth ]; then
120
- # A bare 401/403 is an auth fault that never clears: fail fast instead of waiting out the window (forge#4037).
126
+ # A bare 403 is an auth fault that never clears: fail fast instead of waiting out the window (forge#4037).
121
127
  emit ERROR; echo "gh pr checks refused (HTTP 401/403 without rate-limit evidence); fix the token or its scopes"; exit 2
128
+ elif [ "$GH_CLASS" = unauth ]; then
129
+ # forge#4662: an expired token is unknown state, not "no checks": back off, then fail with an explicit auth message.
130
+ transient=$((transient + 1))
131
+ if [ "$transient" -gt "$TRANSIENT_MAX" ]; then emit ERROR; echo "gh pr checks refused (HTTP 401) ${transient} times in a row; the bot token is expired or revoked, run scripts/refresh-bot-token.sh"; exit 2; fi
132
+ sleep "$(jitter "$INTERVAL")"; continue
122
133
  elif [ "$GH_CLASS" = transient ]; then
123
134
  # Network timeout / 5xx: a small bounded retry, never the long rate-limit wait.
124
135
  transient=$((transient + 1))
@@ -74,7 +74,9 @@ expect "rate limited for the whole window is RATE_LIMITED rc 4" "4|CI_GATE: RATE
74
74
  expect "more than 5 rate-limit hits are not an ERROR" "0|CI_GATE: PASS" "$(run "$(mk rlmany RATE RATE RATE RATE RATE RATE RATE "$P")" --timeout 60)"
75
75
  # forge#4037: only real rate-limit evidence is RATE_LIMITED; a bare 401/403 fails fast; timeouts/5xx are a small bounded retry.
76
76
  expect "bare 403 is a permanent error rc 2" "2|CI_GATE: ERROR" "$(run "$(mk a403 A403 "$P")" --timeout 60)"
77
- expect "bare 401 is a permanent error rc 2" "2|CI_GATE: ERROR" "$(run "$(mk a401 A401 "$P")" --timeout 60)"
77
+ # forge#4662: a 401 is a refreshable token, so it is retried a bounded number of times before it is an error.
78
+ expect "401 then pass retries (token refreshed meanwhile)" "0|CI_GATE: PASS" "$(run "$(mk a401 A401 "$P")" --timeout 60)"
79
+ expect "persistent 401 is rc 2 after the bounded retries" "2|CI_GATE: ERROR" "$(run "$(mk a401all A401)" --timeout 60)"
78
80
  expect "HTTP 429 is RATE_LIMITED rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$(mk r429 R429)" --timeout 2)"
79
81
  expect "x-ratelimit-remaining: 0 on a 403 is RATE_LIMITED rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$(mk rhdr RHDR)" --timeout 2)"
80
82
  expect "network timeout then pass retries" "0|CI_GATE: PASS" "$(run "$(mk tmo TMO "$P")")"
@@ -1,10 +1,13 @@
1
1
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
2
2
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
3
3
  <!-- /bootstrap needs-human issue: one per credential, account or external input (OAuth client, cloud or DNS account,
4
- payment keys, API keys, legal review). Label it needs-human and set "needsHuman": true in the plan. Code issues
4
+ payment keys, API keys, legal review). Label it needs-human and needs-human:external (the script adds the latter), give it a `gates:` line (preview|staging-deploy|prod-deploy|feature-flag:<name>) and an `owner:` line, and set "needsHuman": true in the plan. Code issues
5
5
  reference it (#{{KEY}}) but never list it under Dependencies; their mocks keep dev and CI green. Copy from "## Problem" down. -->
6
6
  ## Problem
7
7
 
8
+ gates: preview
9
+ owner: maintainer
10
+
8
11
  {which credential, account or external input is needed, and which issues use it}
9
12
 
10
13
  ### Setup steps
@@ -0,0 +1,26 @@
1
+ // Guard test template for repos with a drizzle-style migration journal (#4661). Copy it next to your tests and set JOURNAL.
2
+ // Newest-only migrators (drizzle `migrate()`) skip an entry whose `when` is older than one already applied. CI migrates a
3
+ // fresh database, so it never notices; parallel milestone branches interleave stamps and hit this on persistent databases.
4
+ // Run in CI with the base ref fetched: BASE_REF=origin/main node --test migration-journal-guard.test.mjs
5
+ //
6
+ // Optional upgrade test (repo-specific, not provided): migrate a database to the base's head, apply the branch's
7
+ // migrations, then assert that every migration the branch added is recorded as applied.
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { execFileSync } from "node:child_process";
11
+ import { readFileSync } from "node:fs";
12
+
13
+ const JOURNAL = process.env.JOURNAL ?? "drizzle/meta/_journal.json";
14
+ const BASE_REF = process.env.BASE_REF ?? "origin/main";
15
+ const entries = JSON.parse(readFileSync(JOURNAL, "utf8")).entries;
16
+
17
+ test("journal `when` strictly increases", () => {
18
+ for (let i = 1; i < entries.length; i++) assert.ok(entries[i].when > entries[i - 1].when, `${entries[i].tag} when ${entries[i].when} is not after ${entries[i - 1].tag}`);
19
+ });
20
+
21
+ test("no new entry is older than the base max `when`", (t) => {
22
+ let base;
23
+ try { base = JSON.parse(execFileSync("git", ["show", `${BASE_REF}:${JOURNAL}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })).entries; } catch { return t.skip(`no ${JOURNAL} at ${BASE_REF}`); }
24
+ const known = new Set(base.map((e) => e.tag)), max = Math.max(...base.map((e) => e.when));
25
+ for (const e of entries.filter((x) => !known.has(x.tag))) assert.ok(e.when > max, `${e.tag} when ${e.when} is not above the base max ${max}: re-stamp it with the generator`);
26
+ });