forgedock 1.8.0 → 1.8.2
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/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +2 -2
- package/commands/orchestrate/phase-1-resolve.md +56 -12
- package/commands/orchestrate/phase-3-dependency.md +15 -5
- package/commands/orchestrate/phase-4-execution.md +593 -78
- package/commands/orchestrate/phase-5-cleanup.md +4 -2
- package/commands/orchestrate/phase-6-report.md +7 -3
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/phase-4-proposals.md +6 -4
- package/commands/pipeline-health/phase-5-report.md +4 -2
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +172 -13
- package/commands/review-pr-agents.md +6 -4
- package/commands/review-pr-staging.md +13 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +4 -2
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +81 -19
- package/commands/work-on/build/context.md +61 -18
- package/commands/work-on/build/implement.md +62 -18
- package/commands/work-on/build/validate.md +240 -60
- package/commands/work-on/build.md +344 -146
- package/commands/work-on/close.md +617 -246
- package/commands/work-on/decompose.md +239 -41
- package/commands/work-on/investigate.md +377 -61
- package/commands/work-on/remediate.md +42 -9
- package/commands/work-on/review.md +240 -29
- package/commands/work-on-monolithic.md +48 -7
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +11 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# wait-ci-green.sh — CI gate every autonomous merge must pass first.
|
|
6
|
+
#
|
|
7
|
+
# Usage: wait-ci-green.sh <pr-number> -R <owner/repo> [--timeout SECONDS] [--interval SECONDS]
|
|
8
|
+
#
|
|
9
|
+
# Waits until every check on the PR has finished, then decides. Fails closed: a merge may
|
|
10
|
+
# proceed ONLY on exit 0. Field test: pipeline agents merged PRs to staging while checks were
|
|
11
|
+
# pending or red (#3165 merged with the side-effect gate failing), because neither the specs nor
|
|
12
|
+
# branch protection required green CI, and `gh pr merge --auto` only waits for *required* checks.
|
|
13
|
+
#
|
|
14
|
+
# Exit codes (one "CI_GATE: <RESULT>" line on stdout, details after it):
|
|
15
|
+
# 0 PASS every check is pass/skipping (or the repo reports no checks after the grace period
|
|
16
|
+
# and FORGE_CI_REQUIRE_CHECKS is not 1)
|
|
17
|
+
# 1 FAIL at least one check failed or was cancelled (listed)
|
|
18
|
+
# 2 ERROR bad usage, gh unreadable for too long, the PR head moved during the wait, or
|
|
19
|
+
# FORGE_CI_REQUIRE_CHECKS=1 and no checks were reported
|
|
20
|
+
# 3 TIMEOUT checks still pending when the timeout expired (listed)
|
|
21
|
+
#
|
|
22
|
+
# Every result also prints "CI_GATE_HEAD: <sha>" — the exact PR head the decision applies to. Callers
|
|
23
|
+
# MUST merge with `--match-head-commit <that sha>` so a push after the gate cannot land unchecked.
|
|
24
|
+
#
|
|
25
|
+
# Env: FORGE_CI_TIMEOUT (default 540 — stays under the 10-minute Bash tool limit; on TIMEOUT the
|
|
26
|
+
# caller re-runs the gate, it does not raise the timeout), FORGE_CI_INTERVAL (default 20),
|
|
27
|
+
# FORGE_CI_NO_CHECKS_GRACE (default 120), FORGE_CI_SETTLE (default 45 — never PASS earlier than
|
|
28
|
+
# this after the wait starts, so workflows that register late are not missed),
|
|
29
|
+
# FORGE_CI_REQUIRE_CHECKS (default 0).
|
|
30
|
+
# "No checks" passes only when GitHub Actions also reports no workflow runs for the head commit;
|
|
31
|
+
# if runs exist but their checks have not appeared yet, the gate keeps waiting.
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
|
|
34
|
+
PR=""; REPO=""
|
|
35
|
+
TIMEOUT="${FORGE_CI_TIMEOUT:-540}"; INTERVAL="${FORGE_CI_INTERVAL:-20}"; SETTLE="${FORGE_CI_SETTLE:-45}"
|
|
36
|
+
GRACE="${FORGE_CI_NO_CHECKS_GRACE:-120}"; REQUIRE="${FORGE_CI_REQUIRE_CHECKS:-0}"
|
|
37
|
+
usage() { echo "CI_GATE: ERROR"; echo "CI_GATE_HEAD: unknown"; echo "usage: wait-ci-green.sh <pr> -R <owner/repo> [--timeout S] [--interval S]" >&2; exit 2; }
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
-R) [ $# -ge 2 ] || usage; REPO="$2"; shift 2 ;;
|
|
41
|
+
-R*) REPO="${1#-R}"; REPO="${REPO# }"; shift ;;
|
|
42
|
+
--timeout) [ $# -ge 2 ] || usage; TIMEOUT="$2"; shift 2 ;;
|
|
43
|
+
--interval) [ $# -ge 2 ] || usage; INTERVAL="$2"; shift 2 ;;
|
|
44
|
+
-h|--help) sed -n '5,25p' "$0"; exit 0 ;;
|
|
45
|
+
*) [ -z "$PR" ] || usage; PR="$1"; shift ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
case "$PR" in ''|*[!0-9]*) usage ;; esac
|
|
49
|
+
case "$REPO" in */*) ;; *) usage ;; esac
|
|
50
|
+
for v in "$TIMEOUT" "$INTERVAL" "$GRACE" "$SETTLE"; do case "$v" in ''|*[!0-9]*) usage ;; esac; done
|
|
51
|
+
[ "$INTERVAL" -ge 1 ] || INTERVAL=1
|
|
52
|
+
|
|
53
|
+
emit() { echo "CI_GATE: $1"; echo "CI_GATE_HEAD: ${START_SHA:-unknown}"; }
|
|
54
|
+
head_sha() { gh pr view "$PR" -R "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null; }
|
|
55
|
+
START_SHA="$(head_sha)"
|
|
56
|
+
if [ -z "$START_SHA" ]; then START_SHA=""; emit ERROR; echo "cannot read PR #$PR head in $REPO"; exit 2; fi
|
|
57
|
+
|
|
58
|
+
start=$(date +%s); errors=0
|
|
59
|
+
while :; do
|
|
60
|
+
now=$(date +%s); elapsed=$((now - start))
|
|
61
|
+
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
|
|
62
|
+
# gh pr checks exits non-zero both on outage and on "no checks reported"; tell them apart by output.
|
|
63
|
+
if printf '%s' "${JSON:-}" | jq -e 'type=="array"' >/dev/null 2>&1; then :; else
|
|
64
|
+
# Capture first: under pipefail, `gh ... | grep -q` fails whenever gh exits 1, which it
|
|
65
|
+
# does precisely in the "no checks reported" case this branch exists to detect.
|
|
66
|
+
NC_OUT="$(gh pr checks "$PR" -R "$REPO" 2>&1 || true)"
|
|
67
|
+
if printf '%s' "$NC_OUT" | grep -qi 'no checks reported'; then JSON='[]'
|
|
68
|
+
else
|
|
69
|
+
errors=$((errors + 1))
|
|
70
|
+
if [ "$errors" -ge 5 ]; then emit ERROR; echo "gh pr checks unreadable 5 times in a row"; exit 2; fi
|
|
71
|
+
sleep "$INTERVAL"; continue
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
errors=0
|
|
76
|
+
cur="$(head_sha)"
|
|
77
|
+
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
|
|
78
|
+
total=$(printf '%s' "$JSON" | jq 'length')
|
|
79
|
+
failed=$(printf '%s' "$JSON" | jq -r '.[] | select(.bucket=="fail" or .bucket=="cancel") | " - \(.name) (\(.bucket))"')
|
|
80
|
+
pending=$(printf '%s' "$JSON" | jq -r '.[] | select(.bucket!="pass" and .bucket!="skipping" and .bucket!="fail" and .bucket!="cancel") | " - \(.name) (\(.bucket))"')
|
|
81
|
+
if [ -n "$failed" ]; then emit FAIL; echo "PR #$PR head ${START_SHA:0:7}: failing checks:"; echo "$failed"; exit 1; fi
|
|
82
|
+
if [ "$total" -eq 0 ]; then
|
|
83
|
+
if [ "$elapsed" -ge "$GRACE" ]; then
|
|
84
|
+
RUNS=$(gh api "repos/$REPO/actions/runs?head_sha=$START_SHA&per_page=1" --jq '.total_count' 2>/dev/null || echo "")
|
|
85
|
+
if [ -n "$RUNS" ] && [ "$RUNS" != "0" ]; then
|
|
86
|
+
: # workflow runs exist for this head but no checks are reported yet — keep waiting
|
|
87
|
+
elif [ "$REQUIRE" = "1" ]; then
|
|
88
|
+
emit ERROR; echo "no checks reported after ${GRACE}s and FORGE_CI_REQUIRE_CHECKS=1"; exit 2
|
|
89
|
+
else
|
|
90
|
+
emit PASS; echo "no checks reported for PR #$PR after ${GRACE}s and no workflow runs for its head (no CI on this branch)"; exit 0
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
elif [ -z "$pending" ] && [ "$elapsed" -ge "$SETTLE" ]; then
|
|
94
|
+
emit PASS; echo "PR #$PR head ${START_SHA:0:7}: $total checks passed or skipped"; exit 0
|
|
95
|
+
fi
|
|
96
|
+
if [ "$elapsed" -ge "$TIMEOUT" ]; then emit TIMEOUT; echo "PR #$PR: still pending after ${TIMEOUT}s:"; echo "$pending"; exit 3; fi
|
|
97
|
+
sleep "$INTERVAL"
|
|
98
|
+
done
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
# wait-ci-green.test.sh — offline tests for scripts/wait-ci-green.sh (fake gh on PATH).
|
|
5
|
+
# Usage: bash scripts/wait-ci-green.test.sh
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; GATE="$HERE/wait-ci-green.sh"
|
|
8
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
9
|
+
PASS=0; FAILN=0
|
|
10
|
+
expect() { [ "$2" = "$3" ] && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: $1 (got '$3' want '$2')"; }; }
|
|
11
|
+
|
|
12
|
+
# Fake gh: `pr view --json headRefOid` prints line N of $T/sha (N = call count, last line repeats);
|
|
13
|
+
# `pr checks --json` prints the N-th fixture $T/checks.N (last one repeats); "ERR" fixture = outage.
|
|
14
|
+
mkdir -p "$T/bin"
|
|
15
|
+
cat > "$T/bin/gh" <<'GH'
|
|
16
|
+
#!/usr/bin/env bash
|
|
17
|
+
D="$FAKE_DIR"
|
|
18
|
+
if [ "$1 $2" = "pr view" ]; then
|
|
19
|
+
n=$(( $(cat "$D/sha.n" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$D/sha.n"
|
|
20
|
+
total=$(wc -l < "$D/sha"); [ "$n" -gt "$total" ] && n=$total; sed -n "${n}p" "$D/sha"; exit 0
|
|
21
|
+
fi
|
|
22
|
+
if [ "$1" = "api" ]; then
|
|
23
|
+
case "$2" in *actions/runs*) cat "$D/runs" 2>/dev/null || echo 0; exit 0 ;; esac
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
if [ "$1 $2" = "pr checks" ]; then
|
|
27
|
+
case " $* " in *" --json "*) ;; *) [ -f "$D/nochecks" ] && { echo "no checks reported on the 'x' branch" >&2; exit 1; }; exit 1 ;; esac
|
|
28
|
+
n=$(( $(cat "$D/chk.n" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$D/chk.n"
|
|
29
|
+
f="$D/checks.$n"; while [ ! -f "$f" ] && [ "$n" -gt 1 ]; do n=$((n-1)); f="$D/checks.$n"; done
|
|
30
|
+
c=$(cat "$f"); [ "$c" = "ERR" ] && { echo "HTTP 502" >&2; exit 1; }
|
|
31
|
+
[ "$c" = "NONE" ] && { echo "no checks reported on the 'x' branch" >&2; exit 1; }
|
|
32
|
+
echo "$c"; exit 0
|
|
33
|
+
fi
|
|
34
|
+
exit 1
|
|
35
|
+
GH
|
|
36
|
+
chmod +x "$T/bin/gh"
|
|
37
|
+
|
|
38
|
+
run() { # run <case-dir> [gate args...] -> prints "rc|first line"
|
|
39
|
+
local d="$1"; shift
|
|
40
|
+
local out; out=$(FAKE_DIR="$d" PATH="$T/bin:$PATH" FORGE_CI_INTERVAL=1 FORGE_CI_NO_CHECKS_GRACE=1 FORGE_CI_SETTLE="${SETTLE:-0}" bash "$GATE" 7 -R o/r "$@" 2>/dev/null); local rc=$?
|
|
41
|
+
printf '%s|%s' "$rc" "$(printf '%s\n' "$out" | head -1)"
|
|
42
|
+
}
|
|
43
|
+
mk() { local d="$T/$1"; mkdir -p "$d"; printf 'abc1234\n' > "$d/sha"; shift; local i=1; for c in "$@"; do printf '%s' "$c" > "$d/checks.$i"; i=$((i+1)); done; echo "$d"; }
|
|
44
|
+
|
|
45
|
+
P='[{"name":"a","bucket":"pass"},{"name":"b","bucket":"skipping"}]'
|
|
46
|
+
expect "all pass/skipping" "0|CI_GATE: PASS" "$(run "$(mk pass "$P")")"
|
|
47
|
+
expect "one failure" "1|CI_GATE: FAIL" "$(run "$(mk fail '[{"name":"a","bucket":"pass"},{"name":"b","bucket":"fail"}]')")"
|
|
48
|
+
expect "cancelled counts as failure" "1|CI_GATE: FAIL" "$(run "$(mk cancel '[{"name":"a","bucket":"cancel"}]')")"
|
|
49
|
+
expect "pending then pass" "0|CI_GATE: PASS" "$(run "$(mk pend '[{"name":"a","bucket":"pending"}]' "$P")")"
|
|
50
|
+
expect "pending then fail" "1|CI_GATE: FAIL" "$(run "$(mk pendfail '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"fail"}]')")"
|
|
51
|
+
expect "pending past timeout" "3|CI_GATE: TIMEOUT" "$(run "$(mk slow '[{"name":"a","bucket":"pending"}]')" --timeout 2)"
|
|
52
|
+
expect "fail wins over pending" "1|CI_GATE: FAIL" "$(run "$(mk mixed '[{"name":"a","bucket":"pending"},{"name":"b","bucket":"fail"}]')")"
|
|
53
|
+
expect "transient outage then pass" "0|CI_GATE: PASS" "$(run "$(mk blip ERR "$P")")"
|
|
54
|
+
expect "persistent outage" "2|CI_GATE: ERROR" "$(run "$(mk down ERR)" --timeout 60)"
|
|
55
|
+
d=$(mk none NONE); touch "$d/nochecks"
|
|
56
|
+
expect "no checks after grace passes" "0|CI_GATE: PASS" "$(run "$d")"
|
|
57
|
+
expect "no checks + REQUIRE fails closed" "2|CI_GATE: ERROR" "$(FORGE_CI_REQUIRE_CHECKS=1 run "$d")"
|
|
58
|
+
d=$(mk moved '[{"name":"a","bucket":"pending"}]' "$P"); printf 'abc1234\nabc1234\ndef5678\n' > "$d/sha"
|
|
59
|
+
expect "head moved during wait" "2|CI_GATE: ERROR" "$(run "$d")"
|
|
60
|
+
expect "bad pr number" "2|CI_GATE: ERROR" "$(FAKE_DIR="$T/pass" PATH="$T/bin:$PATH" bash "$GATE" abc -R o/r 2>/dev/null | head -1 | sed 's/^/2|/')"
|
|
61
|
+
expect "missing repo" "2" "$(FAKE_DIR="$T/pass" PATH="$T/bin:$PATH" bash "$GATE" 7 >/dev/null 2>&1; echo $?)"
|
|
62
|
+
|
|
63
|
+
d=$(mk runsnochecks NONE); touch "$d/nochecks"; echo 3 > "$d/runs"
|
|
64
|
+
expect "no checks but workflow runs exist keeps waiting (timeout)" "3|CI_GATE: TIMEOUT" "$(run "$d" --timeout 3)"
|
|
65
|
+
expect "pass is held until the settle period" "0|CI_GATE: PASS" "$(SETTLE=2 run "$(mk settle "$P")")"
|
|
66
|
+
out=$(FAKE_DIR="$(mk headline "$P")" PATH="$T/bin:$PATH" FORGE_CI_INTERVAL=1 FORGE_CI_SETTLE=0 bash "$GATE" 7 -R o/r 2>/dev/null)
|
|
67
|
+
expect "every result names the gated head" "CI_GATE_HEAD: abc1234" "$(printf '%s\n' "$out" | grep '^CI_GATE_HEAD:')"
|
|
68
|
+
|
|
69
|
+
# Static guard: every autonomous `gh pr merge` command line in the pipeline specs must be preceded
|
|
70
|
+
# (within 25 lines) by a CI gate result check. Prose/manual instructions are not command lines.
|
|
71
|
+
for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
72
|
+
bad=$(awk '
|
|
73
|
+
{ line[NR]=$0 }
|
|
74
|
+
/^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge / {
|
|
75
|
+
ok=0; for (i=NR-1; i>0 && i>=NR-25; i--) if (line[i] ~ /CI_GATE_RC/) { ok=1; break }
|
|
76
|
+
if (!ok) print FILENAME ":" NR
|
|
77
|
+
}' "$HERE/../$spec")
|
|
78
|
+
expect "every autonomous merge in $spec is CI-gated" "" "$bad"
|
|
79
|
+
done
|
|
80
|
+
# Every autonomous merge pins the gated head (forge#3187): no merge line may omit --match-head-commit.
|
|
81
|
+
for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
82
|
+
unpinned=$(grep -nE '^[[:space:]]*(MERGE_RESULT=\$\(|MERGE_OUTPUT=\$\()?gh pr merge ' "$HERE/../$spec" | grep -v -- '--match-head-commit')
|
|
83
|
+
expect "every autonomous merge in $spec pins the gated head" "" "$unpinned"
|
|
84
|
+
done
|
|
85
|
+
# The gate's own default timeout stays under the 10-minute Bash tool limit (forge#3189).
|
|
86
|
+
expect "default gate timeout fits one Bash call" "540" "$(sed -n 's/.*TIMEOUT="\${FORGE_CI_TIMEOUT:-\([0-9]*\)}".*/\1/p' "$GATE")"
|
|
87
|
+
|
|
88
|
+
# The /work-on router never merges: every merge lives in a CI-gated phase.
|
|
89
|
+
if grep -qE '^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge ' "$HERE/../commands/work-on.md"; then FAILN=$((FAILN+1)); echo "FAIL: work-on.md router contains a merge command"; else PASS=$((PASS+1)); fi
|
|
90
|
+
|
|
91
|
+
# Reviewed-head guard: review-pr Phase 8 must refuse to merge a head other than the reviewed commit,
|
|
92
|
+
# and the work-on callers must route "stale review" to a re-review, never a manual merge.
|
|
93
|
+
grep -qF '${GATED_HEAD#"$REVIEWED_SHORT"}' "$HERE/../commands/review-pr.md" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: review-pr Phase 8 lacks reviewed-head guard"; }
|
|
94
|
+
for f in commands/work-on/review.md; do
|
|
95
|
+
grep -q 'stale review' "$HERE/../$f" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: $f does not route stale review"; }
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
echo "wait-ci-green tests: pass=$PASS fail=$FAILN"
|
|
99
|
+
[ "$FAILN" -eq 0 ]
|