forgedock 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/engine/phases.mjs +4 -2
- package/bin/engine/telemetry-seam.mjs +1 -1
- package/bin/engine-cli.mjs +40 -1
- package/bin/engine.mjs +48 -3
- package/bin/forgedock.mjs +1 -1
- package/bin/lean/effects.mjs +51 -0
- package/bin/lean/lean.mjs +352 -0
- package/bin/lean/lean.test.mjs +525 -0
- package/bin/lean/prompts.mjs +62 -0
- package/bin/lean/risk.mjs +49 -0
- package/bin/runner.mjs +99 -6
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr.md +7 -6
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/implement.md +9 -1
- package/commands/work-on/build/validate.md +14 -7
- package/commands/work-on/build.md +118 -8
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/remediate.md +11 -4
- package/commands/work-on/review.md +12 -4
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +8 -1
- package/docs/RELEASE-NOTES.md +21 -0
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +11 -3
- package/package.json +2 -2
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/ci-local/lib.mjs +11 -3
- package/scripts/ci-local/run.mjs +155 -49
- package/scripts/ci-local/scoping.mjs +27 -4
- package/scripts/ci-local.sh +3 -2
- package/scripts/ci-local.test.sh +189 -0
- package/scripts/classify-finding.sh +1 -1
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/lib/telemetry.sh +2 -0
- package/scripts/review-finalize.sh +101 -12
- package/scripts/review-finalize.test.sh +136 -5
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +8 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/verify-phase-trail.test.sh +6 -0
package/scripts/ci-local.test.sh
CHANGED
|
@@ -915,5 +915,194 @@ OUT=$(PATH="$T/bin:$PATH" bash "$S" run --repo "$r" --base main --base-branch ma
|
|
|
915
915
|
eq "FIX-3: the run passes with the job deferred" 0 "$RC"
|
|
916
916
|
hasnt "FIX-3: no container was started" "$(cat "$FAKE_DOCKER_LOG")" "run -d"
|
|
917
917
|
|
|
918
|
+
# ------------------------------------------------------------------------------------------------ forge#4057: concurrency
|
|
919
|
+
# SEC-3 universe: a single-test step is judged against the repo-wide tests, not the one test it runs
|
|
920
|
+
r=$(blank); mkdir -p "$r/scripts"
|
|
921
|
+
echo 'echo a' > "$r/scripts/a.sh"; echo 'bash scripts/a.sh' > "$r/scripts/a.test.sh"
|
|
922
|
+
echo 'echo x' > "$r/scripts/x.test.sh"; echo 'echo z' > "$r/scripts/zz.sh"
|
|
923
|
+
commit_base "$r"
|
|
924
|
+
_scope() { # _scope <command> <changed files...>
|
|
925
|
+
node -e "
|
|
926
|
+
import('$ROOT/scripts/ci-local/scoping.mjs').then(m=>{const [cmd,...c]=process.argv.slice(1);const s=m.makeScopeCtx({repo:'$r',baseRef:'main',from:'main',changed:c});
|
|
927
|
+
const r=m.scopeCommand(cmd,'',s);console.log(JSON.stringify({action:r.action,widen:r.widen_reason||''}))})" "$@"; }
|
|
928
|
+
echo '# c' >> "$r/scripts/a.sh"
|
|
929
|
+
R=$(_scope 'bash scripts/x.test.sh' scripts/a.sh)
|
|
930
|
+
eq "4057: an unrelated x.test.sh is skipped when a.sh has a sibling a.test.sh" skip "$(printf '%s' "$R" | jq -r '.action')" # edge_e2_unrelated_test_skipped
|
|
931
|
+
R=$(_scope 'bash scripts/a.test.sh' scripts/a.sh)
|
|
932
|
+
eq "4057: the sibling a.test.sh still runs" run "$(printf '%s' "$R" | jq -r '.action')"
|
|
933
|
+
echo '# c' >> "$r/scripts/zz.sh"
|
|
934
|
+
R=$(_scope 'bash scripts/x.test.sh' scripts/zz.sh)
|
|
935
|
+
has "4057: SEC-3 widen still fires when a changed source reaches no test anywhere" "$(printf '%s' "$R" | jq -r '.widen')" "empty-selection" # edge_e1_sec3_widen_still_fires
|
|
936
|
+
eq "4057: ...and the step runs" run "$(printf '%s' "$R" | jq -r '.action')"
|
|
937
|
+
R=$(_scope 'bash scripts/x.test.sh' scripts/a.sh scripts/zz.sh)
|
|
938
|
+
has "4057: one unreached source file among several still widens a single-test step" "$(printf '%s' "$R" | jq -r '.widen')" "scripts/zz.sh"
|
|
939
|
+
# SEC-3 universe is per ecosystem: a test in another language never covers a changed source (polyglot under-block)
|
|
940
|
+
r=$(blank); mkdir -p "$r/pkg" "$r/tests"
|
|
941
|
+
echo 'def f(): return 1' > "$r/pkg/utils.py"; echo 'def test_core(): assert True' > "$r/tests/test_core.py"
|
|
942
|
+
echo 'test("u", () => {})' > "$r/tests/utils.test.ts"
|
|
943
|
+
commit_base "$r"
|
|
944
|
+
echo '# c' >> "$r/pkg/utils.py"
|
|
945
|
+
R=$(_scope 'pytest tests/test_core.py' pkg/utils.py)
|
|
946
|
+
eq "SEC-3 ecosystem: a pytest step is not skipped because an unrelated .ts test matches the changed .py" run "$(printf '%s' "$R" | jq -r '.action')"
|
|
947
|
+
has "SEC-3 ecosystem: ...it widens and names the unreached source" "$(printf '%s' "$R" | jq -r '.widen')" "pkg/utils.py"
|
|
948
|
+
R=$(_scope 'pytest' pkg/utils.py)
|
|
949
|
+
eq "SEC-3 ecosystem: a bare pytest runner step widens too" run "$(printf '%s' "$R" | jq -r '.action')"
|
|
950
|
+
R=$(_scope 'python3 tests/test_core.py' pkg/utils.py)
|
|
951
|
+
eq "SEC-3 ecosystem: a single python test step widens (no py test reaches the change)" run "$(printf '%s' "$R" | jq -r '.action')"
|
|
952
|
+
# DRR-1: a single-language change never widens another ecosystem's runner step (it skips/narrows as at a0d45ae)
|
|
953
|
+
r=$(blank); mkdir -p "$r/pkg" "$r/tests"
|
|
954
|
+
echo 'def f(): return 1' > "$r/pkg/utils.py"; printf 'from pkg import utils\ndef test_core(): assert utils.f()\n' > "$r/tests/test_core.py"
|
|
955
|
+
echo 'import test from "node:test"; test("a", () => {})' > "$r/tests/a.test.mjs"
|
|
956
|
+
commit_base "$r"
|
|
957
|
+
echo '# c' >> "$r/pkg/utils.py"
|
|
958
|
+
R=$(_scope 'node --test tests/a.test.mjs' pkg/utils.py)
|
|
959
|
+
eq "DRR-1: a node step is not widened by a Python-only change" skip "$(printf '%s' "$R" | jq -r '.action')"
|
|
960
|
+
eq "DRR-1: ...and carries no widen reason" "" "$(printf '%s' "$R" | jq -r '.widen')"
|
|
961
|
+
R=$(_scope 'pytest tests/test_core.py' pkg/utils.py)
|
|
962
|
+
eq "DRR-1: the pytest step still runs the reaching test" run "$(printf '%s' "$R" | jq -r '.action')"
|
|
963
|
+
eq "DRR-1: ...scoped, not widened" "" "$(printf '%s' "$R" | jq -r '.widen')"
|
|
964
|
+
|
|
965
|
+
# FIFO slots (tickets), dead-owner reaping, deadline while queued
|
|
966
|
+
_slot() { FORGE_CI_LOCAL_HOME="$SLOT_HOME" FORGE_CI_LOCAL_SLOTS=1 node --input-type=module -e "
|
|
967
|
+
import('$ROOT/scripts/ci-local/run.mjs').then(async (m) => {
|
|
968
|
+
const [id, hold, budget] = process.argv.slice(1).map(Number);
|
|
969
|
+
const t0 = Date.now();
|
|
970
|
+
const rel = await m.acquireSlot('t' + id, t0 + budget);
|
|
971
|
+
if (!rel) { console.log('timeout ' + id); return; }
|
|
972
|
+
console.log('acq ' + id + (process.env.SLOT_SHOW_WAIT === '1' ? ' wait=' + (Date.now() - t0) : '')); await new Promise((r) => setTimeout(r, hold)); rel();
|
|
973
|
+
})" "$@"; }
|
|
974
|
+
SLOT_HOME="$T/fifo-home"; mkdir -p "$SLOT_HOME"; LOG="$T/fifo.log"; : > "$LOG"
|
|
975
|
+
# hold (1500ms) exceeds the cumulative spawn spacing (3 x 0.3s), so invocations 2-4 all queue behind invocation 1
|
|
976
|
+
for i in 1 2 3 4; do SLOT_SHOW_WAIT=1 _slot "$i" 1500 30000 >> "$LOG" 2>&1 & sleep 0.3; done; wait
|
|
977
|
+
eq "4057: 4 invocations, 1 slot: acquired strictly in ticket order, none starved" "acq 1 acq 2 acq 3 acq 4" "$(sed 's/ wait=.*//' "$LOG" | tr '\n' ' ' | sed 's/ $//')" # edge_e4_fifo_order
|
|
978
|
+
W1=$(sed -n 's/^acq 1 wait=//p' "$LOG")
|
|
979
|
+
case "$W1" in ''|*[!0-9]*) bad "4059: invocation 1 wait not recorded ('$W1')" ;; *) [ "$W1" -lt 500 ] && ok || bad "4059: invocation 1 acquired immediately (wait ${W1}ms, want < 500)" ;; esac
|
|
980
|
+
for i in 2 3 4; do # edge_e2_queued_waits
|
|
981
|
+
Wi=$(sed -n "s/^acq $i wait=//p" "$LOG")
|
|
982
|
+
case "$Wi" in ''|*[!0-9]*) bad "4059: invocation $i wait not recorded ('$Wi')" ;; *) [ "$Wi" -ge 500 ] && ok || bad "4059: invocation $i queued behind the holder (wait ${Wi}ms, want >= 500)" ;; esac
|
|
983
|
+
done
|
|
984
|
+
eq "4057: no ticket is left behind" 0 "$(ls "$SLOT_HOME/ci-local.lock/queue" | wc -l | tr -d ' ')"
|
|
985
|
+
SLOT_HOME="$T/fifo-home2"; mkdir -p "$SLOT_HOME/ci-local.lock/slot-0" "$SLOT_HOME/ci-local.lock/queue"
|
|
986
|
+
( : ) & DEAD=$!; wait "$DEAD" 2>/dev/null
|
|
987
|
+
printf '%s 1 ghost\n' "$DEAD" > "$SLOT_HOME/ci-local.lock/slot-0/owner"
|
|
988
|
+
: > "$SLOT_HOME/ci-local.lock/queue/00000000000001-000001-$DEAD-abcdef"
|
|
989
|
+
eq "4057: a dead slot owner and a dead earlier ticket are reaped, the queue is not blocked" "acq 9" "$(_slot 9 0 5000)" # edge_e3_dead_owner_reaped
|
|
990
|
+
eq "4057: no ticket left after a reaped acquire" 0 "$(ls "$SLOT_HOME/ci-local.lock/queue" | wc -l | tr -d ' ')"
|
|
991
|
+
mkdir -p "$SLOT_HOME/ci-local.lock/slot-0"; printf '%s %s000 live\n' "$$" "$(date +%s)" > "$SLOT_HOME/ci-local.lock/slot-0/owner"
|
|
992
|
+
eq "4057: the deadline while queued returns no slot" "timeout 8" "$(_slot 8 0 700)" # edge_e6_deadline_ticket_removed
|
|
993
|
+
eq "4057: ...and removes its ticket (no leak)" 0 "$(ls "$SLOT_HOME/ci-local.lock/queue" | wc -l | tr -d ' ')"
|
|
994
|
+
|
|
995
|
+
# single-flight dedup: identical check in two jobs runs once; a different env must not dedup
|
|
996
|
+
r=$(blank); mkdir -p "$r/.github/workflows"
|
|
997
|
+
cat > "$r/.github/workflows/ci.yml" <<'YML'
|
|
998
|
+
on: pull_request
|
|
999
|
+
jobs:
|
|
1000
|
+
a:
|
|
1001
|
+
runs-on: ubuntu-latest
|
|
1002
|
+
steps:
|
|
1003
|
+
- run: echo x >> counter.txt; sleep 1
|
|
1004
|
+
b:
|
|
1005
|
+
runs-on: ubuntu-latest
|
|
1006
|
+
steps:
|
|
1007
|
+
- run: echo x >> counter.txt; sleep 1
|
|
1008
|
+
c:
|
|
1009
|
+
runs-on: ubuntu-latest
|
|
1010
|
+
env:
|
|
1011
|
+
VARIANT: "1"
|
|
1012
|
+
steps:
|
|
1013
|
+
- run: echo x >> counter.txt; sleep 1
|
|
1014
|
+
YML
|
|
1015
|
+
echo s > "$r/s.txt"; commit_base "$r"; echo x >> "$r/s.txt"
|
|
1016
|
+
OUT=$(FORGE_CI_LOCAL_NO_CACHE=1 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
1017
|
+
eq "4057: dedup run passes" 0 "$RC"
|
|
1018
|
+
eq "4057: identical step ran once, the different-env step ran too (2 executions of 3 jobs)" 2 "$(wc -l < "$r/counter.txt" | tr -d ' ')" # edge_e8_different_env_not_deduped
|
|
1019
|
+
has "4057: the duplicate job reports dedup" "$OUT" "dedup=1"
|
|
1020
|
+
has "4057: the result line carries the dedup count and wait_ms" "$OUT" "wait_ms="
|
|
1021
|
+
r=$(blank); mkdir -p "$r/.github/workflows"
|
|
1022
|
+
cat > "$r/.github/workflows/ci.yml" <<'YML'
|
|
1023
|
+
on: pull_request
|
|
1024
|
+
jobs:
|
|
1025
|
+
a:
|
|
1026
|
+
runs-on: ubuntu-latest
|
|
1027
|
+
steps:
|
|
1028
|
+
- run: echo y >> fail.txt; sleep 1; exit 3
|
|
1029
|
+
b:
|
|
1030
|
+
runs-on: ubuntu-latest
|
|
1031
|
+
steps:
|
|
1032
|
+
- run: echo y >> fail.txt; sleep 1; exit 3
|
|
1033
|
+
YML
|
|
1034
|
+
echo s > "$r/s.txt"; commit_base "$r"; echo x >> "$r/s.txt"
|
|
1035
|
+
OUT=$(FORGE_CI_LOCAL_NO_CACHE=1 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
1036
|
+
eq "4057: a failing check shared by two jobs fails the gate" 1 "$RC"
|
|
1037
|
+
eq "4057: ...and was executed once, not re-run by the duplicate" 1 "$(wc -l < "$r/fail.txt" | tr -d ' ')" # edge_e5_failure_shared_once
|
|
1038
|
+
eq "4057: ...both jobs report the failure" 2 "$(printf '%s\n' "$OUT" | grep -c '^CI_LOCAL_JOB: fail ')"
|
|
1039
|
+
|
|
1040
|
+
# forge#4060: jobs with the same command but different service containers must not share a dedup result or a cached pass
|
|
1041
|
+
mksvc() { # $1 = repo, $2 = image of job b's postgres, $3 = job c's second-service ordering flag
|
|
1042
|
+
mkdir -p "$1/.github/workflows"
|
|
1043
|
+
cat > "$1/.github/workflows/ci.yml" <<YML
|
|
1044
|
+
on: pull_request
|
|
1045
|
+
jobs:
|
|
1046
|
+
a:
|
|
1047
|
+
runs-on: ubuntu-latest
|
|
1048
|
+
services:
|
|
1049
|
+
postgres:
|
|
1050
|
+
image: postgres:14
|
|
1051
|
+
steps:
|
|
1052
|
+
- run: echo x >> counter.txt; sleep 1
|
|
1053
|
+
b:
|
|
1054
|
+
runs-on: ubuntu-latest
|
|
1055
|
+
services:
|
|
1056
|
+
postgres:
|
|
1057
|
+
image: $2
|
|
1058
|
+
steps:
|
|
1059
|
+
- run: echo x >> counter.txt; sleep 1
|
|
1060
|
+
YML
|
|
1061
|
+
}
|
|
1062
|
+
r=$(blank); mksvc "$r" postgres:16
|
|
1063
|
+
echo s > "$r/s.txt"; commit_base "$r"; echo x >> "$r/s.txt"
|
|
1064
|
+
: > "$FAKE_DOCKER_LOG"
|
|
1065
|
+
OUT=$(PATH="$T/bin:$PATH" FORGE_CI_LOCAL_NO_CACHE=1 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
1066
|
+
eq "4060: different-services run passes" 0 "$RC"
|
|
1067
|
+
eq "4060: same command with different services ran in both jobs, no shared flight result" 2 "$(wc -l < "$r/counter.txt" | tr -d ' ')" # edge_e4_different_services_not_deduped
|
|
1068
|
+
hasnt "4060: no job reports dedup" "$OUT" "dedup=1"
|
|
1069
|
+
r=$(blank); mksvc "$r" postgres:14
|
|
1070
|
+
echo s > "$r/s.txt"; commit_base "$r"; echo x >> "$r/s.txt"
|
|
1071
|
+
OUT=$(PATH="$T/bin:$PATH" FORGE_CI_LOCAL_NO_CACHE=1 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat 2>&1); RC=$?
|
|
1072
|
+
eq "4060: identical services still dedup (one execution)" 1 "$(wc -l < "$r/counter.txt" | tr -d ' ')"
|
|
1073
|
+
has "4060: the identical-services duplicate reports dedup" "$OUT" "dedup=1"
|
|
1074
|
+
# persisted cache: a pass for services X is not a hit for services Y
|
|
1075
|
+
r=$(blank); mksvc "$r" postgres:16
|
|
1076
|
+
echo s > "$r/s.txt"; commit_base "$r"; echo x >> "$r/s.txt"
|
|
1077
|
+
OUT=$(PATH="$T/bin:$PATH" FORGE_CI_LOCAL_NO_CACHE=0 FORGE_CI_LOCAL_SLOTS=2 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat --max-parallel 1 2>&1)
|
|
1078
|
+
eq "4060: persisted cache is not shared across different service sets" 2 "$(wc -l < "$r/counter.txt" | tr -d ' ')" # edge_e5_cache_not_shared
|
|
1079
|
+
|
|
1080
|
+
# wait_ms is reported apart from duration; cached jobs wait for nothing; cheap jobs go first once durations are known
|
|
1081
|
+
r=$(blank); mkdir -p "$r/.github/workflows"
|
|
1082
|
+
cat > "$r/.github/workflows/ci.yml" <<'YML'
|
|
1083
|
+
on: pull_request
|
|
1084
|
+
jobs:
|
|
1085
|
+
big:
|
|
1086
|
+
runs-on: ubuntu-latest
|
|
1087
|
+
steps:
|
|
1088
|
+
- run: sleep 1.5 # ord-big
|
|
1089
|
+
small:
|
|
1090
|
+
runs-on: ubuntu-latest
|
|
1091
|
+
steps:
|
|
1092
|
+
- run: echo ord-small
|
|
1093
|
+
YML
|
|
1094
|
+
echo s > "$r/s.txt"; commit_base "$r"; echo x >> "$r/s.txt"
|
|
1095
|
+
OUT=$(FORGE_CI_LOCAL_NO_CACHE=0 FORGE_CI_LOCAL_SLOTS=1 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat --max-parallel 2 2>&1); RC=$?
|
|
1096
|
+
eq "4057: wait run passes" 0 "$RC"
|
|
1097
|
+
WM=$(printf '%s\n' "$OUT" | sed -n 's/^CI_LOCAL_RESULT:.* wait_ms=\([0-9]*\).*/\1/p')
|
|
1098
|
+
[ "${WM:-0}" -ge 500 ] && ok || bad "4057: one job queued for the single slot, so wait_ms is reported (got '${WM:-}')" "$OUT"
|
|
1099
|
+
has "4057: job lines carry wait=" "$OUT" " wait="
|
|
1100
|
+
OUT=$(FORGE_CI_LOCAL_NO_CACHE=0 FORGE_CI_LOCAL_SLOTS=1 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat --max-parallel 2 2>&1)
|
|
1101
|
+
has "4057: a fully cached job waits for no slot (wait=0.0s)" "$OUT" "cached=1 wait=0.0s" # edge_e7_cached_job_no_wait
|
|
1102
|
+
OUT=$(FORGE_CI_LOCAL_NO_CACHE=1 FORGE_CI_LOCAL_SLOTS=1 bash "$S" run --repo "$r" --base main --base-branch main --head-branch feat --max-parallel 1 2>&1)
|
|
1103
|
+
FIRST=$(printf '%s\n' "$OUT" | grep '^CI_LOCAL_JOB: pass' | head -n 1)
|
|
1104
|
+
has "4057: the cheapest known job runs first (small before big)" "$FIRST" "gha:ci:small"
|
|
1105
|
+
FORGE_TELEMETRY=off bash -c 'grep -q "wait_ms" "$0"' "$S" && ok || bad "4057: ci-local.sh forwards wait_ms to telemetry"
|
|
1106
|
+
|
|
918
1107
|
echo "ci-local.test.sh: $pass passed, $fail failed"
|
|
919
1108
|
[ "$fail" -eq 0 ]
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
# lists the deferred issue numbers confirmed open, one per line; a deferred item
|
|
57
57
|
# demotes only when its number is listed (closed, unreadable or absent: no demotion).
|
|
58
58
|
# The caller lists a number only after verifying it is an open issue (not a PR) whose
|
|
59
|
-
# body carries `FORGE:DEFERRED_FROM: #<merge issue>` and the path.
|
|
59
|
+
# body carries `FORGE:DEFERRED_FROM: #<merge issue>` and a checklist line for the exact path.
|
|
60
60
|
# `--merge-issue <N>` is the PR's linked issue: `deferred → #N` never demotes (that
|
|
61
61
|
# issue is always open during review, so it is no follow-up).
|
|
62
62
|
# Never demoted: CRITICAL/HIGH, `not-affected` items, safety-exempt findings
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
#
|
|
6
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
|
|
8
|
-
#
|
|
7
|
+
# that stay deferred: trusted lookup (claim marker <!-- FORGE:DEFERRED_ISSUE: #M --> on the parent, then a state-all search) of a follow-up carrying
|
|
8
|
+
# <!-- FORGE:DEFERRED_FROM: #N -->, append the missing items (exact path match; a closed one is reopened), or create ONE issue
|
|
9
|
+
# with a checklist, claim it on the parent and re-check once for a concurrent lower-numbered duplicate.
|
|
9
10
|
#
|
|
10
11
|
# Usage: file-deferred-followups.sh --parent N --repo OWNER/REPO [--items FILE|-] [--folded N]
|
|
11
12
|
# items TSV, one per line: <path> TAB <why> TAB <known-flaw flag: known-flaw|1|yes|true, else empty>
|
|
@@ -71,27 +72,60 @@ finish() { # $1 = batched_into number or empty, $2 = filed count
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
# --- Trusted lookup (author predicate applied before path match), one retry ---------------------------------
|
|
74
|
-
|
|
75
|
+
# Sources, in order: (1) claim markers `<!-- FORGE:DEFERRED_ISSUE: #M -->` on the parent's REST comments (read-after-write
|
|
76
|
+
# consistent), (2) a search over ALL issue states (eventually consistent, so only a fallback). Every candidate is then
|
|
77
|
+
# verified by number over REST (trusted author, DEFERRED_FROM marker, not a PR); the lowest number is canonical.
|
|
78
|
+
JQ_PRE='def lst: split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0));
|
|
79
|
+
def trusted: ((.author_association // "") as $x | ($assoc | lst) | index($x) != null)
|
|
80
|
+
or ((.user.type // "") == "Bot")
|
|
81
|
+
or ((.user.login // "") as $x | ($logins | lst) | index($x) != null);'
|
|
82
|
+
CLAIM_RE='^\s*<!-- FORGE:DEFERRED_ISSUE: #(?<n>[0-9]+) -->'
|
|
83
|
+
claim_nums() {
|
|
84
|
+
gh api --paginate "repos/${REPO}/issues/${PARENT}/comments" 2>/dev/null \
|
|
85
|
+
| jq -s -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg re "$CLAIM_RE" "$JQ_PRE"'
|
|
86
|
+
[.[][] | select(trusted) | (.body // "") | capture($re) | .n | tonumber] | unique | .[]' 2>/dev/null
|
|
87
|
+
}
|
|
88
|
+
search_nums() {
|
|
75
89
|
gh api -X GET search/issues -f per_page=50 \
|
|
76
|
-
-f q="\"FORGE:DEFERRED_FROM: #${PARENT}\" in:body repo:${REPO} is:issue
|
|
77
|
-
| jq -
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
|
90
|
+
-f q="\"FORGE:DEFERRED_FROM: #${PARENT}\" in:body repo:${REPO} is:issue" 2>/dev/null \
|
|
91
|
+
| jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
|
|
92
|
+
[.items[] | select(trusted) | select((.body // "") | contains($m)) | .number] | sort | .[]' 2>/dev/null
|
|
93
|
+
}
|
|
94
|
+
verify_num() { # number -> `number<TAB>state` when a trusted, non-PR issue carries the marker; empty when not (a 404 is "not it")
|
|
95
|
+
local out rc
|
|
96
|
+
out=$(gh api "repos/${REPO}/issues/$1" 2>"$TMP/verr"); rc=$?
|
|
97
|
+
if [ "$rc" -ne 0 ]; then grep -qE '404|Not Found|410' "$TMP/verr" 2>/dev/null && return 0; return 1; fi
|
|
98
|
+
printf '%s' "$out" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
|
|
99
|
+
select(.pull_request == null) | select(trusted) | select((.body // "") | contains($m))
|
|
100
|
+
| "\(.number)\t\(.state)"' 2>/dev/null
|
|
101
|
+
}
|
|
102
|
+
CANON=""; CSTATE=""
|
|
103
|
+
pick_verified() { # numbers... ; sets CANON/CSTATE from the first verified one; non-zero only when a fetch failed
|
|
104
|
+
local n r
|
|
105
|
+
for n in "$@"; do
|
|
106
|
+
r=$(verify_num "$n") || return 1
|
|
107
|
+
if [ -n "$r" ]; then CANON="${r%%$'\t'*}"; CSTATE="${r#*$'\t'}"; return 0; fi
|
|
108
|
+
done
|
|
109
|
+
return 0
|
|
87
110
|
}
|
|
88
|
-
|
|
111
|
+
find_canonical() {
|
|
112
|
+
local cands
|
|
113
|
+
CANON=""; CSTATE=""
|
|
114
|
+
cands=$(claim_nums) || return 1
|
|
115
|
+
# shellcheck disable=SC2086
|
|
116
|
+
pick_verified $cands || return 1
|
|
117
|
+
[ -n "$CANON" ] && return 0
|
|
118
|
+
cands=$(search_nums) || return 1
|
|
119
|
+
# shellcheck disable=SC2086
|
|
120
|
+
pick_verified $cands || return 1
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
LOOKUP_OK=""
|
|
89
124
|
for attempt in 1 2; do
|
|
90
|
-
|
|
91
|
-
LOOKUP=""
|
|
125
|
+
if find_canonical; then LOOKUP_OK=1; break; fi
|
|
92
126
|
[ "$attempt" = 1 ] && sleep "${FORGE_DEFERRED_RETRY_SLEEP:-2}"
|
|
93
127
|
done
|
|
94
|
-
if [ -z "$
|
|
128
|
+
if [ -z "$LOOKUP_OK" ]; then
|
|
95
129
|
echo "DEFERRED_LOOKUP=FAILED"
|
|
96
130
|
exit 3
|
|
97
131
|
fi
|
|
@@ -108,27 +142,35 @@ has_entry() { # body-file path : exact checklist-line match, not substring
|
|
|
108
142
|
grep -F -- "\`$2\`" "$1" | grep -qE "^- \[[ xX]\] \`$(printf '%s' "$2" | sed 's/[][\.*^$+?(){}|]/\\&/g')\`( |\$)"
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
# Append the missing items to follow-up $1 (state $2), then reopen it when closed (review-finalize needs it open).
|
|
146
|
+
append_items() { # number state ; non-zero = failure
|
|
147
|
+
local num="$1" state="$2" note=""
|
|
114
148
|
# Re-read the body immediately before editing (parallel builds of the same parent), then append what is missing.
|
|
115
|
-
|
|
116
|
-
echo "DEFERRED_FILE=FAILED"; exit 4
|
|
117
|
-
fi
|
|
149
|
+
gh api "repos/${REPO}/issues/${num}" --jq '.body // ""' > "$TMP/body" 2>/dev/null || return 1
|
|
118
150
|
: > "$TMP/add"
|
|
119
151
|
while IFS="$SEP" read -r p w k; do
|
|
120
152
|
has_entry "$TMP/body" "$p" || entry "$p" "$w" "$k" >> "$TMP/add"
|
|
121
153
|
done < "$TMP/items"
|
|
154
|
+
[ "$state" = "closed" ] && note=" (follow-up was closed; reopened)"
|
|
122
155
|
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"
|
|
156
|
+
{ cat "$TMP/body"; printf '\n### Added by a later build of #%s%s\n\n' "$PARENT" "$note"; cat "$TMP/add"; } > "$TMP/newbody"
|
|
124
157
|
if [ -n "${DRY_RUN:-}" ]; then
|
|
125
|
-
echo "DRY_RUN: gh issue edit $
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
echo "DRY_RUN: gh issue edit $num (append $(wc -l < "$TMP/add" | tr -d ' ') item(s))" >&2
|
|
159
|
+
else
|
|
160
|
+
gh issue edit "$num" -R "$REPO" --body-file "$TMP/newbody" >/dev/null 2>&1 || return 1
|
|
128
161
|
fi
|
|
129
162
|
fi
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
if [ "$state" = "closed" ]; then
|
|
164
|
+
if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: gh issue reopen $num" >&2
|
|
165
|
+
else gh issue reopen "$num" -R "$REPO" >/dev/null 2>&1 || return 1; fi
|
|
166
|
+
fi
|
|
167
|
+
return 0
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if [ -n "$CANON" ]; then
|
|
171
|
+
append_items "$CANON" "$CSTATE" || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
172
|
+
while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$CANON"; done < "$TMP/items"
|
|
173
|
+
finish "$CANON" 0
|
|
132
174
|
exit 0
|
|
133
175
|
fi
|
|
134
176
|
|
|
@@ -154,6 +196,25 @@ else
|
|
|
154
196
|
NUM=$(printf '%s' "$URL" | grep -oE '[0-9]+$' | tail -n 1)
|
|
155
197
|
[ -n "$NUM" ] || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
156
198
|
fi
|
|
199
|
+
if [ -z "${DRY_RUN:-}" ]; then
|
|
200
|
+
# Claim the follow-up on the parent (REST comments are read-after-write consistent; best effort, the search still finds it).
|
|
201
|
+
gh issue comment "$PARENT" -R "$REPO" --body "<!-- FORGE:DEFERRED_ISSUE: #${NUM} -->" >/dev/null 2>&1 \
|
|
202
|
+
|| echo "file-deferred-followups: warning: could not post claim marker for #${NUM} on #${PARENT}" >&2
|
|
203
|
+
# Post-create re-check: a concurrent build may have filed first. The lowest trusted number is canonical; the newer one is closed.
|
|
204
|
+
if find_canonical; then
|
|
205
|
+
if [ -n "$CANON" ] && [ "$CANON" -lt "$NUM" ]; then
|
|
206
|
+
if append_items "$CANON" "$CSTATE"; then
|
|
207
|
+
gh issue close "$NUM" -R "$REPO" --reason "not planned" --comment "Duplicate of #${CANON}: deferred items of #${PARENT} are tracked there." >/dev/null 2>&1 \
|
|
208
|
+
|| echo "file-deferred-followups: warning: could not close duplicate #${NUM}" >&2
|
|
209
|
+
NUM="$CANON"
|
|
210
|
+
else
|
|
211
|
+
echo "file-deferred-followups: warning: could not move items to #${CANON}; keeping #${NUM}" >&2
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
else
|
|
215
|
+
echo "file-deferred-followups: warning: post-create re-check failed; keeping #${NUM}" >&2
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
157
218
|
while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$NUM"; done < "$TMP/items"
|
|
158
219
|
finish "$NUM" 1
|
|
159
220
|
exit 0
|
|
@@ -8,19 +8,27 @@ S="$HERE/file-deferred-followups.sh"
|
|
|
8
8
|
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
9
9
|
mkdir "$T/bin"
|
|
10
10
|
export FORGE_DEFERRED_RETRY_SLEEP=0
|
|
11
|
-
export GH_LOG="$T/gh.log" SEARCH_JSON="$T/search.json" ISSUE_BODY="$T/issue.body"
|
|
11
|
+
export GH_LOG="$T/gh.log" SEARCH_JSON="$T/search.json" ISSUE_BODY="$T/issue.body" COMMENTS_JSON="$T/comments.json" T_DIR="$T"
|
|
12
12
|
|
|
13
13
|
cat > "$T/bin/gh" <<'MOCK'
|
|
14
14
|
#!/usr/bin/env bash
|
|
15
15
|
echo "$*" >> "$GH_LOG"
|
|
16
16
|
case "$1 $2" in
|
|
17
17
|
"api -X") [ -n "${LOOKUP_FAIL:-}" ] && exit 1; cat "$SEARCH_JSON"; exit 0 ;;
|
|
18
|
+
"api --paginate") [ -n "${CLAIM_FAIL:-}" ] && exit 1; cat "$COMMENTS_JSON"; exit 0 ;;
|
|
18
19
|
"issue create") [ -n "${CREATE_FAIL:-}" ] && exit 1; echo "https://github.com/o/r/issues/901"; exit 0 ;;
|
|
20
|
+
"issue comment") [ -n "${COMMENT_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
21
|
+
"issue reopen") [ -n "${REOPEN_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
22
|
+
"issue close") [ -n "${CLOSE_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
19
23
|
"issue edit")
|
|
20
24
|
[ -n "${EDIT_FAIL:-}" ] && exit 1
|
|
21
25
|
while [ $# -gt 0 ]; do [ "$1" = "--body-file" ] && cp "$2" "$ISSUE_BODY.edited"; shift; done; exit 0 ;;
|
|
22
26
|
esac
|
|
23
|
-
|
|
27
|
+
if [ "$1" = api ]; then
|
|
28
|
+
case "$*" in *--jq*) cat "$ISSUE_BODY"; exit 0 ;; esac
|
|
29
|
+
n="${2##*/}"; [ -f "$T_DIR/issue.$n.json" ] && { cat "$T_DIR/issue.$n.json"; exit 0; }
|
|
30
|
+
echo "HTTP 404: Not Found" >&2; exit 1
|
|
31
|
+
fi
|
|
24
32
|
exit 1
|
|
25
33
|
MOCK
|
|
26
34
|
chmod +x "$T/bin/gh"; export PATH="$T/bin:$PATH"
|
|
@@ -29,7 +37,12 @@ PASS=0; FAIL=0
|
|
|
29
37
|
ok() { PASS=$((PASS+1)); echo "PASS: $1"; }
|
|
30
38
|
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1"; }
|
|
31
39
|
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
|
|
40
|
+
reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited" "$T"/issue.*.json; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL CLAIM_FAIL COMMENT_FAIL REOPEN_FAIL CLOSE_FAIL DRY_RUN
|
|
41
|
+
echo '{"items":[]}' > "$SEARCH_JSON"; echo '[]' > "$COMMENTS_JSON"; }
|
|
42
|
+
# issue_json NUMBER STATE ASSOCIATION [PARENT]: REST view of a follow-up carrying the DEFERRED_FROM marker
|
|
43
|
+
issue_json() { printf '{"number":%s,"state":"%s","author_association":"%s","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #%s -->"}\n' "$1" "$2" "$3" "${4:-10}" > "$T/issue.$1.json"; }
|
|
44
|
+
# claim_json ASSOCIATION NUMBER...: parent comments carrying claim markers
|
|
45
|
+
claim_json() { local a="$1" out="" n; shift; for n in "$@"; do out="${out:+$out,}{\"author_association\":\"$a\",\"user\":{\"type\":\"User\"},\"body\":\"<!-- FORGE:DEFERRED_ISSUE: #$n -->\"}"; done; echo "[$out]" > "$COMMENTS_JSON"; }
|
|
33
46
|
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
47
|
|
|
35
48
|
# 1. N items -> exactly one create
|
|
@@ -43,7 +56,7 @@ check "create: telemetry" "printf '%s' \"\$OUT\" | grep -q 'DEFERRED_TELEMETRY d
|
|
|
43
56
|
# 2. append to existing trusted open issue; exact path match
|
|
44
57
|
reset
|
|
45
58
|
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"
|
|
59
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
47
60
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
48
61
|
check "append: rc 0, no create" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG'"
|
|
49
62
|
check "append: telemetry batched into 77 filed=0" "printf '%s' \"\$OUT\" | grep -q 'batched_into=#77 filed=0'"
|
|
@@ -55,7 +68,7 @@ reset
|
|
|
55
68
|
printf 'd/w.sh\t\tknown-flaw\n' > "$T/emptywhy.tsv"
|
|
56
69
|
bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv" >/dev/null; RC=$?
|
|
57
70
|
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"
|
|
71
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
59
72
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv"); RC=$?
|
|
60
73
|
check "empty-why: append keeps known-flaw flag" "[ $RC -eq 0 ] && grep -q 'd/w.sh. \*\*known-flaw\*\*' '$ISSUE_BODY.edited'"
|
|
61
74
|
|
|
@@ -66,13 +79,13 @@ check "missing option value: rc 2" "[ $RC -eq 2 ]"
|
|
|
66
79
|
# 3. already-listed path is not duplicated; nothing to add -> no edit
|
|
67
80
|
reset
|
|
68
81
|
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"
|
|
82
|
+
echo '{"items":[{"number":77,"author_association":"MEMBER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
70
83
|
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
71
84
|
check "idempotent: no edit, no create on resume" "[ $RC -eq 0 ] && ! grep -qE '^issue (edit|create)' '$GH_LOG'"
|
|
72
85
|
|
|
73
86
|
# 4. untrusted match ignored -> new issue
|
|
74
87
|
reset
|
|
75
|
-
echo '{"items":[{"number":66,"author_association":"NONE","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
88
|
+
echo '{"items":[{"number":66,"author_association":"NONE","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 66 open NONE; issue_json 77 open OWNER
|
|
76
89
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv")
|
|
77
90
|
check "untrusted: ignored, create filed" "grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'filed=1'"
|
|
78
91
|
|
|
@@ -87,7 +100,7 @@ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
|
87
100
|
check "create failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
|
|
88
101
|
reset; export EDIT_FAIL=1
|
|
89
102
|
printf 'x\n' > "$ISSUE_BODY"
|
|
90
|
-
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
103
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
91
104
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
92
105
|
check "edit failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
|
|
93
106
|
|
|
@@ -102,5 +115,85 @@ OUT=$(printf '' | bash "$S" --parent 10 --repo o/r)
|
|
|
102
115
|
check "empty: no create, deferred_items=0" "! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'deferred_items=0'"
|
|
103
116
|
bash "$S" --repo o/r >/dev/null 2>&1; check "usage: missing parent rc 2" "[ $? -eq 2 ]"
|
|
104
117
|
|
|
118
|
+
# 9. e1: search index lags (returns nothing) but the claim marker on the parent finds the follow-up
|
|
119
|
+
reset
|
|
120
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 open OWNER; claim_json OWNER 77
|
|
121
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
122
|
+
check "index-lag: claim found, no duplicate create" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'batched_into=#77 filed=0'"
|
|
123
|
+
check "index-lag: search never needed" "! grep -q search/issues '$GH_LOG'"
|
|
124
|
+
|
|
125
|
+
# 10. e2: closed follow-up (found only by a state-all search) is reused and reopened
|
|
126
|
+
reset
|
|
127
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER
|
|
128
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
129
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
130
|
+
check "closed: reused, reopened, no create" "[ $RC -eq 0 ] && grep -q '^issue reopen 77' '$GH_LOG' && ! grep -q '^issue create' '$GH_LOG'"
|
|
131
|
+
check "closed: search query covers all states" "grep -q 'is:issue' '$GH_LOG' && ! grep 'api -X' '$GH_LOG' | grep -q 'is:open'"
|
|
132
|
+
reset; export REOPEN_FAIL=1
|
|
133
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
|
|
134
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
135
|
+
check "closed: reopen failure is rc 4" "[ $RC -eq 4 ]"
|
|
136
|
+
|
|
137
|
+
# 11. create posts the claim marker on the parent
|
|
138
|
+
reset
|
|
139
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
140
|
+
check "claim: posted on parent after create" "[ $RC -eq 0 ] && grep -q '^issue comment 10 .*FORGE:DEFERRED_ISSUE: #901' '$GH_LOG'"
|
|
141
|
+
reset; export COMMENT_FAIL=1
|
|
142
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
|
|
143
|
+
check "claim: post failure only warns" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#901 filed=1'"
|
|
144
|
+
|
|
145
|
+
# 12. e3: a concurrent build filed #800 first; the first claim read (before our create) misses it, the post-create re-check sees it
|
|
146
|
+
race_setup() { # first comments read returns [] (the other build has not claimed yet), later reads return the real listing
|
|
147
|
+
CALLS="$T/claim.calls"; : > "$CALLS"; export CALLS
|
|
148
|
+
mv "$T/bin/gh" "$T/bin/gh.real"
|
|
149
|
+
cat > "$T/bin/gh" <<'WRAP'
|
|
150
|
+
#!/usr/bin/env bash
|
|
151
|
+
if [ "$1 $2" = "api --paginate" ]; then
|
|
152
|
+
echo x >> "$CALLS"
|
|
153
|
+
if [ "$(wc -l < "$CALLS" | tr -d ' ')" -le 1 ]; then echo "$*" >> "$GH_LOG"; echo '[]'; exit 0; fi
|
|
154
|
+
fi
|
|
155
|
+
exec "$(dirname "$0")/gh.real" "$@"
|
|
156
|
+
WRAP
|
|
157
|
+
chmod +x "$T/bin/gh"
|
|
158
|
+
}
|
|
159
|
+
race_teardown() { mv "$T/bin/gh.real" "$T/bin/gh"; }
|
|
160
|
+
reset
|
|
161
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 800 open OWNER; claim_json OWNER 800
|
|
162
|
+
race_setup
|
|
163
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
164
|
+
race_teardown
|
|
165
|
+
check "race: newer duplicate closed, items moved to lowest" "[ $RC -eq 0 ] && grep -q '^issue close 901' '$GH_LOG' && grep -q '^issue edit 800' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'batched_into=#800 filed=1'"
|
|
166
|
+
check "race: rows point at canonical issue" "[ \$(printf '%s\n' \"\$OUT\" | grep -c '800\$') -ge 3 ]"
|
|
167
|
+
# e5: duplicate-close failure only warns
|
|
168
|
+
reset; export CLOSE_FAIL=1
|
|
169
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 800 open OWNER; claim_json OWNER 800
|
|
170
|
+
race_setup
|
|
171
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
|
|
172
|
+
race_teardown
|
|
173
|
+
check "race: duplicate-close failure still rc 0" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#800'"
|
|
174
|
+
|
|
175
|
+
# 13. e4: claim listing failure is a failed lookup, never "none found"
|
|
176
|
+
reset; export CLAIM_FAIL=1
|
|
177
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
178
|
+
check "claim-failure: rc 3, no create" "[ $RC -eq 3 ] && printf '%s' \"\$OUT\" | grep -q DEFERRED_LOOKUP=FAILED && ! grep -q '^issue create' '$GH_LOG'"
|
|
179
|
+
|
|
180
|
+
# 15. e6: untrusted claim comment is ignored (falls through to create)
|
|
181
|
+
reset
|
|
182
|
+
issue_json 77 open OWNER; claim_json NONE 77
|
|
183
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
|
|
184
|
+
check "untrusted-claim: ignored, create filed" "[ $RC -eq 0 ] && grep -q '^issue create' '$GH_LOG'"
|
|
185
|
+
|
|
186
|
+
# 16. e7: several claims, lowest valid wins; a claim to a missing issue falls through
|
|
187
|
+
reset
|
|
188
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 80 open OWNER; issue_json 90 open OWNER; claim_json OWNER 70 90 80
|
|
189
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
190
|
+
check "multi-claim: lowest valid (80) wins, missing 70 skipped" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#80 filed=0'"
|
|
191
|
+
|
|
192
|
+
# 17. DRY_RUN: no claim, reopen or close
|
|
193
|
+
reset; export DRY_RUN=1
|
|
194
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
|
|
195
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
196
|
+
check "dry-run: no comment/reopen/close/edit" "! grep -qE '^issue (comment|reopen|close|edit|create)' '$GH_LOG'"
|
|
197
|
+
|
|
105
198
|
echo "passed=$PASS failed=$FAIL"
|
|
106
199
|
[ "$FAIL" -eq 0 ]
|
package/scripts/lib/telemetry.sh
CHANGED
|
@@ -51,6 +51,8 @@ _forge_tel_record() { # event k=v... -> one JSON line on stdout
|
|
|
51
51
|
run: ($run | str), phase: ($phase | str), event: $ev, eid: $eid} + $p' --args "${args[@]}"
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
# build_selfreview (forge#3971: tier, agents, findings, fixed, declined, skipped=<reason>; ids and counts only) has no review-side
|
|
55
|
+
# routing: it defaults to the issue target below, like diff_size.
|
|
54
56
|
# finding_disposition payload is generic key=value (incl. scope=introduced|pre-existing|unknown); no per-field allowlist applies.
|
|
55
57
|
# Prints "<number> <repo>" of the comment target (issue or PR) for this event, or nothing when none is usable.
|
|
56
58
|
_forge_tel_target() {
|