forgedock 1.16.2 → 1.17.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 +92 -0
- package/bin/engine/phases.mjs +86 -26
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +13 -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 +12 -1
- package/bin/engine.mjs +145 -40
- package/bin/forgedock.mjs +7 -1
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +187 -0
- package/bin/runner.mjs +7 -0
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +140 -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 +224 -0
- package/commands/cleanup.md +83 -2
- package/commands/deploy-info.md +8 -4
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +17 -2
- 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 +102 -25
- package/commands/review-pr.md +119 -103
- package/commands/work-on/build/implement.md +26 -5
- package/commands/work-on/build/validate.md +3 -2
- package/commands/work-on/build.md +95 -64
- package/commands/work-on/close.md +17 -6
- package/commands/work-on/decompose.md +45 -4
- package/commands/work-on/investigate.md +6 -2
- package/commands/work-on/remediate.md +22 -6
- package/commands/work-on/review.md +2 -2
- package/docs/CONFIG.md +33 -0
- package/docs/OUTCOME-CODES.md +48 -0
- package/docs/RELEASE-NOTES.md +12 -0
- package/docs/STATS.md +62 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +13 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- 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-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 +54 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +5 -0
- 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 +8 -3
- 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/remediate-liveness.sh +42 -6
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-pr-domain-map.test.sh +8 -3
- package/scripts/review-pr-gate2.test.sh +15 -0
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +65 -5
- package/scripts/review-prepare.sh +517 -0
- package/scripts/review-prepare.test.sh +270 -0
- package/scripts/review-roster.sh +172 -0
- package/scripts/review-roster.test.sh +163 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/wait-ci-green.sh +8 -1
- package/templates/forge.yaml.minimal +9 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# cost-report.sh — pipeline token cost per ForgeDock version and repo (forge#3824).
|
|
6
|
+
#
|
|
7
|
+
# Reads the engine run logs (<runs-dir>/*.jsonl) and prints a markdown table, one row per
|
|
8
|
+
# (version, repo): finished-run count n, mean/median cache-read tokens per issue, mean per
|
|
9
|
+
# phase, remediated %, merged %, and cache-read tokens per merged issue.
|
|
10
|
+
#
|
|
11
|
+
# Rules:
|
|
12
|
+
# - A run counts only when finished: the last RUN_START..end slice of a log has a
|
|
13
|
+
# RUN_TERMINAL after any RUN_REOPEN / RUN_NEXT_CYCLE. A log is counted once.
|
|
14
|
+
# - Runs with no usage events, or with a hydrated (reconstructed) RUN_START, are excluded:
|
|
15
|
+
# they carry no usage and would drag the means down.
|
|
16
|
+
# - Tokens = usage.cache_read_input_tokens summed per phase over PHASE_ATTEMPT_USAGE when the
|
|
17
|
+
# phase has any (PHASE_COMMIT.usage is then cumulative, so it is not added again), else over
|
|
18
|
+
# PHASE_COMMIT and PHASE_FAILED (older logs).
|
|
19
|
+
# Per-phase means divide by n (a run that skipped a phase counts 0), so the phase
|
|
20
|
+
# columns add up to the per-issue mean. "merged" is RUN_TERMINAL.reason == "merged".
|
|
21
|
+
# - Version/repo come from RUN_START. Older logs lack them: repo becomes "unknown", and the
|
|
22
|
+
# version is estimated from the log's mtime against release times (marked with "~").
|
|
23
|
+
# - Groups with n < --min-n (default 10) are flagged low-n.
|
|
24
|
+
#
|
|
25
|
+
# Usage: bash scripts/cost-report.sh [--runs-dir DIR] [--last N] [--versions a,b] [--min-n N]
|
|
26
|
+
# [--release-times FILE]
|
|
27
|
+
# --runs-dir default $FORGE_RUNS_DIR, else ~/.forge/runs
|
|
28
|
+
# --last N only the N highest versions (unknown excluded)
|
|
29
|
+
# --versions comma list of versions (a trailing "~" in the data is ignored)
|
|
30
|
+
# --release-times file of "<version> <epoch-seconds>" lines; default: git tags v* of this repo
|
|
31
|
+
# Exit: 0 report printed (also when there are no finished runs) | 2 bad usage / missing runs dir
|
|
32
|
+
# Portable: bash 3.2, jq, POSIX stat/git.
|
|
33
|
+
|
|
34
|
+
set -u
|
|
35
|
+
|
|
36
|
+
RUNS_DIR="${FORGE_RUNS_DIR:-$HOME/.forge/runs}"
|
|
37
|
+
LAST=""
|
|
38
|
+
VERSIONS=""
|
|
39
|
+
MIN_N=10
|
|
40
|
+
REL_FILE=""
|
|
41
|
+
|
|
42
|
+
die() { echo "cost-report: $1" >&2; exit 2; }
|
|
43
|
+
is_uint() { case "$1" in ''|*[!0-9]*) return 1 ;; *) return 0 ;; esac; }
|
|
44
|
+
|
|
45
|
+
while [ $# -gt 0 ]; do
|
|
46
|
+
case "$1" in
|
|
47
|
+
--runs-dir) [ $# -ge 2 ] || die "--runs-dir needs a value"; RUNS_DIR="$2"; shift 2 ;;
|
|
48
|
+
--last) [ $# -ge 2 ] || die "--last needs a value"; LAST="$2"; shift 2 ;;
|
|
49
|
+
--versions) [ $# -ge 2 ] || die "--versions needs a value"; VERSIONS="$2"; shift 2 ;;
|
|
50
|
+
--min-n) [ $# -ge 2 ] || die "--min-n needs a value"; MIN_N="$2"; shift 2 ;;
|
|
51
|
+
--release-times) [ $# -ge 2 ] || die "--release-times needs a value"; REL_FILE="$2"; shift 2 ;;
|
|
52
|
+
-h|--help) sed -n '2,32p' "$0"; exit 0 ;;
|
|
53
|
+
*) die "unknown argument: $1" ;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
[ -d "$RUNS_DIR" ] || die "runs dir not found: $RUNS_DIR"
|
|
58
|
+
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
59
|
+
is_uint "$MIN_N" || die "--min-n must be a non-negative integer"
|
|
60
|
+
if [ -n "$LAST" ]; then is_uint "$LAST" || die "--last must be a non-negative integer"; fi
|
|
61
|
+
if [ -n "$REL_FILE" ] && [ ! -f "$REL_FILE" ]; then die "release-times file not found: $REL_FILE"; fi
|
|
62
|
+
|
|
63
|
+
# Release times: "<version> <epoch>" lines, from the file or this repo's v* tags (fail-soft: none).
|
|
64
|
+
if [ -n "$REL_FILE" ]; then
|
|
65
|
+
REL_RAW=$(cat "$REL_FILE")
|
|
66
|
+
else
|
|
67
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
68
|
+
REL_RAW=$(git -C "$ROOT" for-each-ref refs/tags --format='%(refname:short) %(creatordate:unix)' 2>/dev/null \
|
|
69
|
+
| sed -n 's/^v\([0-9][0-9.]*\) /\1 /p')
|
|
70
|
+
fi
|
|
71
|
+
REL_JSON=$(printf '%s\n' "$REL_RAW" | jq -R -s -c '
|
|
72
|
+
split("\n") | map(split(" ") | select(length == 2 and (.[1] | test("^[0-9]+$"))) | {v: .[0], t: (.[1] | tonumber)})
|
|
73
|
+
| sort_by(.t)')
|
|
74
|
+
[ -n "$REL_JSON" ] || REL_JSON='[]'
|
|
75
|
+
|
|
76
|
+
mtime_of() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0; }
|
|
77
|
+
|
|
78
|
+
# Pass 1: one summary object per log (empty for logs that cannot be parsed or have no RUN_START).
|
|
79
|
+
ROWS=""
|
|
80
|
+
for f in "$RUNS_DIR"/*.jsonl; do
|
|
81
|
+
[ -f "$f" ] || continue
|
|
82
|
+
mt=$(mtime_of "$f")
|
|
83
|
+
row=$(jq -c -s --argjson mt "$mt" '
|
|
84
|
+
. as $all
|
|
85
|
+
| ([range(0; length) | select($all[.].event == "RUN_START")] | last) as $s
|
|
86
|
+
| if $s == null then empty else
|
|
87
|
+
$all[$s:] as $ev
|
|
88
|
+
| $ev[0] as $start
|
|
89
|
+
| ([range(0; $ev | length) | select($ev[.].event == "RUN_TERMINAL")] | last) as $t
|
|
90
|
+
| ([range(0; $ev | length) | select($ev[.].event == "RUN_REOPEN" or $ev[.].event == "RUN_NEXT_CYCLE")] | last) as $r
|
|
91
|
+
| ([$ev[] | select(.event == "PHASE_ATTEMPT_USAGE") | .phase] | unique) as $ap
|
|
92
|
+
| [$ev[] | select(((.event == "PHASE_ATTEMPT_USAGE")
|
|
93
|
+
or ((.event == "PHASE_COMMIT" or .event == "PHASE_FAILED") and ((.phase as $p | $ap | index($p)) == null)))
|
|
94
|
+
and (.usage | type) == "object")] as $u
|
|
95
|
+
| {
|
|
96
|
+
version: ($start.version // null), repo: ($start.repo // null), mt: $mt,
|
|
97
|
+
hydrated: ($start.hydrated == true),
|
|
98
|
+
finished: ($t != null and ($r == null or $t > $r)),
|
|
99
|
+
reason: (if $t == null then null else $ev[$t].reason end),
|
|
100
|
+
usage_events: ($u | length),
|
|
101
|
+
remediated: ([$ev[] | select(.event == "PHASE_COMMIT" and .phase == "remediate")] | length > 0),
|
|
102
|
+
cr: ([$u[] | .usage.cache_read_input_tokens // 0] | add // 0),
|
|
103
|
+
out: ([$u[] | .usage.output_tokens // 0] | add // 0),
|
|
104
|
+
phase: ($u | group_by(.phase) | map({key: (.[0].phase // "other"), value: ([.[] | .usage.cache_read_input_tokens // 0] | add)}) | from_entries)
|
|
105
|
+
}
|
|
106
|
+
end' "$f" 2>/dev/null) || row=""
|
|
107
|
+
[ -n "$row" ] && ROWS="$ROWS$row
|
|
108
|
+
"
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
printf '%s' "$ROWS" | jq -s -r \
|
|
112
|
+
--argjson rel "$REL_JSON" --argjson minn "$MIN_N" \
|
|
113
|
+
--arg last "$LAST" --arg versions "$VERSIONS" '
|
|
114
|
+
def vkey: (sub("~$"; "") | split(".") | map(tonumber? // 0));
|
|
115
|
+
def mean: if length == 0 then 0 else add / length end;
|
|
116
|
+
def median: sort | if length == 0 then 0 elif length % 2 == 1 then .[length / 2 | floor] else (.[length / 2 - 1] + .[length / 2]) / 2 end;
|
|
117
|
+
def k: ((. / 1000) | round | tostring) + "k";
|
|
118
|
+
def pct: ((. * 100) | round | tostring) + "%";
|
|
119
|
+
def ver($mt): if .version != null then .version
|
|
120
|
+
else ([$rel[] | select(.t <= $mt)] | last | if . == null then "unknown" else .v + "~" end) end;
|
|
121
|
+
|
|
122
|
+
. as $rows
|
|
123
|
+
| ($rows | map(select(.finished | not)) | length) as $unfinished
|
|
124
|
+
| ($rows | map(select(.finished and (.hydrated or .usage_events == 0))) | length) as $nousage
|
|
125
|
+
| [ $rows[] | select(.finished and (.hydrated | not) and .usage_events > 0)
|
|
126
|
+
| . as $r | $r + {ver: ($r | ver($r.mt)), repo: ($r.repo // "unknown")} ] as $runs
|
|
127
|
+
| ($versions | split(",") | map(select(length > 0))) as $vf
|
|
128
|
+
| ($runs | map(.ver | select(. != "unknown")) | unique | sort_by(vkey)) as $vers
|
|
129
|
+
| (if $last == "" then $vers else ($vers | .[(length - ($last | tonumber) | if . < 0 then 0 else . end):]) end) as $keep
|
|
130
|
+
| [ $runs[] | select((.ver | IN($keep[])) or ($last == "" and .ver == "unknown"))
|
|
131
|
+
| select($vf == [] or ((.ver | sub("~$"; "")) | IN($vf[]))) ]
|
|
132
|
+
| group_by([.ver, .repo]) | sort_by(.[0].ver | vkey) as $groups
|
|
133
|
+
| "| Version | Repo | n | Cache-read/issue mean | median | investigate | build | review | remediate | close | Remediated | Merged | Per merged issue | Confidence |",
|
|
134
|
+
"|---|---|---|---|---|---|---|---|---|---|---|---|---|---|",
|
|
135
|
+
( $groups[] | . as $g | length as $n
|
|
136
|
+
| ([.[] | select(.reason == "merged")] | length) as $m
|
|
137
|
+
| (map(.cr) | add) as $tot
|
|
138
|
+
| def pm($p): (map(.phase[$p] // 0) | add) / $n;
|
|
139
|
+
"| \($g[0].ver) | \($g[0].repo) | \($n) | \((map(.cr) | mean) | k) | \((map(.cr) | median) | k)"
|
|
140
|
+
+ " | \(pm("investigate") | k) | \(pm("build") | k) | \(pm("review") | k) | \(pm("remediate") | k) | \(pm("close") | k)"
|
|
141
|
+
+ " | \(([.[] | select(.remediated)] | length) / $n | pct) | \($m / $n | pct)"
|
|
142
|
+
+ " | \(if $m == 0 then "n/a" else ($tot / $m | k) end)"
|
|
143
|
+
+ " | \(if $n < $minn then "low-n (<\($minn))" else "ok" end) |" ),
|
|
144
|
+
"",
|
|
145
|
+
"Finished runs only; tokens are cache-read per issue. Excluded: \($unfinished) unfinished, \($nousage) without usage data (hydrated or empty). \"~\" = version estimated from log mtime; \"unknown\" = no version available."
|
|
146
|
+
'
|
|
@@ -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
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
# Exit: 0 measured (even when over); 2 usage or git failure with EMPTY stdout (fail closed: unknown is not zero).
|
|
31
31
|
|
|
32
32
|
set -euo pipefail
|
|
33
|
+
# Telemetry (forge#3834): best-effort decision record; never changes stdout, stderr or the exit code.
|
|
34
|
+
_TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
|
|
35
|
+
type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
|
|
33
36
|
|
|
34
37
|
REPO_PATH=""; BASE=""; THRESHOLD="1000"; EXTRA=()
|
|
35
38
|
die() { echo "diff-size.sh: $*" >&2; exit 2; }
|
|
@@ -112,6 +115,8 @@ echo "new_lines=$NEWL"
|
|
|
112
115
|
echo "test_lines=$TESTL"
|
|
113
116
|
echo "cohesive_lines=$COHL"
|
|
114
117
|
echo "cohesive=$COHESIVE"
|
|
118
|
+
_res=OK; if [ "$COHESIVE" = true ]; then _res=COHESIVE; elif [ "$OVER" = true ]; then _res=OVER; fi
|
|
119
|
+
forge_emit diff_size lines="$TOTAL" excluded="$EXCL" threshold="$THRESHOLD" result="$_res"
|
|
115
120
|
if [ -n "$LIST" ]; then
|
|
116
121
|
printf '%s' "$LIST" | sort -t' ' -k1,1nr | head -n 10 | sed 's/^/top=/' || true
|
|
117
122
|
fi
|
|
@@ -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 ]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"seq":1,"event":"RUN_START","issue":1,"run":"r_1_staging","lane":"staging","version":"2.0.0","repo":"o/a"}
|
|
2
|
+
{"seq":2,"event":"PHASE_COMMIT","phase":"investigate","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":10000}}
|
|
3
|
+
{"seq":3,"event":"PHASE_COMMIT","phase":"build","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":100000}}
|
|
4
|
+
{"seq":4,"event":"PHASE_COMMIT","phase":"review","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":200000}}
|
|
5
|
+
{"seq":5,"event":"PHASE_COMMIT","phase":"close","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":20000}}
|
|
6
|
+
{"seq":6,"event":"RUN_TERMINAL","reason":"merged"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{"seq":1,"event":"RUN_START","issue":2,"run":"r_2_staging","lane":"staging","version":"2.0.0","repo":"o/a"}
|
|
2
|
+
{"seq":2,"event":"PHASE_COMMIT","phase":"investigate","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":10000}}
|
|
3
|
+
{"seq":3,"event":"PHASE_COMMIT","phase":"build","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":200000}}
|
|
4
|
+
{"seq":4,"event":"PHASE_COMMIT","phase":"review","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":300000}}
|
|
5
|
+
{"seq":5,"event":"PHASE_COMMIT","phase":"remediate","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":40000}}
|
|
6
|
+
{"seq":6,"event":"PHASE_COMMIT","phase":"close","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":10000}}
|
|
7
|
+
{"seq":7,"event":"RUN_TERMINAL","reason":"merged"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"seq":1,"event":"RUN_START","issue":3,"run":"r_3_staging","lane":"staging","version":"2.0.0","repo":"o/a"}
|
|
2
|
+
{"seq":2,"event":"PHASE_COMMIT","phase":"investigate","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":10000}}
|
|
3
|
+
{"seq":3,"event":"PHASE_COMMIT","phase":"build","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":90000}}
|
|
4
|
+
{"seq":4,"event":"RUN_TERMINAL","reason":"needs-human"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"seq":1,"event":"RUN_START","issue":4,"run":"r_4_staging","lane":"staging","version":"2.0.0","repo":"o/b"}
|
|
2
|
+
{"seq":2,"event":"PHASE_COMMIT","phase":"build","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":1000000}}
|
|
3
|
+
{"seq":3,"event":"RUN_TERMINAL","reason":"merged"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{"seq":1,"ts":"2026-10-10T00:00:00.000Z","event":"RUN_START","issue":5,"run":"r_5_staging","lane":"staging","version":"2.0.0","forge_version":"2.0.0","repo":"o/c"}
|
|
2
|
+
{"seq":2,"ts":"2026-10-10T00:00:01.000Z","event":"PHASE_ATTEMPT_USAGE","phase":"investigate","attempt":1,"usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":10000},"duration_ms":900,"cost_usd":null,"model":null,"turns":null}
|
|
3
|
+
{"seq":3,"ts":"2026-10-10T00:00:02.000Z","event":"PHASE_COMMIT","phase":"investigate","outputs":{},"usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":10000},"last_attempt_usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":10000}}
|
|
4
|
+
{"seq":4,"ts":"2026-10-10T00:00:03.000Z","event":"PHASE_ATTEMPT_USAGE","phase":"build","attempt":1,"usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":100000},"duration_ms":900,"cost_usd":null,"model":null,"turns":null}
|
|
5
|
+
{"seq":5,"ts":"2026-10-10T00:00:04.000Z","event":"PHASE_FAILED","phase":"build","attempt":1,"reason":"no commits","maxAttempts":3,"usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":100000}}
|
|
6
|
+
{"seq":6,"ts":"2026-10-10T00:00:05.000Z","event":"PHASE_ATTEMPT_USAGE","phase":"build","attempt":2,"usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":50000},"duration_ms":900,"cost_usd":null,"model":null,"turns":null}
|
|
7
|
+
{"seq":7,"ts":"2026-10-10T00:00:06.000Z","event":"PHASE_COMMIT","phase":"build","outputs":{},"usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":150000},"last_attempt_usage":{"input_tokens":1,"output_tokens":1,"cache_creation_input_tokens":0,"cache_read_input_tokens":50000}}
|
|
8
|
+
{"seq":8,"ts":"2026-10-10T00:00:07.000Z","event":"RUN_TERMINAL","reason":"merged"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"seq":1,"event":"RUN_START","issue":8,"run":"r_8_staging","lane":"staging"}
|
|
2
|
+
{"seq":2,"event":"PHASE_COMMIT","phase":"build","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":70000}}
|
|
3
|
+
{"seq":3,"event":"RUN_TERMINAL","reason":"merged"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"seq":1,"event":"RUN_START","issue":5,"run":"r_5_staging","lane":"staging","version":"2.0.0","repo":"o/a"}
|
|
2
|
+
{"seq":2,"event":"PHASE_COMMIT","phase":"build","outputs":{},"usage":{"input_tokens":10,"output_tokens":5,"cache_creation_input_tokens":1,"cache_read_input_tokens":9999000}}
|