forgedock 1.16.2 → 1.18.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/bin/engine/outcome-codes.mjs +114 -0
- package/bin/engine/phases.mjs +211 -100
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +20 -5
- package/bin/engine/telemetry-seam.mjs +62 -0
- package/bin/engine/telemetry-seam.test.mjs +60 -0
- package/bin/engine/telemetry.mjs +148 -0
- package/bin/engine/telemetry.test.mjs +120 -0
- package/bin/engine-cli.mjs +17 -1
- package/bin/engine.mjs +227 -48
- package/bin/forgedock.mjs +8 -2
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +220 -0
- package/bin/runner.mjs +70 -4
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +211 -0
- package/bin/stats/index.mjs +154 -0
- package/bin/stats/local-source.mjs +79 -0
- package/bin/stats/package.json +6 -0
- package/bin/stats/render.mjs +82 -0
- package/bin/stats/stats.test.mjs +312 -0
- package/commands/cleanup.md +90 -2
- package/commands/deploy-info.md +8 -4
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +28 -6
- package/commands/orchestrate/phase-5-cleanup.md +18 -2
- package/commands/pipeline-health/phase-1-context.md +2 -0
- package/commands/pipeline-health/phase-2-metrics.md +12 -5
- package/commands/pipeline-status.md +21 -1
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents.md +10 -2
- package/commands/review-pr-staging.md +196 -496
- package/commands/review-pr.md +512 -3134
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +13 -1
- package/commands/work-on/build/implement.md +28 -6
- package/commands/work-on/build/validate.md +5 -2
- package/commands/work-on/build.md +197 -145
- package/commands/work-on/close.md +66 -20
- package/commands/work-on/decompose.md +113 -34
- package/commands/work-on/investigate.md +15 -14
- package/commands/work-on/remediate.md +49 -19
- package/commands/work-on/review.md +7 -5
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +76 -24
- package/docs/OUTCOME-CODES.md +53 -0
- package/docs/RELEASE-NOTES.md +24 -0
- package/docs/STATS.md +63 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +19 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/packages/protocol/src/phases.js +5 -5
- package/scripts/builder-resume-match.test.sh +58 -0
- package/scripts/cap-findings.sh +110 -0
- package/scripts/cap-findings.test.sh +130 -0
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +65 -16
- package/scripts/classify-finding.test.sh +43 -1
- package/scripts/classify-grep-exit.sh +20 -0
- package/scripts/classify-grep-exit.test.sh +46 -0
- package/scripts/close-parent-c3.test.sh +123 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +97 -3
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/doctor-pipeline-state.sh +43 -0
- package/scripts/file-deferred-followups.sh +159 -0
- package/scripts/file-deferred-followups.test.sh +106 -0
- package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
- package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
- package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
- package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
- package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
- package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
- package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
- package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
- package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
- package/scripts/forge-root.test.sh +18 -11
- package/scripts/lib/fence-bootstrap.sh +4 -3
- package/scripts/lib/pr-issue-number.sh +18 -0
- package/scripts/lib/review-template-resolve.sh +41 -0
- package/scripts/lib/telemetry.sh +168 -0
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/remediate-liveness.sh +42 -6
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-finalize.sh +1427 -0
- package/scripts/review-finalize.test.sh +833 -0
- package/scripts/review-pr-delta-wiring.test.sh +19 -72
- package/scripts/review-pr-domain-map.test.sh +23 -13
- package/scripts/review-pr-gate2.test.sh +24 -65
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +56 -80
- package/scripts/review-prepare.sh +553 -0
- package/scripts/review-prepare.test.sh +288 -0
- package/scripts/review-roster.sh +186 -0
- package/scripts/review-roster.test.sh +185 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/trusted-comments.test.sh +9 -2
- package/scripts/wait-ci-green.sh +8 -1
- package/scripts/wait-ci-green.test.sh +3 -3
- package/templates/forge.yaml.minimal +9 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cost-report.test.sh — cases for scripts/cost-report.sh (forge#3824).
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
S="$HERE/cost-report.sh"
|
|
8
|
+
FIX="$HERE/fixtures/cost-report"
|
|
9
|
+
PASS=0; FAILN=0
|
|
10
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
11
|
+
ok() { PASS=$((PASS+1)); }
|
|
12
|
+
bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
|
|
13
|
+
has() { # name text needle
|
|
14
|
+
case "$2" in *"$3"*) ok ;; *) bad "$1 (missing: $3)"; printf '%s\n' "$2" | sed 's/^/ /' ;; esac
|
|
15
|
+
}
|
|
16
|
+
hasnt() { # name text needle
|
|
17
|
+
case "$2" in *"$3"*) bad "$1 (unexpected: $3)" ;; *) ok ;; esac
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
RUNS="$TMP/runs"; mkdir -p "$RUNS"
|
|
21
|
+
cp "$FIX"/*.jsonl "$RUNS"/
|
|
22
|
+
# Old logs carry no version: their mtime (UTC) is mapped to the release active at that time.
|
|
23
|
+
TZ=UTC touch -t 200201010000 "$RUNS/old-log-no-version.jsonl"
|
|
24
|
+
printf '1.0.0 1000000000\n2.0.0 1100000000\n' > "$TMP/releases.txt"
|
|
25
|
+
|
|
26
|
+
OUT=$(bash "$S" --runs-dir "$RUNS" --release-times "$TMP/releases.txt"); rc=$?
|
|
27
|
+
[ "$rc" -eq 0 ] && ok || bad "exit 0 on a valid runs dir (rc=$rc)"
|
|
28
|
+
|
|
29
|
+
# Repo split: o/a (3 finished runs) and o/b (1) are separate rows.
|
|
30
|
+
ROW_A=$(printf '%s\n' "$OUT" | grep '^| 2\.0\.0 | o/a |')
|
|
31
|
+
ROW_B=$(printf '%s\n' "$OUT" | grep '^| 2\.0\.0 | o/b |')
|
|
32
|
+
[ -n "$ROW_A" ] && ok || bad "row for 2.0.0 o/a"
|
|
33
|
+
[ -n "$ROW_B" ] && ok || bad "row for 2.0.0 o/b"
|
|
34
|
+
|
|
35
|
+
# o/a: unfinished (no terminal, reopened) and hydrated runs are excluded, so n=3.
|
|
36
|
+
has "o/a n=3 (unfinished and hydrated excluded)" "$ROW_A" "| 2.0.0 | o/a | 3 |"
|
|
37
|
+
# totals 330k, 560k, 100k: mean 330k, median 330k
|
|
38
|
+
has "o/a mean and median" "$ROW_A" "| 330k | 330k |"
|
|
39
|
+
# per-phase means divide by n: investigate 30k/3, build 390k/3, review 500k/3, remediate 40k/3, close 30k/3
|
|
40
|
+
has "o/a per-phase means" "$ROW_A" "| 10k | 130k | 167k | 13k | 10k |"
|
|
41
|
+
# 1 of 3 remediated, 2 of 3 merged (needs-human is not merged)
|
|
42
|
+
has "o/a remediated and merged rates" "$ROW_A" "| 33% | 67% |"
|
|
43
|
+
# per merged issue: 990k total over 2 merged runs
|
|
44
|
+
has "o/a tokens per merged issue" "$ROW_A" "| 495k |"
|
|
45
|
+
has "o/a low-n flag" "$ROW_A" "low-n (<10)"
|
|
46
|
+
has "o/b single run per-merged math" "$ROW_B" "| 2.0.0 | o/b | 1 | 1000k | 1000k |"
|
|
47
|
+
has "o/b per merged" "$ROW_B" "| 100% | 1000k |"
|
|
48
|
+
hasnt "unfinished usage never counted" "$OUT" "9999k"
|
|
49
|
+
|
|
50
|
+
# o/c: new-style log (PHASE_ATTEMPT_USAGE). Per-phase attempts are summed once: investigate 10k + build 100k + 50k
|
|
51
|
+
# = 160k; the cumulative PHASE_COMMIT (150k) and the PHASE_FAILED (100k) must not be added on top.
|
|
52
|
+
ROW_C=$(printf '%s\n' "$OUT" | grep '^| 2\.0\.0 | o/c |')
|
|
53
|
+
has "o/c attempt events not double counted" "$ROW_C" "| 2.0.0 | o/c | 1 | 160k | 160k |"
|
|
54
|
+
|
|
55
|
+
# Old log: version estimated from mtime (marked ~), repo unknown.
|
|
56
|
+
has "old-log fallback" "$OUT" "| 1.0.0~ | unknown | 1 | 70k |"
|
|
57
|
+
has "excluded footer" "$OUT" "Excluded: 2 unfinished, 1 without usage data"
|
|
58
|
+
|
|
59
|
+
# --min-n lowers the flag threshold.
|
|
60
|
+
OUT2=$(bash "$S" --runs-dir "$RUNS" --release-times "$TMP/releases.txt" --min-n 3)
|
|
61
|
+
has "min-n 3 marks o/a ok" "$(printf '%s\n' "$OUT2" | grep '^| 2\.0\.0 | o/a |')" "| ok |"
|
|
62
|
+
|
|
63
|
+
# --last / --versions filtering.
|
|
64
|
+
OUT3=$(bash "$S" --runs-dir "$RUNS" --release-times "$TMP/releases.txt" --last 1)
|
|
65
|
+
hasnt "--last 1 drops the older estimated version" "$OUT3" "1.0.0~"
|
|
66
|
+
has "--last 1 keeps the newest" "$OUT3" "| 2.0.0 | o/a |"
|
|
67
|
+
OUT4=$(bash "$S" --runs-dir "$RUNS" --release-times "$TMP/releases.txt" --versions 1.0.0)
|
|
68
|
+
has "--versions matches an estimated version" "$OUT4" "| 1.0.0~ |"
|
|
69
|
+
hasnt "--versions excludes others" "$OUT4" "| 2.0.0 |"
|
|
70
|
+
|
|
71
|
+
# Fail closed: missing runs dir exits 2; empty dir still reports (header only).
|
|
72
|
+
bash "$S" --runs-dir "$TMP/nope" >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "missing runs dir exits 2"
|
|
73
|
+
bash "$S" --bogus >/dev/null 2>&1; [ $? -eq 2 ] && ok || bad "unknown flag exits 2"
|
|
74
|
+
mkdir "$TMP/empty"
|
|
75
|
+
OUT5=$(bash "$S" --runs-dir "$TMP/empty" --release-times "$TMP/releases.txt"); rc=$?
|
|
76
|
+
[ "$rc" -eq 0 ] && ok || bad "empty runs dir exits 0"
|
|
77
|
+
has "empty dir footer" "$OUT5" "Excluded: 0 unfinished"
|
|
78
|
+
|
|
79
|
+
echo "cost-report.test: $PASS passed, $FAILN failed"
|
|
80
|
+
[ "$FAILN" -eq 0 ]
|
package/scripts/diff-size.sh
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# diff-size.sh — the ONE measurement of a build's diff size
|
|
2
|
+
# diff-size.sh — the ONE measurement of a build's diff size and scope (forge#3450, forge#3862).
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
#
|
|
6
6
|
# Usage: diff-size.sh --repo-path P --base B [--threshold N] [--exclude-glob G]...
|
|
7
|
+
# [--test-weight PCT] [--module-depth N] [--plan-files FILE] [--estimate N] [--overrun-pct PCT]
|
|
8
|
+
# The size is never a gate (forge#3862): review_weight scales review depth and the scope diff (--plan-files) flags
|
|
9
|
+
# files outside the architect plan. --threshold/over/cohesive are DEPRECATED (kept one release for old callers).
|
|
7
10
|
# Measures lines changed (added + deleted) by the index plus any earlier commits against the MERGE BASE of
|
|
8
11
|
# HEAD and origin/B (git diff --cached --numstat --no-renames $(git merge-base HEAD origin/B)), so commits that
|
|
9
12
|
# landed on the base after the branch point are never counted as this build's changes. Exit 2 when the merge
|
|
@@ -15,7 +18,7 @@
|
|
|
15
18
|
# build/ dist/ generated/ fixtures/ vendor/ are anchored to the repo root (so first-party dirs such as
|
|
16
19
|
# commands/work-on/build/ are counted); __generated__/ and __snapshots__/ match at any depth.
|
|
17
20
|
# Globs are data, never evaluated.
|
|
18
|
-
# Default globs (mirrored in docs/CONFIG.md under build.diff_size.exclude_globs):
|
|
21
|
+
# Default globs (mirrored in docs/CONFIG.md under build.scope.exclude_globs, deprecated alias build.diff_size.exclude_globs):
|
|
19
22
|
# lockfiles (package-lock.json yarn.lock pnpm-lock.yaml Cargo.lock poetry.lock uv.lock go.sum Gemfile.lock
|
|
20
23
|
# composer.lock), *.min.*, *.snap, root-anchored dist/ build/ generated/ fixtures/ vendor/, any-depth
|
|
21
24
|
# __generated__/ __snapshots__/
|
|
@@ -27,11 +30,25 @@
|
|
|
27
30
|
# by basename); cohesive_lines = the union (a new test file counts once). cohesive=true when diff_lines>0 and
|
|
28
31
|
# cohesive_lines*100 >= 70*diff_lines and diff_lines <= threshold*3/2 (integer math; threshold 0 -> false).
|
|
29
32
|
# Test lines are classified, never discounted from diff_lines.
|
|
33
|
+
# review_weight (forge#3862): ws_lines = lines changed with whitespace ignored (git diff -w; a pure re-indent is 0)
|
|
34
|
+
# over the counted (non-excluded) files; review_weight = ceil((ws_lines of non-test files + --test-weight percent
|
|
35
|
+
# (default 50) of ws_lines of test files)); modules = distinct directories (first --module-depth path segments,
|
|
36
|
+
# default 2) holding a file with at least one non-whitespace changed line. Keys ws_lines, review_weight, modules
|
|
37
|
+
# follow cohesive= and precede top=.
|
|
38
|
+
# Scope diff, emitted only with --plan-files and/or --estimate: --plan-files FILE lists the architect's planned files
|
|
39
|
+
# (one per line; a leading "- " or "* ", backticks and "./" are stripped; an entry ending in "/" is a directory prefix;
|
|
40
|
+
# blank and "#" lines are ignored). scope=no_baseline (no entries) | ok | out_of_plan; plan_files=N; out_of_plan_count=K;
|
|
41
|
+
# up to 50 "out_of_plan=<path>" lines (counted files absent from the plan). --estimate N with --overrun-pct P
|
|
42
|
+
# (default 200) sets overrun=true when ws_lines*100 > N*P (N=0 means no estimate: overrun=false). Unreadable
|
|
43
|
+
# --plan-files is exit 2 (fail closed: an unreadable plan is never "in plan").
|
|
30
44
|
# Exit: 0 measured (even when over); 2 usage or git failure with EMPTY stdout (fail closed: unknown is not zero).
|
|
31
45
|
|
|
32
46
|
set -euo pipefail
|
|
47
|
+
# Telemetry (forge#3834): best-effort decision record; never changes stdout, stderr or the exit code.
|
|
48
|
+
_TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
|
|
49
|
+
type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
|
|
33
50
|
|
|
34
|
-
REPO_PATH=""; BASE=""; THRESHOLD="1000"; EXTRA=()
|
|
51
|
+
REPO_PATH=""; BASE=""; THRESHOLD="1000"; EXTRA=(); TEST_WEIGHT="50"; MODULE_DEPTH="2"; PLAN_FILE=""; ESTIMATE=""; OVERRUN_PCT="200"
|
|
35
52
|
die() { echo "diff-size.sh: $*" >&2; exit 2; }
|
|
36
53
|
while [ $# -gt 0 ]; do
|
|
37
54
|
case "$1" in
|
|
@@ -39,12 +56,33 @@ while [ $# -gt 0 ]; do
|
|
|
39
56
|
--base) [ $# -ge 2 ] || die "missing value for $1"; BASE="$2"; shift 2 ;;
|
|
40
57
|
--threshold) [ $# -ge 2 ] || die "missing value for $1"; THRESHOLD="$2"; shift 2 ;;
|
|
41
58
|
--exclude-glob) [ $# -ge 2 ] || die "missing value for $1"; [ -z "$2" ] || EXTRA+=("$2"); shift 2 ;;
|
|
59
|
+
--test-weight) [ $# -ge 2 ] || die "missing value for $1"; TEST_WEIGHT="$2"; shift 2 ;;
|
|
60
|
+
--module-depth) [ $# -ge 2 ] || die "missing value for $1"; MODULE_DEPTH="$2"; shift 2 ;;
|
|
61
|
+
--plan-files) [ $# -ge 2 ] || die "missing value for $1"; PLAN_FILE="$2"; shift 2 ;;
|
|
62
|
+
--estimate) [ $# -ge 2 ] || die "missing value for $1"; ESTIMATE="$2"; shift 2 ;;
|
|
63
|
+
--overrun-pct) [ $# -ge 2 ] || die "missing value for $1"; OVERRUN_PCT="$2"; shift 2 ;;
|
|
42
64
|
*) die "unknown argument: $1" ;;
|
|
43
65
|
esac
|
|
44
66
|
done
|
|
45
67
|
[ -n "$REPO_PATH" ] && [ -d "$REPO_PATH" ] || die "--repo-path must be an existing directory"
|
|
46
68
|
[ -n "$BASE" ] || die "--base is required"
|
|
47
69
|
case "$THRESHOLD" in ""|*[!0-9]*) die "--threshold must be a non-negative integer" ;; esac
|
|
70
|
+
case "$TEST_WEIGHT" in ""|*[!0-9]*) die "--test-weight must be a non-negative integer percent" ;; esac
|
|
71
|
+
case "$MODULE_DEPTH" in ""|0|*[!0-9]*) die "--module-depth must be a positive integer" ;; esac
|
|
72
|
+
case "$OVERRUN_PCT" in ""|*[!0-9]*) die "--overrun-pct must be a non-negative integer percent" ;; esac
|
|
73
|
+
case "$ESTIMATE" in ""|*[!0-9]*) [ -z "$ESTIMATE" ] || die "--estimate must be a non-negative integer" ;; esac
|
|
74
|
+
PLAN=(); PLAN_SET=0
|
|
75
|
+
if [ -n "$PLAN_FILE" ]; then
|
|
76
|
+
[ -f "$PLAN_FILE" ] && [ -r "$PLAN_FILE" ] || die "--plan-files must be a readable file"
|
|
77
|
+
PLAN_SET=1
|
|
78
|
+
while IFS= read -r _l || [ -n "$_l" ]; do
|
|
79
|
+
_l="${_l#"${_l%%[![:space:]]*}"}"; _l="${_l%"${_l##*[![:space:]]}"}"
|
|
80
|
+
case "$_l" in ""|"#"*) continue ;; esac
|
|
81
|
+
case "$_l" in "- "*|"* "*) _l="${_l#??}" ;; esac
|
|
82
|
+
_l="${_l//\`/}"; _l="${_l#./}"
|
|
83
|
+
[ -n "$_l" ] && PLAN+=("$_l")
|
|
84
|
+
done < "$PLAN_FILE"
|
|
85
|
+
fi
|
|
48
86
|
|
|
49
87
|
DEFAULTS=(package-lock.json yarn.lock pnpm-lock.yaml Cargo.lock poetry.lock uv.lock go.sum Gemfile.lock composer.lock
|
|
50
88
|
'*.min.*' '*.snap' __generated__/ __snapshots__/)
|
|
@@ -99,6 +137,32 @@ while IFS= read -r -d $'\001' rec; do
|
|
|
99
137
|
fi
|
|
100
138
|
done <<< "$RAW"
|
|
101
139
|
|
|
140
|
+
# Whitespace-insensitive pass (forge#3862): review_weight and module count come from `git diff -w`.
|
|
141
|
+
RAWW="$(git -C "$REPO_PATH" diff --cached -w --numstat --no-renames -z "$MB" -- | tr '\0' '\001')" \
|
|
142
|
+
|| die "git diff -w against merge base $MB failed"
|
|
143
|
+
WSL=0; WS_NT=0; WS_T=0; MODS=$'\001'; NMODS=0
|
|
144
|
+
while IFS= read -r -d $'\001' rec; do
|
|
145
|
+
[ -n "$rec" ] || continue
|
|
146
|
+
a="${rec%%$'\t'*}"; rest="${rec#*$'\t'}"; d="${rest%%$'\t'*}"; path="${rest#*$'\t'}"
|
|
147
|
+
case "$a" in ''|*[!0-9]*) a=0 ;; esac
|
|
148
|
+
case "$d" in ''|*[!0-9]*) d=0 ;; esac
|
|
149
|
+
n=$((a + d))
|
|
150
|
+
excluded "$path" && continue
|
|
151
|
+
WSL=$((WSL + n))
|
|
152
|
+
if is_test "$path"; then WS_T=$((WS_T + n)); else WS_NT=$((WS_NT + n)); fi
|
|
153
|
+
if [ "$n" -gt 0 ]; then
|
|
154
|
+
dir="${path%/*}"; [ "$dir" = "$path" ] && dir="."
|
|
155
|
+
mod=""; _i=0; _rest="$dir"
|
|
156
|
+
while [ "$_i" -lt "$MODULE_DEPTH" ] && [ -n "$_rest" ]; do
|
|
157
|
+
_seg="${_rest%%/*}"; mod="${mod:+$mod/}$_seg"
|
|
158
|
+
case "$_rest" in */*) _rest="${_rest#*/}" ;; *) _rest="" ;; esac
|
|
159
|
+
_i=$((_i + 1))
|
|
160
|
+
done
|
|
161
|
+
case "$MODS" in *$'\001'"$mod"$'\001'*) ;; *) MODS="${MODS}${mod}"$'\001'; NMODS=$((NMODS + 1)) ;; esac
|
|
162
|
+
fi
|
|
163
|
+
done <<< "$RAWW"
|
|
164
|
+
REVIEW_WEIGHT=$(( (WS_NT * 100 + WS_T * TEST_WEIGHT + 99) / 100 ))
|
|
165
|
+
|
|
102
166
|
OVER=false
|
|
103
167
|
if [ "$THRESHOLD" -gt 0 ] && [ "$TOTAL" -gt "$THRESHOLD" ]; then OVER=true; fi
|
|
104
168
|
echo "diff_lines=$TOTAL"
|
|
@@ -112,6 +176,36 @@ echo "new_lines=$NEWL"
|
|
|
112
176
|
echo "test_lines=$TESTL"
|
|
113
177
|
echo "cohesive_lines=$COHL"
|
|
114
178
|
echo "cohesive=$COHESIVE"
|
|
179
|
+
echo "ws_lines=$WSL"
|
|
180
|
+
echo "review_weight=$REVIEW_WEIGHT"
|
|
181
|
+
echo "modules=$NMODS"
|
|
182
|
+
OOP_LIST=""; OOP=0; SCOPE=ok
|
|
183
|
+
if [ "$PLAN_SET" = 1 ] || [ -n "$ESTIMATE" ]; then
|
|
184
|
+
if [ "${#PLAN[@]}" -eq 0 ]; then SCOPE=no_baseline; else
|
|
185
|
+
while IFS= read -r -d $'\001' rec; do
|
|
186
|
+
[ -n "$rec" ] || continue
|
|
187
|
+
path="${rec#*$'\t'}"; path="${path#*$'\t'}"
|
|
188
|
+
excluded "$path" && continue
|
|
189
|
+
inplan=0
|
|
190
|
+
for _p in "${PLAN[@]}"; do
|
|
191
|
+
case "$_p" in */) case "$path" in "$_p"*) inplan=1; break ;; esac ;; *) [ "$path" = "$_p" ] && { inplan=1; break; } ;; esac
|
|
192
|
+
done
|
|
193
|
+
if [ "$inplan" = 0 ]; then OOP=$((OOP + 1)); OOP_LIST="${OOP_LIST}${path}"$'\n'; fi
|
|
194
|
+
done <<< "$RAW"
|
|
195
|
+
[ "$OOP" -gt 0 ] && SCOPE=out_of_plan
|
|
196
|
+
fi
|
|
197
|
+
OVERRUN=false
|
|
198
|
+
if [ -n "$ESTIMATE" ] && [ "$ESTIMATE" -gt 0 ] && [ $((WSL * 100)) -gt $((ESTIMATE * OVERRUN_PCT)) ]; then OVERRUN=true; fi
|
|
199
|
+
echo "scope=$SCOPE"
|
|
200
|
+
echo "plan_files=${#PLAN[@]}"
|
|
201
|
+
echo "out_of_plan_count=$OOP"
|
|
202
|
+
[ -n "$ESTIMATE" ] && { echo "estimate=$ESTIMATE"; echo "overrun=$OVERRUN"; }
|
|
203
|
+
fi
|
|
204
|
+
_res=OK; if [ "$COHESIVE" = true ]; then _res=COHESIVE; elif [ "$OVER" = true ]; then _res=OVER; fi
|
|
205
|
+
forge_emit diff_size lines="$TOTAL" excluded="$EXCL" threshold="$THRESHOLD" result="$_res" review_weight="$REVIEW_WEIGHT" modules="$NMODS" ws_lines="$WSL"
|
|
115
206
|
if [ -n "$LIST" ]; then
|
|
116
207
|
printf '%s' "$LIST" | sort -t' ' -k1,1nr | head -n 10 | sed 's/^/top=/' || true
|
|
117
208
|
fi
|
|
209
|
+
if [ -n "$OOP_LIST" ]; then
|
|
210
|
+
printf '%s' "$OOP_LIST" | head -n 50 | sed 's/^/out_of_plan=/' || true
|
|
211
|
+
fi
|
|
@@ -137,10 +137,67 @@ check "test name patterns" "$OUT" test_lines 30
|
|
|
137
137
|
reset; gen 20 > keep.py; gen 900 > package-lock.json; git add -A
|
|
138
138
|
OUT="$(run --threshold 1000)"; check "excluded not in new_lines" "$OUT" new_lines 20
|
|
139
139
|
|
|
140
|
-
# existing key order is unchanged: cohesion keys sit between over= and top=
|
|
140
|
+
# existing key order is unchanged: cohesion keys sit between over= and top=; review keys follow cohesive=
|
|
141
141
|
reset; gen 3 > s.txt; git add -A
|
|
142
142
|
KEYS="$(run | sed -n 's/^\([a-z_]*\)=.*/\1/p' | tr '\n' ' ')"
|
|
143
|
-
[ "$KEYS" = "diff_lines excluded_lines threshold over new_lines test_lines cohesive_lines cohesive top " ] && ok || bad "key order ($KEYS)"
|
|
143
|
+
[ "$KEYS" = "diff_lines excluded_lines threshold over new_lines test_lines cohesive_lines cohesive ws_lines review_weight modules top " ] && ok || bad "key order ($KEYS)"
|
|
144
|
+
|
|
145
|
+
# review_weight (forge#3862): a pure re-indent counts about 0, whatever its raw size
|
|
146
|
+
reset; mkdir -p svc; awk 'BEGIN{for(i=0;i<120;i++) print "if x:\n y" i}' > svc/h.py; git add -A; git commit -qm h; git push -q origin feat:staging 2>/dev/null
|
|
147
|
+
awk 'BEGIN{for(i=0;i<120;i++) print "if x:\n y" i}' > svc/h.py; git add -A
|
|
148
|
+
OUT="$(run)"; check "re-indent raw diff_lines" "$OUT" diff_lines 240; check "re-indent ws_lines" "$OUT" ws_lines 0
|
|
149
|
+
check "re-indent review_weight" "$OUT" review_weight 0; check "re-indent modules" "$OUT" modules 0
|
|
150
|
+
git reset -q --hard origin/staging
|
|
151
|
+
|
|
152
|
+
# tests are weighted (default 50%, configurable); ws_lines is unweighted
|
|
153
|
+
reset; mkdir -p src tests; gen 100 > src/m.py; gen 200 > tests/test_m.py; git add -A
|
|
154
|
+
OUT="$(run)"; check "weight ws_lines" "$OUT" ws_lines 300; check "weight default 50" "$OUT" review_weight 200
|
|
155
|
+
OUT="$(run --test-weight 25)"; check "weight 25" "$OUT" review_weight 150
|
|
156
|
+
OUT="$(run --test-weight 0)"; check "weight 0" "$OUT" review_weight 100
|
|
157
|
+
OUT="$(run --test-weight 100)"; check "weight 100" "$OUT" review_weight 300
|
|
158
|
+
reset; mkdir -p src; gen 3 > src/o.py; mkdir -p tests; gen 1 > tests/test_o.py; git add -A
|
|
159
|
+
OUT="$(run)"; check "weight rounds up" "$OUT" review_weight 4
|
|
160
|
+
|
|
161
|
+
# generated and vendored code is excluded from review_weight and modules
|
|
162
|
+
reset; gen 10 > keep.py; mkdir -p vendor generated; gen 900 > vendor/v.js; gen 900 > generated/g.js; gen 900 > package-lock.json; git add -A
|
|
163
|
+
OUT="$(run)"; check "excluded ws_lines" "$OUT" ws_lines 10; check "excluded review_weight" "$OUT" review_weight 10; check "excluded modules" "$OUT" modules 1
|
|
164
|
+
|
|
165
|
+
# module count: distinct directories at --module-depth
|
|
166
|
+
reset; mkdir -p a/b/c a/b/d a/e f; gen 5 > a/b/c/1.py; gen 5 > a/b/d/2.py; gen 5 > a/e/3.py; gen 5 > f/4.py; gen 5 > root.py; git add -A
|
|
167
|
+
OUT="$(run)"; check "modules depth 2" "$OUT" modules 4
|
|
168
|
+
OUT="$(run --module-depth 1)"; check "modules depth 1" "$OUT" modules 3
|
|
169
|
+
OUT="$(run --module-depth 3)"; check "modules depth 3" "$OUT" modules 5
|
|
170
|
+
|
|
171
|
+
# scope diff: in-plan, out-of-plan, prefix entries, formatting noise, no baseline, overrun
|
|
172
|
+
reset; mkdir -p src tests docs; gen 10 > src/a.py; gen 10 > src/b.py; gen 10 > tests/test_a.py; gen 10 > docs/x.md; git add -A
|
|
173
|
+
printf '%s\n' '# plan' '- `src/a.py`' '* ./src/b.py' '' 'tests/' > "$TMP/plan"
|
|
174
|
+
OUT="$(run --plan-files "$TMP/plan")"
|
|
175
|
+
check "scope out_of_plan" "$OUT" scope out_of_plan; check "scope count" "$OUT" out_of_plan_count 1
|
|
176
|
+
check "scope plan_files" "$OUT" plan_files 3; check "scope path" "$OUT" out_of_plan docs/x.md
|
|
177
|
+
printf '%s\n' src/a.py src/b.py tests/test_a.py docs/x.md > "$TMP/plan"
|
|
178
|
+
OUT="$(run --plan-files "$TMP/plan")"; check "scope ok" "$OUT" scope ok; check "scope ok count" "$OUT" out_of_plan_count 0
|
|
179
|
+
printf '%s\n' 'src/a.py' > "$TMP/plan"
|
|
180
|
+
OUT="$(run --plan-files "$TMP/plan")"; check "scope 3 out" "$OUT" out_of_plan_count 3
|
|
181
|
+
: > "$TMP/plan"; OUT="$(run --plan-files "$TMP/plan")"; check "empty plan is no_baseline" "$OUT" scope no_baseline; check "no_baseline count" "$OUT" out_of_plan_count 0
|
|
182
|
+
OUT="$(run)"; [ -z "$(val "$OUT" scope)" ] && ok || bad "scope keys absent without plan args"
|
|
183
|
+
# excluded files are never out-of-plan; a path is data (glob chars, spaces) not a pattern
|
|
184
|
+
reset; mkdir -p vendor; gen 5 > vendor/z.js; gen 5 > 'we ird[1].txt'; git add -A
|
|
185
|
+
printf '%s\n' 'src/a.py' > "$TMP/plan"
|
|
186
|
+
OUT="$(run --plan-files "$TMP/plan")"; check "excluded not out-of-plan" "$OUT" out_of_plan_count 1; check "path with space and glob quoted" "$OUT" out_of_plan 'we ird[1].txt'
|
|
187
|
+
printf '%s\n' '*.txt' > "$TMP/plan"; OUT="$(run --plan-files "$TMP/plan")"; check "plan entry is not a glob" "$OUT" out_of_plan_count 1
|
|
188
|
+
# overrun against the plan estimate (ws-insensitive actual)
|
|
189
|
+
reset; gen 500 > big.py; git add -A
|
|
190
|
+
OUT="$(run --estimate 200)"; check "overrun 2.5x" "$OUT" overrun true; check "overrun estimate" "$OUT" estimate 200; check "overrun no plan file is no_baseline" "$OUT" scope no_baseline
|
|
191
|
+
OUT="$(run --estimate 250)"; check "at 2x not overrun" "$OUT" overrun false
|
|
192
|
+
OUT="$(run --estimate 200 --overrun-pct 300)"; check "custom overrun pct" "$OUT" overrun false
|
|
193
|
+
OUT="$(run --estimate 0)"; check "estimate 0 is no estimate" "$OUT" overrun false
|
|
194
|
+
# unreadable plan fails closed; bad numeric args are usage errors
|
|
195
|
+
OUT="$(bash "$S" --repo-path "$TMP/w" --base staging --plan-files "$TMP/none" 2>/dev/null)"; rc=$?
|
|
196
|
+
{ [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "unreadable plan (rc=$rc)"
|
|
197
|
+
for bad_arg in "--test-weight x" "--module-depth 0" "--overrun-pct -1" "--estimate x"; do
|
|
198
|
+
OUT="$(bash "$S" --repo-path "$TMP/w" --base staging $bad_arg 2>/dev/null)"; rc=$?
|
|
199
|
+
{ [ "$rc" = 2 ] && [ -z "$OUT" ]; } && ok || bad "bad arg $bad_arg (rc=$rc)"
|
|
200
|
+
done
|
|
144
201
|
|
|
145
202
|
# fail closed: bad repo path, bad base, bad threshold, unknown arg, missing base
|
|
146
203
|
OUT="$(bash "$S" --repo-path "$TMP/nope" --base staging 2>/dev/null)"; rc=$?
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
# PRIOR_GIST annotations in recent issue comments)
|
|
50
50
|
# I9 Issues stuck in workflow:remediating beyond threshold (the state must
|
|
51
51
|
# never outlive its remediation run)
|
|
52
|
+
# I10 Issues in workflow:engine-error that also carry another in-flight
|
|
53
|
+
# workflow:* label, or that sit there beyond threshold
|
|
52
54
|
#
|
|
53
55
|
# /pipeline-health should consume this script's JSON output rather than
|
|
54
56
|
# re-discovering state from scratch.
|
|
@@ -357,6 +359,47 @@ while IFS= read -r row; do
|
|
|
357
359
|
fi
|
|
358
360
|
done < <(echo "$REMEDIATING" | jq -c '.[]')
|
|
359
361
|
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
# I10: Issues stuck in workflow:engine-error
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
# engine-error is a terminal projection: it must not coexist with another
|
|
366
|
+
# in-flight workflow:* label (stranded state) and must not sit unrecovered past
|
|
367
|
+
# the threshold. Same fail-inconclusive pattern as I9.
|
|
368
|
+
if ! ENGINE_ERRORS=$(gh issue list $GH_FLAG \
|
|
369
|
+
--state open --label "workflow:engine-error" \
|
|
370
|
+
--limit 100 \
|
|
371
|
+
--json number,title,updatedAt,labels 2>"$GH_STDERR_TMP"); then
|
|
372
|
+
echo "WARNING: gh issue list failed for workflow:engine-error — I10 check inconclusive, skipping (not treated as empty): $(cat "$GH_STDERR_TMP")" >&2
|
|
373
|
+
add_skip "I10" "all" "gh issue list failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
|
|
374
|
+
ENGINE_ERRORS="[]"
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
while IFS= read -r row; do
|
|
378
|
+
num=$(echo "$row" | jq -r '.number')
|
|
379
|
+
labels=$(echo "$row" | jq -c '[.labels[].name]')
|
|
380
|
+
if has_terminal_label "$labels"; then
|
|
381
|
+
continue
|
|
382
|
+
fi
|
|
383
|
+
updated=$(echo "$row" | jq -r '.updatedAt')
|
|
384
|
+
hours=$(hours_since "$updated")
|
|
385
|
+
other=$(echo "$labels" | jq -r '[.[] | select(startswith("workflow:") and . != "workflow:engine-error")] | first // empty')
|
|
386
|
+
if [ -n "$other" ]; then
|
|
387
|
+
add_finding \
|
|
388
|
+
"stuck_engine_error" "warning" \
|
|
389
|
+
"$num" "workflow:engine-error" "$hours" \
|
|
390
|
+
"FORGE:STATE" \
|
|
391
|
+
"forgedock run-issue $num --retry" \
|
|
392
|
+
"Issue #$num carries workflow:engine-error together with $other (stranded state). Remove the stale label or recover with forgedock run-issue $num --retry"
|
|
393
|
+
elif [ "$hours" -ge "$STUCK_HOURS" ]; then
|
|
394
|
+
add_finding \
|
|
395
|
+
"stuck_engine_error" "warning" \
|
|
396
|
+
"$num" "workflow:engine-error" "$hours" \
|
|
397
|
+
"FORGE:STATE" \
|
|
398
|
+
"forgedock run-issue $num --retry" \
|
|
399
|
+
"Issue #$num stuck in workflow:engine-error for ${hours}h (threshold: ${STUCK_HOURS}h). Recover with forgedock run-issue $num --retry"
|
|
400
|
+
fi
|
|
401
|
+
done < <(echo "$ENGINE_ERRORS" | jq -c '.[]')
|
|
402
|
+
|
|
360
403
|
# ---------------------------------------------------------------------------
|
|
361
404
|
# I4: Issues in workflow:building with no FORGE:BUILDER annotation
|
|
362
405
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# file-deferred-followups.sh — file ALL deferred out-of-scope items of one build as ONE follow-up issue.
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
#
|
|
6
|
+
# The builder decides fold-vs-defer (judgment, build.md B2.0); this script does the mechanics for the items
|
|
7
|
+
# that stay deferred: trusted lookup of an open follow-up carrying <!-- FORGE:DEFERRED_FROM: #N -->, append the
|
|
8
|
+
# missing items to it as checklist entries (exact path match), or create ONE issue with a checklist.
|
|
9
|
+
#
|
|
10
|
+
# Usage: file-deferred-followups.sh --parent N --repo OWNER/REPO [--items FILE|-] [--folded N]
|
|
11
|
+
# items TSV, one per line: <path> TAB <why> TAB <known-flaw flag: known-flaw|1|yes|true, else empty>
|
|
12
|
+
# Env: DRY_RUN (non-empty: no create/edit, issue number 0), FORGE_TRAIL_TRUSTED_ASSOCIATIONS (default
|
|
13
|
+
# OWNER,MEMBER,COLLABORATOR), FORGE_TRAIL_TRUSTED_LOGINS (default empty), FORGE_DEFERRED_RETRY_SLEEP (secs).
|
|
14
|
+
# Stdout: `path<TAB>issue` per item, then DEFERRED_TELEMETRY deferred_items=N folded=N batched_into=#M filed=N.
|
|
15
|
+
# Exit: 0 ok | 2 usage | 3 lookup failed (DEFERRED_LOOKUP=FAILED) | 4 create/append failed (DEFERRED_FILE=FAILED).
|
|
16
|
+
# A failed lookup is never "none found". bash 3.2 portable.
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
PARENT=""; REPO=""; ITEMS="-"; FOLDED=0
|
|
21
|
+
while [ $# -gt 0 ]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--parent|--repo|--items|--folded)
|
|
24
|
+
[ $# -ge 2 ] || { echo "file-deferred-followups: $1 requires a value" >&2; exit 2; }
|
|
25
|
+
case "$1" in --parent) PARENT="$2" ;; --repo) REPO="$2" ;; --items) ITEMS="$2" ;; --folded) FOLDED="$2" ;; esac
|
|
26
|
+
shift 2 ;;
|
|
27
|
+
*) echo "usage: file-deferred-followups.sh --parent N --repo OWNER/REPO [--items FILE|-] [--folded N]" >&2; exit 2 ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
case "$PARENT" in ''|*[!0-9]*) echo "file-deferred-followups: --parent must be a number" >&2; exit 2 ;; esac
|
|
31
|
+
case "$FOLDED" in ''|*[!0-9]*) FOLDED=0 ;; esac
|
|
32
|
+
[ -n "$REPO" ] || { echo "file-deferred-followups: --repo required" >&2; exit 2; }
|
|
33
|
+
if [ "$ITEMS" != "-" ] && [ ! -f "$ITEMS" ]; then echo "file-deferred-followups: items file not found: $ITEMS" >&2; exit 2; fi
|
|
34
|
+
|
|
35
|
+
TMP=$(mktemp -d "${TMPDIR:-/tmp}/deferred-followups.XXXXXX") || exit 2
|
|
36
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
37
|
+
if [ "$ITEMS" = "-" ]; then cat > "$TMP/items.raw"; else cat "$ITEMS" > "$TMP/items.raw"; fi
|
|
38
|
+
|
|
39
|
+
# Normalise: drop blank lines and duplicate paths (first wins), strip backticks from the path.
|
|
40
|
+
# Internal file uses a non-whitespace separator (US, \x1f): a tab IFS collapses an empty `why` and drops the known-flaw flag.
|
|
41
|
+
SEP=$(printf '\037')
|
|
42
|
+
: > "$TMP/items"
|
|
43
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
44
|
+
# Split on TAB by hand: `IFS=$'\t' read` would collapse an empty why field and shift the flag into it.
|
|
45
|
+
p="${line%%$'\t'*}"; rest=""; w=""; k=""
|
|
46
|
+
case "$line" in *$'\t'*) rest="${line#*$'\t'}"; w="${rest%%$'\t'*}"; case "$rest" in *$'\t'*) k="${rest#*$'\t'}" ;; esac ;; esac
|
|
47
|
+
p=$(printf '%s' "${p:-}" | tr -d '`' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
48
|
+
[ -n "$p" ] || continue
|
|
49
|
+
if cut -d "$SEP" -f1 "$TMP/items" | grep -qxF -- "$p"; then continue; fi
|
|
50
|
+
case "${k:-}" in known-flaw|1|yes|true) k="known-flaw" ;; *) k="" ;; esac
|
|
51
|
+
w=$(printf '%s' "${w:-}" | tr '\r\n' ' ')
|
|
52
|
+
printf '%s%s%s%s%s\n' "$p" "$SEP" "$w" "$SEP" "$k" >> "$TMP/items"
|
|
53
|
+
done < "$TMP/items.raw"
|
|
54
|
+
TOTAL=$(wc -l < "$TMP/items" | tr -d ' ')
|
|
55
|
+
|
|
56
|
+
MARKER="<!-- FORGE:DEFERRED_FROM: #${PARENT} -->"
|
|
57
|
+
TRUSTED_ASSOC="${FORGE_TRAIL_TRUSTED_ASSOCIATIONS-OWNER,MEMBER,COLLABORATOR}"
|
|
58
|
+
TRUSTED_LOGINS="${FORGE_TRAIL_TRUSTED_LOGINS-}"
|
|
59
|
+
|
|
60
|
+
finish() { # $1 = batched_into number or empty, $2 = filed count
|
|
61
|
+
local into="none"; [ -n "$1" ] && into="#$1"
|
|
62
|
+
local line="DEFERRED_TELEMETRY deferred_items=${TOTAL} folded=${FOLDED} batched_into=${into} filed=$2"
|
|
63
|
+
echo "$line"
|
|
64
|
+
# #3834 emitter (scripts/lib/telemetry.sh): optional, never blocks, never changes stdout or the exit code.
|
|
65
|
+
local d; d=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
66
|
+
if [ -f "$d/lib/telemetry.sh" ]; then
|
|
67
|
+
FORGE_ISSUE="${FORGE_ISSUE:-$PARENT}" FORGE_REPO="${FORGE_REPO:-$REPO}" \
|
|
68
|
+
bash "$d/lib/telemetry.sh" deferred_followups deferred_items="$TOTAL" folded="$FOLDED" \
|
|
69
|
+
batched_into="${1:-none}" filed="$2" >/dev/null 2>&1 </dev/null || true
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# --- Trusted lookup (author predicate applied before path match), one retry ---------------------------------
|
|
74
|
+
do_lookup() {
|
|
75
|
+
gh api -X GET search/issues -f per_page=50 \
|
|
76
|
+
-f q="\"FORGE:DEFERRED_FROM: #${PARENT}\" in:body repo:${REPO} is:issue is:open" 2>/dev/null \
|
|
77
|
+
| jq -c --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" '
|
|
78
|
+
($assoc | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $A
|
|
79
|
+
| ($logins | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $L
|
|
80
|
+
| [.items[]
|
|
81
|
+
| select(((.author_association // "") as $x | $A | index($x) != null)
|
|
82
|
+
or ((.user.type // "") == "Bot")
|
|
83
|
+
or ((.user.login // "") as $x | $L | index($x) != null))
|
|
84
|
+
| select((.body // "") | contains($m))
|
|
85
|
+
| {number, body: (.body // "")}]
|
|
86
|
+
| sort_by(.number)' 2>/dev/null
|
|
87
|
+
}
|
|
88
|
+
LOOKUP=""
|
|
89
|
+
for attempt in 1 2; do
|
|
90
|
+
LOOKUP=$(do_lookup) && [ -n "$LOOKUP" ] && break
|
|
91
|
+
LOOKUP=""
|
|
92
|
+
[ "$attempt" = 1 ] && sleep "${FORGE_DEFERRED_RETRY_SLEEP:-2}"
|
|
93
|
+
done
|
|
94
|
+
if [ -z "$LOOKUP" ]; then
|
|
95
|
+
echo "DEFERRED_LOOKUP=FAILED"
|
|
96
|
+
exit 3
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Checklist line for one item. Path always backticked so review-pr 6B.5 `contains` verification holds.
|
|
100
|
+
entry() { # path why flag
|
|
101
|
+
local l="- [ ] \`$1\`"
|
|
102
|
+
[ -n "$3" ] && l="$l **known-flaw**"
|
|
103
|
+
[ -n "$2" ] && l="$l — $2"
|
|
104
|
+
printf '%s\n' "$l"
|
|
105
|
+
}
|
|
106
|
+
has_entry() { # body-file path : exact checklist-line match, not substring
|
|
107
|
+
grep -qE '^- \[[ xX]\] `' "$1" 2>/dev/null || return 1
|
|
108
|
+
grep -F -- "\`$2\`" "$1" | grep -qE "^- \[[ xX]\] \`$(printf '%s' "$2" | sed 's/[][\.*^$+?(){}|]/\\&/g')\`( |\$)"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
NUM=$(printf '%s' "$LOOKUP" | jq -r 'first | .number // empty' 2>/dev/null)
|
|
112
|
+
|
|
113
|
+
if [ -n "$NUM" ]; then
|
|
114
|
+
# Re-read the body immediately before editing (parallel builds of the same parent), then append what is missing.
|
|
115
|
+
if ! gh api "repos/${REPO}/issues/${NUM}" --jq '.body // ""' > "$TMP/body" 2>/dev/null; then
|
|
116
|
+
echo "DEFERRED_FILE=FAILED"; exit 4
|
|
117
|
+
fi
|
|
118
|
+
: > "$TMP/add"
|
|
119
|
+
while IFS="$SEP" read -r p w k; do
|
|
120
|
+
has_entry "$TMP/body" "$p" || entry "$p" "$w" "$k" >> "$TMP/add"
|
|
121
|
+
done < "$TMP/items"
|
|
122
|
+
if [ -s "$TMP/add" ]; then
|
|
123
|
+
{ cat "$TMP/body"; printf '\n### Added by a later build of #%s\n\n' "$PARENT"; cat "$TMP/add"; } > "$TMP/newbody"
|
|
124
|
+
if [ -n "${DRY_RUN:-}" ]; then
|
|
125
|
+
echo "DRY_RUN: gh issue edit $NUM (append $(wc -l < "$TMP/add" | tr -d ' ') item(s))" >&2
|
|
126
|
+
elif ! gh issue edit "$NUM" -R "$REPO" --body-file "$TMP/newbody" >/dev/null 2>&1; then
|
|
127
|
+
echo "DEFERRED_FILE=FAILED"; exit 4
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$NUM"; done < "$TMP/items"
|
|
131
|
+
finish "$NUM" 0
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
[ "$TOTAL" -gt 0 ] || { finish "" 0; exit 0; }
|
|
136
|
+
|
|
137
|
+
# --- Create ONE issue with a checklist for every item ---------------------------------------------------------
|
|
138
|
+
{
|
|
139
|
+
printf '## Problem\n\n'
|
|
140
|
+
printf 'Deferred from #%s: the items below were declared out of scope in its contract and are tracked here, in one issue, instead of surfacing later as review findings.\n\n' "$PARENT"
|
|
141
|
+
printf '## Affected Files\n\n'
|
|
142
|
+
while IFS="$SEP" read -r p w k; do printf -- '- `%s`\n' "$p"; done < "$TMP/items"
|
|
143
|
+
printf '\n## Acceptance Criteria\n\n'
|
|
144
|
+
while IFS="$SEP" read -r p w k; do entry "$p" "$w" "$k"; done < "$TMP/items"
|
|
145
|
+
printf '\n%s\n' "$MARKER"
|
|
146
|
+
} > "$TMP/newissue"
|
|
147
|
+
|
|
148
|
+
if [ -n "${DRY_RUN:-}" ]; then
|
|
149
|
+
echo "DRY_RUN: gh issue create (follow-up: ${TOTAL} deferred item(s) from #${PARENT})" >&2
|
|
150
|
+
NUM=0
|
|
151
|
+
else
|
|
152
|
+
URL=$(gh issue create -R "$REPO" --title "follow-up: ${TOTAL} deferred item(s) (deferred from #${PARENT})" \
|
|
153
|
+
--body-file "$TMP/newissue" 2>/dev/null) || URL=""
|
|
154
|
+
NUM=$(printf '%s' "$URL" | grep -oE '[0-9]+$' | tail -n 1)
|
|
155
|
+
[ -n "$NUM" ] || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
156
|
+
fi
|
|
157
|
+
while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$NUM"; done < "$TMP/items"
|
|
158
|
+
finish "$NUM" 1
|
|
159
|
+
exit 0
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# file-deferred-followups.test.sh — stubbed-gh tests for scripts/file-deferred-followups.sh
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
S="$HERE/file-deferred-followups.sh"
|
|
8
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
9
|
+
mkdir "$T/bin"
|
|
10
|
+
export FORGE_DEFERRED_RETRY_SLEEP=0
|
|
11
|
+
export GH_LOG="$T/gh.log" SEARCH_JSON="$T/search.json" ISSUE_BODY="$T/issue.body"
|
|
12
|
+
|
|
13
|
+
cat > "$T/bin/gh" <<'MOCK'
|
|
14
|
+
#!/usr/bin/env bash
|
|
15
|
+
echo "$*" >> "$GH_LOG"
|
|
16
|
+
case "$1 $2" in
|
|
17
|
+
"api -X") [ -n "${LOOKUP_FAIL:-}" ] && exit 1; cat "$SEARCH_JSON"; exit 0 ;;
|
|
18
|
+
"issue create") [ -n "${CREATE_FAIL:-}" ] && exit 1; echo "https://github.com/o/r/issues/901"; exit 0 ;;
|
|
19
|
+
"issue edit")
|
|
20
|
+
[ -n "${EDIT_FAIL:-}" ] && exit 1
|
|
21
|
+
while [ $# -gt 0 ]; do [ "$1" = "--body-file" ] && cp "$2" "$ISSUE_BODY.edited"; shift; done; exit 0 ;;
|
|
22
|
+
esac
|
|
23
|
+
case "$1" in api) cat "$ISSUE_BODY"; exit 0 ;; esac
|
|
24
|
+
exit 1
|
|
25
|
+
MOCK
|
|
26
|
+
chmod +x "$T/bin/gh"; export PATH="$T/bin:$PATH"
|
|
27
|
+
|
|
28
|
+
PASS=0; FAIL=0
|
|
29
|
+
ok() { PASS=$((PASS+1)); echo "PASS: $1"; }
|
|
30
|
+
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1"; }
|
|
31
|
+
check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; }
|
|
32
|
+
reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited"; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL DRY_RUN; echo '{"items":[]}' > "$SEARCH_JSON"; }
|
|
33
|
+
printf 'a/x.sh\twhy a\t\nb/y.sh\twhy b\tknown-flaw\nc/z.sh\twhy c\t\n' > "$T/items.tsv"
|
|
34
|
+
|
|
35
|
+
# 1. N items -> exactly one create
|
|
36
|
+
reset
|
|
37
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" --folded 2); RC=$?
|
|
38
|
+
check "create: rc 0" "[ $RC -eq 0 ]"
|
|
39
|
+
check "create: one issue create for 3 items" "[ \$(grep -c '^issue create' '$GH_LOG') -eq 1 ]"
|
|
40
|
+
check "create: rows share issue" "[ \$(printf '%s\n' \"\$OUT\" | grep -c '901\$') -ge 3 ]"
|
|
41
|
+
check "create: telemetry" "printf '%s' \"\$OUT\" | grep -q 'DEFERRED_TELEMETRY deferred_items=3 folded=2 batched_into=#901 filed=1'"
|
|
42
|
+
|
|
43
|
+
# 2. append to existing trusted open issue; exact path match
|
|
44
|
+
reset
|
|
45
|
+
printf '## Affected Files\n- `a/x.sh`\n\n- [ ] `a/x.sh.bak` — other\n<!-- FORGE:DEFERRED_FROM: #10 -->\n' > "$ISSUE_BODY"
|
|
46
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
47
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
48
|
+
check "append: rc 0, no create" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG'"
|
|
49
|
+
check "append: telemetry batched into 77 filed=0" "printf '%s' \"\$OUT\" | grep -q 'batched_into=#77 filed=0'"
|
|
50
|
+
check "append: all three added (substring .bak does not dedup a/x.sh)" "[ \$(grep -c '^- \[ \] \`' '$ISSUE_BODY.edited') -eq 4 ]"
|
|
51
|
+
check "append: known-flaw flagged" "grep -q 'b/y.sh. \*\*known-flaw\*\*' '$ISSUE_BODY.edited'"
|
|
52
|
+
|
|
53
|
+
# 2b. empty why + known-flaw: the flag must survive (a whitespace IFS would collapse the empty field)
|
|
54
|
+
reset
|
|
55
|
+
printf 'd/w.sh\t\tknown-flaw\n' > "$T/emptywhy.tsv"
|
|
56
|
+
bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv" >/dev/null; RC=$?
|
|
57
|
+
rm -f "$ISSUE_BODY.edited"; printf 'x\n' > "$ISSUE_BODY"
|
|
58
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
59
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv"); RC=$?
|
|
60
|
+
check "empty-why: append keeps known-flaw flag" "[ $RC -eq 0 ] && grep -q 'd/w.sh. \*\*known-flaw\*\*' '$ISSUE_BODY.edited'"
|
|
61
|
+
|
|
62
|
+
# 2c. missing option value -> usage exit 2
|
|
63
|
+
bash "$S" --parent >/dev/null 2>&1; RC=$?
|
|
64
|
+
check "missing option value: rc 2" "[ $RC -eq 2 ]"
|
|
65
|
+
|
|
66
|
+
# 3. already-listed path is not duplicated; nothing to add -> no edit
|
|
67
|
+
reset
|
|
68
|
+
printf -- '- [ ] `a/x.sh` — w\n- [x] `b/y.sh`\n- [ ] `c/z.sh`\n' > "$ISSUE_BODY"
|
|
69
|
+
echo '{"items":[{"number":77,"author_association":"MEMBER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
70
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
71
|
+
check "idempotent: no edit, no create on resume" "[ $RC -eq 0 ] && ! grep -qE '^issue (edit|create)' '$GH_LOG'"
|
|
72
|
+
|
|
73
|
+
# 4. untrusted match ignored -> new issue
|
|
74
|
+
reset
|
|
75
|
+
echo '{"items":[{"number":66,"author_association":"NONE","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
76
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv")
|
|
77
|
+
check "untrusted: ignored, create filed" "grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'filed=1'"
|
|
78
|
+
|
|
79
|
+
# 5. lookup failure -> FAILED, nothing filed
|
|
80
|
+
reset; export LOOKUP_FAIL=1
|
|
81
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
82
|
+
check "lookup failure: rc 3, DEFERRED_LOOKUP=FAILED, no create" "[ $RC -eq 3 ] && printf '%s' \"\$OUT\" | grep -q DEFERRED_LOOKUP=FAILED && ! grep -q '^issue create' '$GH_LOG'"
|
|
83
|
+
|
|
84
|
+
# 6. create / edit failure not counted as success
|
|
85
|
+
reset; export CREATE_FAIL=1
|
|
86
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
87
|
+
check "create failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
|
|
88
|
+
reset; export EDIT_FAIL=1
|
|
89
|
+
printf 'x\n' > "$ISSUE_BODY"
|
|
90
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
91
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
92
|
+
check "edit failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
|
|
93
|
+
|
|
94
|
+
# 7. DRY_RUN
|
|
95
|
+
reset; export DRY_RUN=1
|
|
96
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null)
|
|
97
|
+
check "dry-run: no create" "! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'filed=1'"
|
|
98
|
+
|
|
99
|
+
# 8. empty items, usage errors
|
|
100
|
+
reset
|
|
101
|
+
OUT=$(printf '' | bash "$S" --parent 10 --repo o/r)
|
|
102
|
+
check "empty: no create, deferred_items=0" "! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'deferred_items=0'"
|
|
103
|
+
bash "$S" --repo o/r >/dev/null 2>&1; check "usage: missing parent rc 2" "[ $? -eq 2 ]"
|
|
104
|
+
|
|
105
|
+
echo "passed=$PASS failed=$FAIL"
|
|
106
|
+
[ "$FAIL" -eq 0 ]
|