forgedock 1.40.0 → 1.42.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 +6 -3
- package/bin/engine-cli.mjs +40 -4
- package/bin/forgedock.mjs +22 -1
- package/bin/lean/batch.test.mjs +33 -1
- package/bin/lean/capwait.mjs +36 -0
- package/bin/lean/capwait.test.mjs +48 -0
- package/bin/lean/conformance.mjs +150 -2
- package/bin/lean/conformance.test.mjs +126 -0
- package/bin/lean/depcache.mjs +10 -2
- package/bin/lean/depcache.test.mjs +22 -17
- package/bin/lean/execution.mjs +21 -5
- package/bin/lean/execution.test.mjs +7 -1
- package/bin/lean/heavy.mjs +22 -1
- package/bin/lean/lanecheck.mjs +129 -0
- package/bin/lean/lanecheck.test.mjs +118 -0
- package/bin/lean/lanelock.mjs +31 -0
- package/bin/lean/lean.mjs +102 -30
- package/bin/lean/lean.test.mjs +173 -3
- package/bin/lean/orchestrate.mjs +42 -7
- package/bin/lean/phaseledger.mjs +116 -0
- package/bin/lean/phaseledger.test.mjs +72 -0
- package/bin/lean/prodwatch.mjs +12 -1
- package/bin/lean/prodwatch.test.mjs +32 -3
- package/bin/lean/prompts.mjs +19 -0
- package/bin/lean/render.mjs +10 -2
- package/bin/lean/sandbox-provider.contract.test.mjs +48 -0
- package/bin/lean/sandbox-remote.mjs +8 -1
- package/bin/lean/schedd.mjs +5 -1
- package/bin/lean/selftest-gate.mjs +46 -0
- package/bin/lean/selftest-gate.test.mjs +40 -0
- package/bin/lean/selftest-live.mjs +248 -0
- package/bin/lean/selftest-live.test.mjs +154 -0
- package/bin/lean/selftest-nightly.mjs +81 -0
- package/bin/lean/selftest-nightly.test.mjs +84 -0
- package/bin/lean/selftest-static.mjs +81 -0
- package/bin/lean/selftest-static.test.mjs +32 -0
- package/bin/pool/agent.mjs +16 -5
- package/bin/pool/router.mjs +17 -1
- package/bin/pool/warm.mjs +80 -0
- package/bin/review-staging.mjs +28 -6
- package/bin/stats/aggregate.mjs +2 -1
- package/bin/stats/efficiency.mjs +3 -1
- package/bin/stats/efficiency.test.mjs +4 -1
- package/bin/stats/github-source.mjs +2 -1
- package/bin/stats/local-source.mjs +1 -0
- package/bin/stats/render.mjs +2 -0
- package/commands/deploy-pr.md +3 -3
- package/commands/review-pr-staging.md +2 -2
- package/commands/work-on/build.md +2 -2
- package/docs/CONFIG.md +33 -0
- package/docs/OUTCOME-CODES.md +1 -1
- package/docs/PROD-WATCH.md +1 -1
- package/docs/RELEASE-NOTES.md +10 -0
- package/docs/SELFTEST.md +34 -0
- package/docs/eval/conformance-labels-v1.json +13 -0
- package/package.json +1 -1
- package/pool/image/Dockerfile +2 -1
- package/pool/image/entrypoint.sh +44 -11
- package/pool/image/warm-refresh.sh +14 -0
- package/scripts/conformance-calibrate.mjs +132 -0
- package/scripts/file-deferred-followups.sh +21 -8
- package/scripts/file-deferred-followups.test.sh +27 -6
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
# Refreshes a warm sandbox's checkout to the lane's base ref (#4912): an incremental fetch into the existing clone, then
|
|
5
|
+
# a hard reset, so installed dependencies and ignored files stay. Usage: warm-refresh.sh [<ref>] (run in the checkout).
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
ref="${1:-}"
|
|
8
|
+
if [ -n "$ref" ]; then
|
|
9
|
+
git fetch --quiet origin "$ref"
|
|
10
|
+
git checkout --quiet --force -B "$ref" FETCH_HEAD
|
|
11
|
+
else
|
|
12
|
+
git fetch --quiet origin HEAD
|
|
13
|
+
fi
|
|
14
|
+
git reset --quiet --hard FETCH_HEAD
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
/**
|
|
5
|
+
* Conformance calibration: scores labelled gate verdicts (and an optional replay corpus) per check kind and prints
|
|
6
|
+
* precision, recall and false-block rate, with the mode each kind has earned: enforce only at ~0 false blocks.
|
|
7
|
+
*
|
|
8
|
+
* node scripts/conformance-calibrate.mjs --labels <labels.json> [--replay <corpus.json>] [--events <run-log>] [--min-blocks N] [--max-false-rate R] [--json]
|
|
9
|
+
*
|
|
10
|
+
* A case is {id, kind, label} with label true-block | false-block | missed | true-pass, or {id, kind, expected_block, blocked}
|
|
11
|
+
* (the replay-bench shape). A file is `{cases: [...]}` or a bare array. `kind` is any failure code; none is built in.
|
|
12
|
+
* `--events` reads run-log events (JSONL, a JSON array or `{events}`) and adds the mutation-check metrics (#4929).
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
export const LABELS = Object.freeze(["true-block", "false-block", "missed", "true-pass"]);
|
|
18
|
+
export const DEFAULT_MIN_BLOCKS = 3;
|
|
19
|
+
export const DEFAULT_MAX_FALSE_RATE = 0;
|
|
20
|
+
|
|
21
|
+
/** The label of a case, from `label` or derived from `expected_block` and `blocked`; null when neither is usable. */
|
|
22
|
+
export function labelOf(c) {
|
|
23
|
+
if (LABELS.includes(c?.label)) return c.label;
|
|
24
|
+
if (typeof c?.expected_block !== "boolean" || typeof c?.blocked !== "boolean") return null;
|
|
25
|
+
return c.blocked ? (c.expected_block ? "true-block" : "false-block") : c.expected_block ? "missed" : "true-pass";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function casesOf(doc) {
|
|
29
|
+
return (Array.isArray(doc) ? doc : doc?.cases ?? []).filter((c) => c && typeof c.kind === "string" && c.kind && labelOf(c));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Per-kind counts and rates. precision = TP/(TP+FP), recall = TP/(TP+FN), falseBlockRate = FP/(TP+FP) (blocks that were wrong);
|
|
34
|
+
* a rate is null when its denominator is 0. `mode` is enforce only with at least minBlocks blocks and a false-block rate <= maxFalseRate.
|
|
35
|
+
*/
|
|
36
|
+
export function scoreKinds(cases, { minBlocks = DEFAULT_MIN_BLOCKS, maxFalseRate = DEFAULT_MAX_FALSE_RATE } = {}) {
|
|
37
|
+
const by = {};
|
|
38
|
+
for (const c of cases ?? []) {
|
|
39
|
+
const t = (by[c.kind] ??= { kind: c.kind, tp: 0, fp: 0, fn: 0, tn: 0 });
|
|
40
|
+
const l = labelOf(c);
|
|
41
|
+
if (l === "true-block") t.tp++; else if (l === "false-block") t.fp++; else if (l === "missed") t.fn++; else if (l === "true-pass") t.tn++;
|
|
42
|
+
}
|
|
43
|
+
const ratio = (n, d) => (d ? n / d : null);
|
|
44
|
+
return Object.values(by).sort((a, b) => a.kind.localeCompare(b.kind)).map((t) => {
|
|
45
|
+
const blocks = t.tp + t.fp, falseBlockRate = ratio(t.fp, blocks);
|
|
46
|
+
const mode = blocks >= minBlocks && falseBlockRate !== null && falseBlockRate <= maxFalseRate ? "enforce" : "shadow";
|
|
47
|
+
return { ...t, blocks, precision: ratio(t.tp, blocks), recall: ratio(t.tp, t.tp + t.fn), falseBlockRate, mode };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const MUTATION_EVENT = "REQUIREMENT_MUTATION_SURVIVED";
|
|
52
|
+
export const MUTATION_KIND = "conformance.mutation_survived";
|
|
53
|
+
|
|
54
|
+
/** Events from a run-log file: JSONL text, a JSON array or `{events}`; unparseable lines are dropped. */
|
|
55
|
+
export function eventsOf(text) {
|
|
56
|
+
const t = String(text ?? "").trim();
|
|
57
|
+
if (!t) return [];
|
|
58
|
+
try {
|
|
59
|
+
const doc = JSON.parse(t);
|
|
60
|
+
return (Array.isArray(doc) ? doc : doc?.events ?? (doc?.event ? [doc] : [])).filter((e) => e && typeof e === "object");
|
|
61
|
+
} catch { /* JSONL */ }
|
|
62
|
+
return t.split("\n").flatMap((l) => { try { const e = JSON.parse(l); return e && typeof e === "object" ? [e] : []; } catch { return []; } });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Mutation-check metrics for engine.conformance_mutation promotion. survivedShare = survived ids / checked requirements over the
|
|
67
|
+
* REQUIREMENT_MUTATION_SURVIVED events (an event with no ids is a clean check); falseSurvivedRate = false-block / (true-block + false-block)
|
|
68
|
+
* over labelled cases of kind conformance.mutation_survived (a survived verdict on a test that was real). A rate is null with no denominator.
|
|
69
|
+
*/
|
|
70
|
+
export function mutationStats(events, cases) {
|
|
71
|
+
const evs = (events ?? []).filter((e) => e?.event === MUTATION_EVENT);
|
|
72
|
+
let checked = 0, survived = 0, skippedRuns = 0;
|
|
73
|
+
for (const e of evs) {
|
|
74
|
+
checked += Number(e.checked) || 0;
|
|
75
|
+
survived += Array.isArray(e.ids) ? e.ids.length : 0;
|
|
76
|
+
if (e.skipped) skippedRuns++;
|
|
77
|
+
}
|
|
78
|
+
let falseSurvived = 0, labelledSurvived = 0;
|
|
79
|
+
for (const c of cases ?? []) {
|
|
80
|
+
if (c.kind !== MUTATION_KIND) continue;
|
|
81
|
+
const l = labelOf(c);
|
|
82
|
+
if (l === "false-block") { falseSurvived++; labelledSurvived++; } else if (l === "true-block") labelledSurvived++;
|
|
83
|
+
}
|
|
84
|
+
const ratio = (n, d) => (d ? n / d : null);
|
|
85
|
+
return { events: evs.length, checked, survived, survivedShare: ratio(survived, checked), skippedRuns, falseSurvived, labelledSurvived, falseSurvivedRate: ratio(falseSurvived, labelledSurvived) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pct = (x) => (x === null ? "n/a" : `${Math.round(x * 1000) / 10}%`);
|
|
89
|
+
|
|
90
|
+
export function formatKindReport(rows) {
|
|
91
|
+
const head = ["kind", "blocks", "precision", "recall", "false-block", "mode"];
|
|
92
|
+
const body = rows.map((r) => [r.kind, String(r.blocks), pct(r.precision), pct(r.recall), `${r.fp}/${r.blocks} (${pct(r.falseBlockRate)})`, r.mode]);
|
|
93
|
+
const w = head.map((h, i) => Math.max(h.length, ...body.map((b) => b[i].length)));
|
|
94
|
+
const line = (cells) => cells.map((c, i) => c.padEnd(w[i])).join(" ").trimEnd();
|
|
95
|
+
const enforce = rows.filter((r) => r.mode === "enforce").map((r) => `${r.kind}=enforce`);
|
|
96
|
+
return [line(head), ...body.map(line), "", enforce.length ? `engine.conformance_kinds: "${enforce.join(", ")}"` : "engine.conformance_kinds: (none earned enforcement)"].join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatMutationReport(m) {
|
|
100
|
+
return ["mutation check (engine.conformance_mutation)",
|
|
101
|
+
`survived-mutation share: ${m.survived}/${m.checked} (${pct(m.survivedShare)}) over ${m.events} event(s), ${m.skippedRuns} skipped`,
|
|
102
|
+
`false survived: ${m.falseSurvived}/${m.labelledSurvived} (${pct(m.falseSurvivedRate)}) labelled ${MUTATION_KIND} case(s)`].join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseCalibrateArgs(argv) {
|
|
106
|
+
const o = { labels: null, replay: null, events: null, minBlocks: DEFAULT_MIN_BLOCKS, maxFalseRate: DEFAULT_MAX_FALSE_RATE, json: false };
|
|
107
|
+
for (let i = 0; i < argv.length; i++) {
|
|
108
|
+
const a = argv[i];
|
|
109
|
+
if (a === "--labels") o.labels = argv[++i];
|
|
110
|
+
else if (a === "--replay") o.replay = argv[++i];
|
|
111
|
+
else if (a === "--events") o.events = argv[++i];
|
|
112
|
+
else if (a === "--min-blocks") o.minBlocks = Number(argv[++i]);
|
|
113
|
+
else if (a === "--max-false-rate") o.maxFalseRate = Number(argv[++i]);
|
|
114
|
+
else if (a === "--json") o.json = true;
|
|
115
|
+
else throw new Error(`unknown argument ${a}`);
|
|
116
|
+
}
|
|
117
|
+
if (!o.labels && !o.replay) throw new Error("usage: conformance-calibrate.mjs --labels <labels.json> [--replay <corpus.json>] [--events <run-log>] [--min-blocks N] [--max-false-rate R] [--json]");
|
|
118
|
+
return o;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function run(argv, read = (p) => JSON.parse(readFileSync(p, "utf8")), readText = (p) => readFileSync(p, "utf8")) {
|
|
122
|
+
const o = parseCalibrateArgs(argv);
|
|
123
|
+
const cases = [o.labels, o.replay].filter(Boolean).flatMap((p) => casesOf(read(p)));
|
|
124
|
+
const rows = scoreKinds(cases, o);
|
|
125
|
+
const mutation = o.events ? mutationStats(eventsOf(readText(o.events)), cases) : null;
|
|
126
|
+
if (o.json) return JSON.stringify({ cases: cases.length, kinds: rows, ...(mutation ? { mutation } : {}) }, null, 2);
|
|
127
|
+
return `${cases.length} labelled case(s)\n${formatKindReport(rows)}${mutation ? `\n\n${formatMutationReport(mutation)}` : ""}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
131
|
+
try { console.log(run(process.argv.slice(2))); } catch (e) { console.error(e.message); process.exit(2); }
|
|
132
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
# The builder decides fold-vs-defer (judgment, build.md B2.0); this script does the mechanics for the items
|
|
7
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
|
|
8
|
+
# <!-- FORGE:DEFERRED_FROM: #N -->, append the missing items (exact path match; a closed one is reopened only if completed, not resolved by a person, and given new items), or create ONE issue
|
|
9
9
|
# with a checklist, claim it on the parent and re-check once for a concurrent lower-numbered duplicate.
|
|
10
10
|
# Items are grouped by class (engineering | post-deploy | external | ops, #4664): one follow-up per class per parent, each
|
|
11
11
|
# labelled `followup:<class>`, carrying `<!-- FORGE:DEFERRED_CLASS: <class> -->` (absent = engineering, for older issues) and the
|
|
@@ -142,14 +142,22 @@ search_nums() {
|
|
|
142
142
|
| jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
|
|
143
143
|
[.items[] | select(trusted) | select((.body // "") | contains($m)) | .number] | sort | .[]' 2>/dev/null
|
|
144
144
|
}
|
|
145
|
-
verify_num() { # number -> `number<TAB>state` when a trusted, non-PR issue of class $CLS carries the marker; empty when not (a 404 is "not it")
|
|
145
|
+
verify_num() { # number -> `number<TAB>state` when a trusted, non-PR issue of class $CLS carries the marker (a closed one only if completed and not resolved by a person); empty when not (a 404 is "not it")
|
|
146
146
|
local out rc
|
|
147
147
|
out=$(gh api "repos/${REPO}/issues/$1" 2>"$TMP/verr"); rc=$?
|
|
148
148
|
if [ "$rc" -ne 0 ]; then grep -qE '404|Not Found|410' "$TMP/verr" 2>/dev/null && return 0; return 1; fi
|
|
149
|
-
printf '%s' "$out" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" --arg cls "$CLS" "$JQ_PRE"'
|
|
149
|
+
out=$(printf '%s' "$out" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" --arg cls "$CLS" "$JQ_PRE"'
|
|
150
150
|
select(.pull_request == null) | select(trusted) | select((.body // "") | contains($m))
|
|
151
151
|
| select((((.body // "") | capture("FORGE:DEFERRED_CLASS: (?<c>[a-z-]+)") | .c) // "engineering") == $cls)
|
|
152
|
-
|
|
|
152
|
+
| select(.state != "closed" or .state_reason == "completed")
|
|
153
|
+
| "\(.number)\t\(.state)"' 2>/dev/null) || return 1
|
|
154
|
+
# #4924: a closed tracker was resolved on purpose when a person had the last word (fold, supersede, invalid): not reusable.
|
|
155
|
+
case "$out" in *$'\t'closed)
|
|
156
|
+
local last
|
|
157
|
+
last=$(gh api "repos/${REPO}/issues/$1/comments?per_page=100" 2>/dev/null | jq -r 'if type == "array" and length > 0 then (last | .user.type // "") else "" end' 2>/dev/null) || return 1
|
|
158
|
+
[ -z "$last" ] || [ "$last" = "Bot" ] || out="" ;;
|
|
159
|
+
esac
|
|
160
|
+
printf '%s' "$out"
|
|
153
161
|
}
|
|
154
162
|
CANON=""; CSTATE=""
|
|
155
163
|
pick_verified() { # numbers... ; sets CANON/CSTATE from the first verified one; non-zero only when a fetch failed
|
|
@@ -323,10 +331,15 @@ append_items() { # number state ; non-zero = failure
|
|
|
323
331
|
else
|
|
324
332
|
gh issue edit "$num" -R "$REPO" --body-file "$TMP/newbody" >/dev/null 2>&1 || return 1
|
|
325
333
|
fi
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
334
|
+
# #4924: reopen only for new items, and say why.
|
|
335
|
+
if [ "$state" = "closed" ]; then
|
|
336
|
+
if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: gh issue reopen $num" >&2
|
|
337
|
+
else
|
|
338
|
+
gh issue reopen "$num" -R "$REPO" >/dev/null 2>&1 || return 1
|
|
339
|
+
gh issue comment "$num" -R "$REPO" --body "$(printf '<!-- FORGE:FOLLOWUP_REOPENED -->\nReopened by the run of #%s%s: it deferred new items not yet tracked here:\n\n%s' "$PARENT" "${FORGE_RUN_ID:+ (run \`$FORGE_RUN_ID\`)}" "$(cat "$TMP/add")")" >/dev/null 2>&1 \
|
|
340
|
+
|| echo "file-deferred-followups: warning: could not post reopen comment on #${num}" >&2
|
|
341
|
+
fi
|
|
342
|
+
fi
|
|
330
343
|
fi
|
|
331
344
|
return 0
|
|
332
345
|
}
|
|
@@ -29,6 +29,7 @@ if [ "$1" = api ]; then
|
|
|
29
29
|
case "$*" in "api repos/o/r/milestones"*) cat "$T_DIR/milestones.json"; exit 0 ;; esac
|
|
30
30
|
case "$*" in "api repos/o/r/issues/10 --jq"*) cat "$T_DIR/parent.milestone"; exit 0 ;; esac
|
|
31
31
|
case "$*" in *--jq*) cat "$ISSUE_BODY"; exit 0 ;; esac
|
|
32
|
+
case "$2" in repos/o/r/issues/*/comments*) c="${2#repos/o/r/issues/}"; c="$T_DIR/comments.${c%%/*}.json"; if [ -f "$c" ]; then cat "$c"; else echo '[]'; fi; exit 0 ;; esac
|
|
32
33
|
n="${2##*/}"; [ -f "$T_DIR/issue.$n.json" ] && { cat "$T_DIR/issue.$n.json"; exit 0; }
|
|
33
34
|
echo "HTTP 404: Not Found" >&2; exit 1
|
|
34
35
|
fi
|
|
@@ -40,11 +41,12 @@ PASS=0; FAIL=0
|
|
|
40
41
|
ok() { PASS=$((PASS+1)); echo "PASS: $1"; }
|
|
41
42
|
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1"; }
|
|
42
43
|
check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; }
|
|
43
|
-
reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited" "$T"/issue.*.json "$T/create.n"; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL CLAIM_FAIL COMMENT_FAIL REOPEN_FAIL CLOSE_FAIL DRY_RUN FORGE_FOLLOWUP_MILESTONE FORGE_FOLLOWUPS_DEFAULT_MILESTONE FORGE_EXTERNAL_PREREQ
|
|
44
|
+
reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited" "$T"/issue.*.json "$T"/comments.*.json "$T/create.n"; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL CLAIM_FAIL COMMENT_FAIL REOPEN_FAIL CLOSE_FAIL DRY_RUN FORGE_FOLLOWUP_MILESTONE FORGE_FOLLOWUPS_DEFAULT_MILESTONE FORGE_EXTERNAL_PREREQ
|
|
44
45
|
echo '[]' > "$T/milestones.json"; : > "$T/parent.milestone"
|
|
45
46
|
echo '{"items":[]}' > "$SEARCH_JSON"; echo '[]' > "$COMMENTS_JSON"; }
|
|
46
|
-
# issue_json NUMBER STATE ASSOCIATION [PARENT]: REST view of a follow-up carrying the DEFERRED_FROM marker
|
|
47
|
-
|
|
47
|
+
# issue_json NUMBER STATE ASSOCIATION [PARENT] [STATE_REASON]: REST view of a follow-up carrying the DEFERRED_FROM marker
|
|
48
|
+
# (no sed -i: BSD sed on macOS reads the script as the backup suffix, so state_reason was never written there)
|
|
49
|
+
issue_json() { printf '{"number":%s,"state":"%s",%s"author_association":"%s","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #%s -->"}\n' "$1" "$2" "${5:+\"state_reason\":\"$5\",}" "$3" "${4:-10}" > "$T/issue.$1.json"; }
|
|
48
50
|
# claim_json ASSOCIATION NUMBER...: parent comments carrying claim markers
|
|
49
51
|
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"; }
|
|
50
52
|
printf 'a/x.sh\twhy a\t\nb/y.sh\twhy b\tknown-flaw\nc/z.sh\twhy c\t\n' > "$T/items.tsv"
|
|
@@ -128,16 +130,35 @@ check "index-lag: search never needed" "! grep -q search/issues '$GH_LOG'"
|
|
|
128
130
|
|
|
129
131
|
# 10. e2: closed follow-up (found only by a state-all search) is reused and reopened
|
|
130
132
|
reset
|
|
131
|
-
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER
|
|
133
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER 10 completed
|
|
132
134
|
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
133
135
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
134
136
|
check "closed: reused, reopened, no create" "[ $RC -eq 0 ] && grep -q '^issue reopen 77' '$GH_LOG' && ! grep -q '^issue create' '$GH_LOG'"
|
|
135
137
|
check "closed: search query covers all states" "grep -q 'is:issue' '$GH_LOG' && ! grep 'api -X' '$GH_LOG' | grep -q 'is:open'"
|
|
136
138
|
reset; export REOPEN_FAIL=1
|
|
137
|
-
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
|
|
139
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER 10 completed; claim_json OWNER 77
|
|
138
140
|
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
139
141
|
check "closed: reopen failure is rc 4" "[ $RC -eq 4 ]"
|
|
140
142
|
|
|
143
|
+
# 10b. #4924: closed trackers are reopened only when completed, unresolved by a person, and given new items
|
|
144
|
+
reset
|
|
145
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER 10 not_planned; claim_json OWNER 77
|
|
146
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
147
|
+
check "not planned: stays closed, fresh tracker created" "[ $RC -eq 0 ] && ! grep -q '^issue reopen' '$GH_LOG' && ! grep -q '^issue edit 77' '$GH_LOG' && grep -q '^issue create' '$GH_LOG'"
|
|
148
|
+
reset
|
|
149
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER 10 completed; claim_json OWNER 77
|
|
150
|
+
echo '[{"user":{"type":"User"},"body":"folded into #5"}]' > "$T/comments.77.json"
|
|
151
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
152
|
+
check "folded by a person: stays closed, fresh tracker created" "[ $RC -eq 0 ] && ! grep -q '^issue reopen' '$GH_LOG' && grep -q '^issue create' '$GH_LOG'"
|
|
153
|
+
reset
|
|
154
|
+
printf -- '- [ ] `a/x.sh`\n- [ ] `b/y.sh`\n- [ ] `c/z.sh`\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER 10 completed; claim_json OWNER 77
|
|
155
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
156
|
+
check "completed, nothing new: stays closed, no create" "[ $RC -eq 0 ] && ! grep -q '^issue reopen' '$GH_LOG' && ! grep -q '^issue create' '$GH_LOG'"
|
|
157
|
+
reset
|
|
158
|
+
printf -- '- [ ] `a/x.sh`\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER 10 completed; claim_json OWNER 77
|
|
159
|
+
FORGE_RUN_ID=core_10 bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
160
|
+
check "completed, new items: reopened with a comment naming parent, run and items" "[ $RC -eq 0 ] && grep -q '^issue reopen 77' '$GH_LOG' && grep '^issue comment 77' '$GH_LOG' | grep -q 'FORGE:FOLLOWUP_REOPENED' && grep -q 'Reopened by the run of #10 (run .core_10.)' '$GH_LOG' && grep -q 'b/y.sh' '$GH_LOG'"
|
|
161
|
+
|
|
141
162
|
# 11. create posts the claim marker on the parent
|
|
142
163
|
reset
|
|
143
164
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
@@ -195,7 +216,7 @@ check "multi-claim: lowest valid (80) wins, missing 70 skipped" "[ $RC -eq 0 ] &
|
|
|
195
216
|
|
|
196
217
|
# 17. DRY_RUN: no claim, reopen or close
|
|
197
218
|
reset; export DRY_RUN=1
|
|
198
|
-
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
|
|
219
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER 10 completed; claim_json OWNER 77
|
|
199
220
|
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
200
221
|
check "dry-run: no comment/reopen/close/edit" "! grep -qE '^issue (comment|reopen|close|edit|create)' '$GH_LOG'"
|
|
201
222
|
|