forgedock 1.18.0 → 1.20.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/README.md +2 -0
- package/bin/engine/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +54 -4
- package/bin/engine/runlog.mjs +10 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +32 -2
- package/bin/engine.mjs +187 -10
- package/bin/runner.mjs +89 -13
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +136 -28
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +201 -40
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +31 -20
- package/commands/work-on/build/architect.md +54 -1
- package/commands/work-on/build/implement.md +28 -1
- package/commands/work-on/build/validate.md +321 -29
- package/commands/work-on/build.md +184 -17
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +43 -17
- package/commands/work-on/review.md +43 -22
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +93 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +29 -3
- package/package.json +2 -2
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/cap-findings.test.sh +2 -0
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-markers.sh +4 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +379 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +451 -0
- package/scripts/ci-local/scoping.mjs +531 -0
- package/scripts/ci-local.sh +98 -0
- package/scripts/ci-local.test.sh +1108 -0
- package/scripts/classify-finding.sh +33 -7
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +2 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/lib/telemetry.sh +3 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +294 -53
- package/scripts/review-finalize.test.sh +347 -9
- package/scripts/review-pr-delta-wiring.test.sh +13 -0
- package/scripts/review-prepare.sh +5 -3
- package/scripts/review-prepare.test.sh +18 -2
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +10 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +14 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +41 -3
- package/scripts/with-run-tmpdir.sh +20 -0
package/scripts/wait-ci-green.sh
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
# 2 ERROR bad usage, gh unreadable for too long, the PR head moved during the wait, or
|
|
19
19
|
# FORGE_CI_REQUIRE_CHECKS=1 and no checks were reported
|
|
20
20
|
# 3 TIMEOUT checks still pending when the timeout expired (listed)
|
|
21
|
+
# 4 RATE_LIMITED the window ended while GitHub only answered with rate-limit (403/429,
|
|
22
|
+
# x-ratelimit-remaining: 0, secondary-limit text) or transient 5xx/network errors. Retryable,
|
|
23
|
+
# never a CI verdict: callers treat it like TIMEOUT (wait and re-run), and it is never a merge.
|
|
21
24
|
#
|
|
22
25
|
# Every result also prints "CI_GATE_HEAD: <sha>" — the exact PR head the decision applies to. Callers
|
|
23
26
|
# MUST merge with `--match-head-commit <that sha>` so a push after the gate cannot land unchecked.
|
|
@@ -26,7 +29,10 @@
|
|
|
26
29
|
# caller re-runs the gate, it does not raise the timeout), FORGE_CI_INTERVAL (default 20),
|
|
27
30
|
# FORGE_CI_NO_CHECKS_GRACE (default 120), FORGE_CI_SETTLE (default 45 — never PASS earlier than
|
|
28
31
|
# this after the wait starts, so workflows that register late are not missed),
|
|
29
|
-
# FORGE_CI_REQUIRE_CHECKS (default 0)
|
|
32
|
+
# FORGE_CI_REQUIRE_CHECKS (default 0), FORGE_CI_JITTER_PCT (default 20 — each sleep varies by +/- this
|
|
33
|
+
# percent so many waiting lanes do not poll in lockstep).
|
|
34
|
+
# Rate-limit and transient gh failures are retried with a growing, jittered sleep and never count toward
|
|
35
|
+
# the 5-error ERROR limit.
|
|
30
36
|
# "No checks" passes only when GitHub Actions also reports no workflow runs for the head commit;
|
|
31
37
|
# if runs exist but their checks have not appeared yet, the gate keeps waiting.
|
|
32
38
|
set -uo pipefail
|
|
@@ -44,7 +50,7 @@ while [ $# -gt 0 ]; do
|
|
|
44
50
|
-R*) REPO="${1#-R}"; REPO="${REPO# }"; shift ;;
|
|
45
51
|
--timeout) [ $# -ge 2 ] || usage; TIMEOUT="$2"; shift 2 ;;
|
|
46
52
|
--interval) [ $# -ge 2 ] || usage; INTERVAL="$2"; shift 2 ;;
|
|
47
|
-
-h|--help) sed -n '5,
|
|
53
|
+
-h|--help) sed -n '5,32p' "$0"; exit 0 ;;
|
|
48
54
|
*) [ -z "$PR" ] || usage; PR="$1"; shift ;;
|
|
49
55
|
esac
|
|
50
56
|
done
|
|
@@ -52,17 +58,50 @@ case "$PR" in ''|*[!0-9]*) usage ;; esac
|
|
|
52
58
|
case "$REPO" in */*) ;; *) usage ;; esac
|
|
53
59
|
for v in "$TIMEOUT" "$INTERVAL" "$GRACE" "$SETTLE"; do case "$v" in ''|*[!0-9]*) usage ;; esac; done
|
|
54
60
|
[ "$INTERVAL" -ge 1 ] || INTERVAL=1
|
|
61
|
+
JITTER_PCT="${FORGE_CI_JITTER_PCT:-20}"; case "$JITTER_PCT" in ''|*[!0-9]*) JITTER_PCT=20 ;; esac
|
|
62
|
+
[ "$JITTER_PCT" -le 90 ] || JITTER_PCT=90
|
|
63
|
+
# jitter <seconds> -> seconds varied by +/- JITTER_PCT percent (never below 1).
|
|
64
|
+
jitter() { awk -v s="$1" -v p="$JITTER_PCT" -v r="$RANDOM" 'BEGIN{ v=s*(1-p/100+2*(p/100)*(r/32767)); if (v<1) v=1; printf "%.2f", v }'; }
|
|
65
|
+
|
|
66
|
+
# classify_gh <text> -> prints one class, or nothing when the text is unrecognised:
|
|
67
|
+
# 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,
|
|
68
|
+
# abuse detection, too many requests): a wait, bounded by the whole window
|
|
69
|
+
# auth a bare 401/403 (bad credentials, missing scope): permanent, never waited on
|
|
70
|
+
# transient 5xx / gateway / network failures: a small bounded retry (FORGE_CI_TRANSIENT_RETRIES), never the long wait
|
|
71
|
+
classify_gh() {
|
|
72
|
+
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
|
|
73
|
+
elif printf '%s' "$1" | grep -Eqi 'HTTP (401|403)|status (401|403)|bad credentials|requires authentication'; then echo auth
|
|
74
|
+
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
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
55
77
|
|
|
56
78
|
emit() {
|
|
57
|
-
case "$1" in PASS) _rc=0 ;; FAIL) _rc=1 ;; TIMEOUT) _rc=3 ;; *) _rc=2 ;; esac
|
|
79
|
+
case "$1" in PASS) _rc=0 ;; FAIL) _rc=1 ;; TIMEOUT) _rc=3 ;; RATE_LIMITED) _rc=4 ;; *) _rc=2 ;; esac
|
|
58
80
|
FORGE_REPO="${FORGE_REPO:-$REPO}" FORGE_PR="${FORGE_PR:-$PR}" forge_emit ci_gate result="$1" rc="$_rc" checks="${total:-0}"
|
|
59
81
|
echo "CI_GATE: $1"; echo "CI_GATE_HEAD: ${START_SHA:-unknown}"
|
|
60
82
|
}
|
|
61
83
|
head_sha() { gh pr view "$PR" -R "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null; }
|
|
62
|
-
|
|
63
|
-
|
|
84
|
+
# forge#4037: the initial head read goes through the same classification as the checks poll: a rate limit is a wait
|
|
85
|
+
# (rc 4 once the window is spent), a bare 401/403 is a fast permanent error, anything else a small bounded retry.
|
|
86
|
+
START_SHA=""; _hstart=$(date +%s); _hlim=0; _hfail=0; _herr="$(mktemp)"; trap 'rm -f "$_herr"' EXIT
|
|
87
|
+
while :; do
|
|
88
|
+
START_SHA="$(gh pr view "$PR" -R "$REPO" --json headRefOid --jq '.headRefOid' 2>"$_herr")" && [ -n "$START_SHA" ] && break
|
|
89
|
+
START_SHA=""; _hc="$(classify_gh "$(cat "$_herr" 2>/dev/null)")"
|
|
90
|
+
case "$_hc" in
|
|
91
|
+
auth) emit ERROR; echo "cannot read PR #$PR head in $REPO (HTTP 401/403 without rate-limit evidence)"; exit 2 ;;
|
|
92
|
+
limited)
|
|
93
|
+
_hlim=$((_hlim + 1))
|
|
94
|
+
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
|
|
95
|
+
_m=$((_hlim > 4 ? 4 : _hlim)); sleep "$(jitter $((INTERVAL * (1 << (_m - 1)))))" ;;
|
|
96
|
+
*)
|
|
97
|
+
_hfail=$((_hfail + 1))
|
|
98
|
+
if [ "$_hfail" -gt 2 ]; then emit ERROR; echo "cannot read PR #$PR head in $REPO"; exit 2; fi
|
|
99
|
+
sleep "$(jitter "$INTERVAL")" ;;
|
|
100
|
+
esac
|
|
101
|
+
done
|
|
64
102
|
|
|
65
|
-
start=$(date +%s); errors=0
|
|
103
|
+
start=$(date +%s); errors=0; limited=0; transient=0
|
|
104
|
+
TRANSIENT_MAX="${FORGE_CI_TRANSIENT_RETRIES:-4}"; case "$TRANSIENT_MAX" in ''|*[!0-9]*) TRANSIENT_MAX=4 ;; esac
|
|
66
105
|
while :; do
|
|
67
106
|
now=$(date +%s); elapsed=$((now - start))
|
|
68
107
|
if ! JSON="$(gh pr checks "$PR" -R "$REPO" --json name,bucket 2>/dev/null)" || ! printf '%s' "$JSON" | jq -e 'type=="array"' >/dev/null 2>&1; then
|
|
@@ -72,14 +111,27 @@ while :; do
|
|
|
72
111
|
# does precisely in the "no checks reported" case this branch exists to detect.
|
|
73
112
|
NC_OUT="$(gh pr checks "$PR" -R "$REPO" 2>&1 || true)"
|
|
74
113
|
if printf '%s' "$NC_OUT" | grep -qi 'no checks reported'; then JSON='[]'
|
|
114
|
+
elif GH_CLASS="$(classify_gh "$NC_OUT")"; [ "$GH_CLASS" = auth ]; then
|
|
115
|
+
# A bare 401/403 is an auth fault that never clears: fail fast instead of waiting out the window (forge#4037).
|
|
116
|
+
emit ERROR; echo "gh pr checks refused (HTTP 401/403 without rate-limit evidence); fix the token or its scopes"; exit 2
|
|
117
|
+
elif [ "$GH_CLASS" = transient ]; then
|
|
118
|
+
# Network timeout / 5xx: a small bounded retry, never the long rate-limit wait.
|
|
119
|
+
transient=$((transient + 1))
|
|
120
|
+
if [ "$transient" -gt "$TRANSIENT_MAX" ]; then emit ERROR; echo "gh pr checks unavailable (transient error) ${transient} times in a row"; exit 2; fi
|
|
121
|
+
sleep "$(jitter "$INTERVAL")"; continue
|
|
122
|
+
elif [ "$GH_CLASS" = limited ]; then
|
|
123
|
+
# Real rate limit: a wait, not a verdict. Retry with a growing jittered sleep; never counts toward ERROR.
|
|
124
|
+
limited=$((limited + 1)); now=$(date +%s)
|
|
125
|
+
if [ $((now - start)) -ge "$TIMEOUT" ]; then emit RATE_LIMITED; echo "PR #$PR: GitHub rate-limited for the whole ${TIMEOUT}s window (${limited} retries); retry later"; exit 4; fi
|
|
126
|
+
_m=$((limited > 4 ? 4 : limited)); sleep "$(jitter $((INTERVAL * (1 << (_m - 1)))))"; continue
|
|
75
127
|
else
|
|
76
128
|
errors=$((errors + 1))
|
|
77
129
|
if [ "$errors" -ge 5 ]; then emit ERROR; echo "gh pr checks unreadable 5 times in a row"; exit 2; fi
|
|
78
|
-
sleep "$INTERVAL"; continue
|
|
130
|
+
sleep "$(jitter "$INTERVAL")"; continue
|
|
79
131
|
fi
|
|
80
132
|
fi
|
|
81
133
|
fi
|
|
82
|
-
errors=0
|
|
134
|
+
errors=0; limited=0; transient=0
|
|
83
135
|
cur="$(head_sha)"
|
|
84
136
|
if [ -n "$cur" ] && [ "$cur" != "$START_SHA" ]; then emit ERROR; echo "PR head moved during the wait ($START_SHA -> $cur); re-review the new head"; exit 2; fi
|
|
85
137
|
total=$(printf '%s' "$JSON" | jq 'length')
|
|
@@ -101,5 +153,5 @@ while :; do
|
|
|
101
153
|
emit PASS; echo "PR #$PR head ${START_SHA:0:7}: $total checks passed or skipped"; exit 0
|
|
102
154
|
fi
|
|
103
155
|
if [ "$elapsed" -ge "$TIMEOUT" ]; then emit TIMEOUT; echo "PR #$PR: still pending after ${TIMEOUT}s:"; echo "$pending"; exit 3; fi
|
|
104
|
-
sleep "$INTERVAL"
|
|
156
|
+
sleep "$(jitter "$INTERVAL")"
|
|
105
157
|
done
|
|
@@ -16,6 +16,7 @@ cat > "$T/bin/gh" <<'GH'
|
|
|
16
16
|
#!/usr/bin/env bash
|
|
17
17
|
D="$FAKE_DIR"
|
|
18
18
|
if [ "$1 $2" = "pr view" ]; then
|
|
19
|
+
if [ -f "$D/headerr" ]; then hn=$(( $(cat "$D/headerr.n" 2>/dev/null || echo 0) + 1 )); echo "$hn" > "$D/headerr.n"; if [ "$hn" -le "$(cat "$D/headerr.max")" ]; then cat "$D/headerr" >&2; exit 1; fi; fi
|
|
19
20
|
n=$(( $(cat "$D/sha.n" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$D/sha.n"
|
|
20
21
|
total=$(wc -l < "$D/sha"); [ "$n" -gt "$total" ] && n=$total; sed -n "${n}p" "$D/sha"; exit 0
|
|
21
22
|
fi
|
|
@@ -24,10 +25,14 @@ if [ "$1" = "api" ]; then
|
|
|
24
25
|
exit 1
|
|
25
26
|
fi
|
|
26
27
|
if [ "$1 $2" = "pr checks" ]; then
|
|
27
|
-
case " $* " in *" --json "*) ;; *)
|
|
28
|
+
case " $* " in *" --json "*) ;; *)
|
|
29
|
+
[ -f "$D/nochecks" ] && { echo "no checks reported on the 'x' branch" >&2; exit 1; }
|
|
30
|
+
cn=$(cat "$D/chk.n" 2>/dev/null || echo 1); while [ ! -f "$D/checks.$cn" ] && [ "$cn" -gt 1 ]; do cn=$((cn-1)); done; cc=$(cat "$D/checks.$cn" 2>/dev/null || true)
|
|
31
|
+
case "$cc" in RALR) echo "HTTP 403: API rate limit already exceeded for user ID 12345. (https://api.github.com/graphql)" >&2 ;; RATE) echo "HTTP 403: API rate limit exceeded for user (https://api.github.com/graphql)" >&2 ;; SECOND) echo "You have exceeded a secondary rate limit" >&2 ;; ERR) echo "HTTP 502: Bad Gateway" >&2 ;; ERRX) echo "boom: unexpected failure" >&2 ;; A403) echo "HTTP 403: Resource not accessible by personal access token" >&2 ;; A401) echo "HTTP 401: Bad credentials" >&2 ;; R429) echo "HTTP 429: Too Many Requests" >&2 ;; RHDR) printf "HTTP 403: Forbidden\nx-ratelimit-remaining: 0\n" >&2 ;; TMO) echo "dial tcp: i/o timeout" >&2 ;; esac
|
|
32
|
+
exit 1 ;; esac
|
|
28
33
|
n=$(( $(cat "$D/chk.n" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$D/chk.n"
|
|
29
34
|
f="$D/checks.$n"; while [ ! -f "$f" ] && [ "$n" -gt 1 ]; do n=$((n-1)); f="$D/checks.$n"; done
|
|
30
|
-
c=$(cat "$f");
|
|
35
|
+
c=$(cat "$f"); case "$c" in ERR|ERRX|RALR|RATE|SECOND|A403|A401|R429|RHDR|TMO) exit 1 ;; esac
|
|
31
36
|
[ "$c" = "NONE" ] && { echo "no checks reported on the 'x' branch" >&2; exit 1; }
|
|
32
37
|
echo "$c"; exit 0
|
|
33
38
|
fi
|
|
@@ -51,7 +56,40 @@ expect "pending then fail" "1|CI_GATE: FAIL" "$(run "$(mk pendfail '[{"name":"a"
|
|
|
51
56
|
expect "pending past timeout" "3|CI_GATE: TIMEOUT" "$(run "$(mk slow '[{"name":"a","bucket":"pending"}]')" --timeout 2)"
|
|
52
57
|
expect "fail wins over pending" "1|CI_GATE: FAIL" "$(run "$(mk mixed '[{"name":"a","bucket":"pending"},{"name":"b","bucket":"fail"}]')")"
|
|
53
58
|
expect "transient outage then pass" "0|CI_GATE: PASS" "$(run "$(mk blip ERR "$P")")"
|
|
54
|
-
expect "persistent
|
|
59
|
+
expect "persistent unclassified failure is an ERROR" "2|CI_GATE: ERROR" "$(run "$(mk down ERRX)" --timeout 60)"
|
|
60
|
+
# Rate-limit / 5xx classification: retried, never an ERROR; a window that only saw them ends RATE_LIMITED (rc 4).
|
|
61
|
+
expect "rate limited then pass retries" "0|CI_GATE: PASS" "$(run "$(mk rl RATE "$P")")"
|
|
62
|
+
expect "secondary limit then pass retries" "0|CI_GATE: PASS" "$(run "$(mk rl2 SECOND "$P")")"
|
|
63
|
+
expect "transient 5xx then pass retries" "0|CI_GATE: PASS" "$(run "$(mk fivexx ERR "$P")")"
|
|
64
|
+
expect "rate limited for the whole window is RATE_LIMITED rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$(mk rlall RATE)" --timeout 2)"
|
|
65
|
+
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)"
|
|
66
|
+
# forge#4037: only real rate-limit evidence is RATE_LIMITED; a bare 401/403 fails fast; timeouts/5xx are a small bounded retry.
|
|
67
|
+
expect "bare 403 is a permanent error rc 2" "2|CI_GATE: ERROR" "$(run "$(mk a403 A403 "$P")" --timeout 60)"
|
|
68
|
+
expect "bare 401 is a permanent error rc 2" "2|CI_GATE: ERROR" "$(run "$(mk a401 A401 "$P")" --timeout 60)"
|
|
69
|
+
expect "HTTP 429 is RATE_LIMITED rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$(mk r429 R429)" --timeout 2)"
|
|
70
|
+
expect "x-ratelimit-remaining: 0 on a 403 is RATE_LIMITED rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$(mk rhdr RHDR)" --timeout 2)"
|
|
71
|
+
expect "network timeout then pass retries" "0|CI_GATE: PASS" "$(run "$(mk tmo TMO "$P")")"
|
|
72
|
+
expect "persistent network timeout is a bounded ERROR, not the long wait" "2|CI_GATE: ERROR" "$(FORGE_CI_TRANSIENT_RETRIES=2 run "$(mk tmoall TMO)" --timeout 600)"
|
|
73
|
+
expect "persistent 5xx is a bounded ERROR, not the long wait" "2|CI_GATE: ERROR" "$(FORGE_CI_TRANSIENT_RETRIES=2 run "$(mk fivexxall ERR)" --timeout 600)"
|
|
74
|
+
# forge#4037: the initial head read uses the same classification.
|
|
75
|
+
d=$(mk headrl "$P"); echo "HTTP 403: API rate limit exceeded for user" > "$d/headerr"; echo 2 > "$d/headerr.max"
|
|
76
|
+
expect "rate-limited head read waits, then passes" "0|CI_GATE: PASS" "$(run "$d" --timeout 60)"
|
|
77
|
+
d=$(mk headrlall "$P"); echo "HTTP 429: Too Many Requests" > "$d/headerr"; echo 99999 > "$d/headerr.max"
|
|
78
|
+
expect "rate-limited head read for the whole window is rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$d" --timeout 2)"
|
|
79
|
+
d=$(mk head403 "$P"); echo "HTTP 403: Resource not accessible" > "$d/headerr"; echo 99999 > "$d/headerr.max"
|
|
80
|
+
# forge#4040: the GraphQL wording "API rate limit already exceeded (HTTP 403)" is a rate limit (rc 4), not the auth class (rc 2).
|
|
81
|
+
expect "GraphQL 'rate limit already exceeded' 403 is RATE_LIMITED rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$(mk ralr RALR)" --timeout 2)"
|
|
82
|
+
expect "rate limit already exceeded, then pass" "0|CI_GATE: PASS" "$(run "$(mk ralr2 RALR "$P")" --timeout 60)"
|
|
83
|
+
d=$(mk headralr "$P"); echo "HTTP 403: API rate limit already exceeded for user ID 12345." > "$d/headerr"; echo 99999 > "$d/headerr.max"
|
|
84
|
+
expect "GraphQL 'already exceeded' head read is RATE_LIMITED rc 4" "4|CI_GATE: RATE_LIMITED" "$(run "$d" --timeout 2)"
|
|
85
|
+
d=$(mk head403b "$P"); echo "HTTP 403: Resource not accessible" > "$d/headerr"; echo 99999 > "$d/headerr.max"
|
|
86
|
+
expect "bare 403 head read is rc 2" "2|CI_GATE: ERROR" "$(run "$d" --timeout 60)"
|
|
87
|
+
# Jitter: sleeps vary within +/-20% of the interval (fake sleep records its argument and returns at once).
|
|
88
|
+
mkdir -p "$T/bin2"; printf '#!/usr/bin/env bash\necho "$1" >> "$FAKE_DIR/sleeps"\n' > "$T/bin2/sleep"; chmod +x "$T/bin2/sleep"
|
|
89
|
+
d=$(mk jit '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"pending"}]' "$P")
|
|
90
|
+
FAKE_DIR="$d" PATH="$T/bin2:$T/bin:$PATH" FORGE_CI_INTERVAL=100 FORGE_CI_SETTLE=0 bash "$GATE" 7 -R o/r >/dev/null 2>&1
|
|
91
|
+
expect "jittered sleeps stay within +/-20%" "0" "$(awk '$1<80||$1>120{bad++} END{print bad+0}' "$d/sleeps")"
|
|
92
|
+
expect "jitter is applied (not a fixed interval)" "1" "$(awk '{u[$1]=1} END{n=0; for(k in u) n++; print (n>1)?1:0}' "$d/sleeps")"
|
|
55
93
|
d=$(mk none NONE); touch "$d/nochecks"
|
|
56
94
|
expect "no checks after grace passes" "0|CI_GATE: PASS" "$(run "$d")"
|
|
57
95
|
expect "no checks + REQUIRE fails closed" "2|CI_GATE: ERROR" "$(FORGE_CI_REQUIRE_CHECKS=1 run "$d")"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# with-run-tmpdir.sh — run a command with a per-run TMPDIR that is removed when it exits.
|
|
6
|
+
#
|
|
7
|
+
# Usage: bash scripts/with-run-tmpdir.sh <command> [args...]
|
|
8
|
+
# Children inherit the exported TMPDIR (os.tmpdir() honours it), so temp dirs a test
|
|
9
|
+
# forgets to remove cannot outlive the run. The command's exit code is preserved.
|
|
10
|
+
# Bash 3.2 portable.
|
|
11
|
+
|
|
12
|
+
set -u
|
|
13
|
+
|
|
14
|
+
[ "$#" -gt 0 ] || { echo "with-run-tmpdir: no command given" >&2; exit 2; }
|
|
15
|
+
|
|
16
|
+
RUN_TMP=$(mktemp -d) || { echo "with-run-tmpdir: cannot create per-run TMPDIR" >&2; exit 1; }
|
|
17
|
+
trap 'rm -rf "$RUN_TMP"' EXIT
|
|
18
|
+
export TMPDIR="$RUN_TMP"
|
|
19
|
+
|
|
20
|
+
"$@"
|