forgedock 1.35.0 → 1.37.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 +78 -324
- package/bin/bot-token-status.mjs +26 -0
- package/bin/engine/events.mjs +161 -0
- package/bin/engine/events.test.mjs +142 -0
- package/bin/engine/outcome-codes.mjs +39 -1
- package/bin/engine/phases.mjs +65 -5
- package/bin/engine/telemetry-seam.mjs +5 -4
- package/bin/engine/telemetry-seam.test.mjs +4 -0
- package/bin/engine-cli.mjs +372 -46
- package/bin/engine.mjs +3 -1
- package/bin/experiment.mjs +55 -0
- package/bin/forgedock.mjs +108 -5
- package/bin/hooks/reread-nudge.mjs +15 -4
- package/bin/labels.json +50 -0
- package/bin/lean/batch.mjs +52 -6
- package/bin/lean/batch.test.mjs +176 -2
- package/bin/lean/conformance.e2e.test.mjs +1 -1
- package/bin/lean/conformance.mjs +136 -11
- package/bin/lean/conformance.test.mjs +148 -1
- package/bin/lean/decisions.mjs +181 -0
- package/bin/lean/decisions.test.mjs +71 -0
- package/bin/lean/effects.mjs +32 -14
- package/bin/lean/execution.mjs +62 -16
- package/bin/lean/execution.test.mjs +59 -2
- package/bin/lean/gatewatch.mjs +31 -0
- package/bin/lean/gatewatch.test.mjs +59 -0
- package/bin/lean/guardgate.mjs +170 -0
- package/bin/lean/guardgate.test.mjs +153 -0
- package/bin/lean/guards.mjs +243 -0
- package/bin/lean/guards.test.mjs +177 -0
- package/bin/lean/heavy.mjs +54 -8
- package/bin/lean/heavy.test.mjs +74 -1
- package/bin/lean/hostkill.mjs +1 -1
- package/bin/lean/i18n.mjs +65 -0
- package/bin/lean/i18n.test.mjs +47 -0
- package/bin/lean/integration.mjs +276 -0
- package/bin/lean/integration.test.mjs +189 -0
- package/bin/lean/lane.mjs +5 -2
- package/bin/lean/lean.mjs +169 -30
- package/bin/lean/lean.test.mjs +360 -24
- package/bin/lean/lifecycle.mjs +129 -0
- package/bin/lean/lifecycle.test.mjs +118 -0
- package/bin/lean/light.mjs +1 -1
- package/bin/lean/milestonesync.mjs +130 -0
- package/bin/lean/milestonesync.test.mjs +133 -0
- package/bin/lean/orchestrate.mjs +132 -20
- package/bin/lean/parked-sweep.mjs +45 -0
- package/bin/lean/parked-sweep.test.mjs +71 -0
- package/bin/lean/postdeploy-check.mjs +181 -0
- package/bin/lean/postdeploy-check.test.mjs +206 -0
- package/bin/lean/postdeploy-monitor.mjs +97 -0
- package/bin/lean/postdeploy-monitor.test.mjs +147 -0
- package/bin/lean/precheck.mjs +6 -4
- package/bin/lean/precheck.test.mjs +17 -2
- package/bin/lean/prefilter.mjs +106 -21
- package/bin/lean/prefilter.test.mjs +86 -6
- package/bin/lean/probe.mjs +4 -3
- package/bin/lean/probe.test.mjs +1 -1
- package/bin/lean/prompts.mjs +23 -9
- package/bin/lean/resume.test.mjs +1 -1
- package/bin/lean/retry.mjs +28 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +36 -3
- package/bin/lean/sandbox-remote.mjs +11 -4
- package/bin/lean/schedd.mjs +77 -13
- package/bin/lean/schedd.test.mjs +131 -5
- package/bin/lean/sequence.mjs +24 -0
- package/bin/lean/sequence.test.mjs +31 -1
- package/bin/lean/signoff.mjs +66 -0
- package/bin/lean/signoff.test.mjs +102 -0
- package/bin/lean/train.mjs +177 -0
- package/bin/lean/train.test.mjs +113 -0
- package/bin/pool/cli.mjs +2 -0
- package/bin/pool/health.mjs +30 -0
- package/bin/review-context.mjs +298 -0
- package/bin/runner.mjs +26 -3
- package/bin/stats/aggregate.mjs +1 -1
- package/bin/stats/compounding.mjs +157 -0
- package/bin/stats/compounding.test.mjs +74 -0
- package/bin/stats/experiment.mjs +44 -0
- package/bin/stats/github-source.mjs +2 -1
- package/bin/stats/index.mjs +62 -3
- package/bin/stats/release.mjs +75 -0
- package/bin/stats/release.test.mjs +68 -0
- package/commands/bootstrap.md +10 -8
- package/commands/deploy-info.md +3 -0
- package/commands/issue.md +2 -0
- package/commands/milestone.md +43 -54
- package/commands/orchestrate/phase-4-execution.md +2 -0
- package/commands/orchestrate.md +15 -3
- package/commands/pipeline-status.md +20 -0
- package/commands/review-pr.md +1 -0
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build.md +4 -2
- package/commands/work-on/investigate.md +3 -1
- package/commands/work-on/remediate.md +1 -1
- package/docs/CONFIG.md +120 -2
- package/docs/EVENTS.md +65 -0
- package/docs/OUTCOME-CODES.md +9 -1
- package/docs/STATS.md +51 -0
- package/docs/TELEMETRY.md +3 -1
- package/docs/site/install.md +89 -0
- package/docs/site/start-a-new-project.md +1 -1
- package/docs/spec/label-state-machine.md +6 -0
- package/package.json +1 -1
- package/pool/install/install-agent.sh +4 -2
- package/pool/install/install-router.sh +4 -2
- package/pool/install/install-schedd.sh +28 -0
- package/pool/install/unit-hardening.sh +34 -0
- package/scripts/acceptance-red-green.sh +19 -2
- package/scripts/acceptance-red-green.test.sh +2 -0
- package/scripts/bench-lightlane.mjs +2 -1
- package/scripts/bootstrap-create.mjs +3 -2
- package/scripts/bootstrap-validate.mjs +31 -4
- package/scripts/eval-gate-scorecard.mjs +7 -0
- package/scripts/file-deferred-followups.sh +286 -77
- package/scripts/file-deferred-followups.test.sh +117 -2
- package/scripts/hotspots.sh +23 -2
- package/scripts/hotspots.test.sh +9 -0
- package/scripts/plan-validate-live.mjs +111 -0
- package/scripts/readiness.mjs +141 -0
- package/scripts/review-prepare.sh +17 -1
- package/scripts/review-prepare.test.sh +17 -0
- package/scripts/settle-scope.mjs +74 -0
- package/scripts/systemd/forgedock-bot-token-refresh.service +7 -0
- package/scripts/systemd/forgedock-bot-token-refresh.timer +11 -0
- package/scripts/wait-ci-green.sh +14 -3
- package/scripts/wait-ci-green.test.sh +3 -1
- package/templates/bootstrap/needs-human-issue.md +4 -1
- package/templates/github-actions/migration-journal-guard.test.mjs +26 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# file-deferred-followups.sh — file
|
|
2
|
+
# file-deferred-followups.sh — file the deferred out-of-scope items of one build as ONE follow-up issue per class.
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
#
|
|
@@ -7,13 +7,29 @@
|
|
|
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
8
|
# <!-- FORGE:DEFERRED_FROM: #N -->, append the missing items (exact path match; a closed one is reopened), 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
|
+
# Items are grouped by class (engineering | post-deploy | external | ops, #4664): one follow-up per class per parent, each
|
|
11
|
+
# labelled `followup:<class>`, carrying `<!-- FORGE:DEFERRED_CLASS: <class> -->` (absent = engineering, for older issues) and the
|
|
12
|
+
# parent's milestone (a shipped/closed one: the next open milestone by due date, else FORGE_FOLLOWUPS_DEFAULT_MILESTONE).
|
|
10
13
|
#
|
|
11
14
|
# Usage: file-deferred-followups.sh --parent N --repo OWNER/REPO [--items FILE|-] [--folded N]
|
|
12
|
-
# items TSV, one per line: <path
|
|
15
|
+
# items TSV, one per line: <path|-> TAB <why> TAB <known-flaw flag: known-flaw|1|yes|true, else empty> [TAB <class> [TAB <label>]]
|
|
16
|
+
# Column 1 must look like a path (any legal path characters; spaced values need a `/` or extension) or be `-` (a requirement-level item): requirement text or an `R<n>:` prefix there is a usage
|
|
17
|
+
# error (exit 2, DEFERRED_TSV=INVALID, nothing filed). The label (default: the path) is the item's identity in the checklist
|
|
18
|
+
# and in the stdout rows; repeated `R<n>: R<m>:` prefixes in it collapse to the first. Class: column 4, else the why's
|
|
19
|
+
# `post-deploy:|external:|ops:` prefix, else engineering.
|
|
20
|
+
# #4724 buckets: a why carrying `[bucket:<kind>]` is routed by it: `owner` is always class external (never the rollup),
|
|
21
|
+
# `new-issue` engineering, `verify-now` (the parent is already on the default branch) its own per-parent follow-up class
|
|
22
|
+
# `verify-now` that the gate releases at once, and `post-deploy`/`date` go to the ONE open rollup of the release lane.
|
|
23
|
+
# The rollup (class post-deploy) is found by `<!-- FORGE:POSTDEPLOY_ROLLUP: <lane> -->`, not by parent: every lane appends
|
|
24
|
+
# its items under a `### #<root parent>` heading (a follow-up of a follow-up is filed under the root parent), deduplicated
|
|
25
|
+
# on normalized text across parents. Env: FORGE_RELEASE_LANE (the deploy lane, default `default`), FORGE_RELEASE_SHA (informational).
|
|
26
|
+
# A post-deploy why starts `post-deploy: [check:<id>] ...` (#4626); the token is kept verbatim in the checklist line.
|
|
13
27
|
# Env: DRY_RUN (non-empty: no create/edit, issue number 0), FORGE_TRAIL_TRUSTED_ASSOCIATIONS (default
|
|
14
|
-
# OWNER,MEMBER,COLLABORATOR), FORGE_TRAIL_TRUSTED_LOGINS (default empty), FORGE_DEFERRED_RETRY_SLEEP (secs)
|
|
15
|
-
#
|
|
16
|
-
#
|
|
28
|
+
# OWNER,MEMBER,COLLABORATOR), FORGE_TRAIL_TRUSTED_LOGINS (default empty), FORGE_DEFERRED_RETRY_SLEEP (secs),
|
|
29
|
+
# FORGE_FOLLOWUP_MILESTONE (the parent's milestone title; default: read from the parent), FORGE_FOLLOWUPS_DEFAULT_MILESTONE
|
|
30
|
+
# (forge.yaml followups.default_milestone), FORGE_EXTERNAL_PREREQ (issue number an external follow-up links to).
|
|
31
|
+
# Stdout: `identity<TAB>issue` per item, then DEFERRED_TELEMETRY deferred_items=N folded=N batched_into=#M[,#M2] filed=N classes=a,b buckets=kind:N,...
|
|
32
|
+
# Exit: 0 ok | 2 usage or malformed TSV row | 3 lookup failed (DEFERRED_LOOKUP=FAILED) | 4 create/append failed (DEFERRED_FILE=FAILED).
|
|
17
33
|
# A failed lookup is never "none found". bash 3.2 portable.
|
|
18
34
|
|
|
19
35
|
set -uo pipefail
|
|
@@ -37,37 +53,72 @@ TMP=$(mktemp -d "${TMPDIR:-/tmp}/deferred-followups.XXXXXX") || exit 2
|
|
|
37
53
|
trap 'rm -rf "$TMP"' EXIT
|
|
38
54
|
if [ "$ITEMS" = "-" ]; then cat > "$TMP/items.raw"; else cat "$ITEMS" > "$TMP/items.raw"; fi
|
|
39
55
|
|
|
40
|
-
# Normalise: drop blank lines and duplicate
|
|
56
|
+
# Normalise: validate each row, drop blank lines and duplicate identities (first wins), strip backticks.
|
|
41
57
|
# Internal file uses a non-whitespace separator (US, \x1f): a tab IFS collapses an empty `why` and drops the known-flaw flag.
|
|
58
|
+
# Fields: identity, why, flag, path (`-` when none), class.
|
|
42
59
|
SEP=$(printf '\037')
|
|
60
|
+
CLASSES="engineering post-deploy external ops verify-now"
|
|
43
61
|
: > "$TMP/items"
|
|
62
|
+
BAD=0; ROW=0
|
|
63
|
+
bad_row() { BAD=$((BAD+1)); echo "file-deferred-followups: row $ROW: $1" >&2; }
|
|
44
64
|
while IFS= read -r line || [ -n "$line" ]; do
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
ROW=$((ROW+1))
|
|
66
|
+
# Split on TAB by hand: `IFS=$'\t' read` would collapse an empty field and shift the flag into it.
|
|
67
|
+
f=(); rest="$line"; i=0
|
|
68
|
+
while [ $i -lt 5 ]; do
|
|
69
|
+
case "$rest" in *$'\t'*) f[$i]="${rest%%$'\t'*}"; rest="${rest#*$'\t'}" ;; *) f[$i]="$rest"; rest="" ;; esac
|
|
70
|
+
i=$((i+1))
|
|
71
|
+
done
|
|
72
|
+
trim() { printf '%s' "${1:-}" | tr -d '`' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'; }
|
|
73
|
+
p=$(trim "${f[0]}"); w="${f[1]}"; k="${f[2]}"; c=$(trim "${f[3]}"); lab=$(trim "${f[4]}")
|
|
74
|
+
[ -n "$p" ] || [ -n "$lab" ] || continue
|
|
75
|
+
# Column 1: a path or the literal `-`. Requirement text (spaces, an `R<n>:` prefix) belongs in the label column.
|
|
76
|
+
if [ "$p" != "-" ]; then
|
|
77
|
+
if printf '%s' "$p" | grep -qE '^R[0-9]+:'; then bad_row "column 1 starts with a requirement id (\`$p\`); put it in the label column and use \`-\` or a path"; continue; fi
|
|
78
|
+
# Legal path characters (parentheses, =, #, :, ...) pass. Reject control chars, and whitespace values that are prose:
|
|
79
|
+
# a spaced path needs a `/` or a file extension (`docs/My Notes.md`); `add a saved mapping` has neither.
|
|
80
|
+
if printf '%s' "$p" | LC_ALL=C grep -q '[[:cntrl:]]'; then bad_row "column 1 is not a path (\`$p\`)"; continue; fi
|
|
81
|
+
if [[ "$p" =~ [[:space:]] ]] && ! [[ "$p" == */* || "$p" =~ \.[A-Za-z0-9]+$ ]]; then bad_row "column 1 is not a path (\`$p\`)"; continue; fi
|
|
82
|
+
fi
|
|
83
|
+
# Identity: the label, else the path. Repeated `R1: R2:` prefixes collapse to the first.
|
|
84
|
+
id="${lab:-$p}"
|
|
85
|
+
while [[ "$id" =~ ^(R[0-9]+:)[[:space:]]*R[0-9]+:[[:space:]]*(.*)$ ]]; do id="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; done
|
|
86
|
+
if [ "$id" = "-" ]; then bad_row "no path and no label"; continue; fi
|
|
87
|
+
w=$(printf '%s' "$w" | tr '\r\n' ' ')
|
|
88
|
+
if [ -z "$c" ]; then
|
|
89
|
+
case "$w" in post-deploy:*) c="post-deploy" ;; external:*) c="external" ;; ops:*) c="ops" ;; *) c="engineering" ;; esac
|
|
90
|
+
fi
|
|
91
|
+
case " $CLASSES " in *" $c "*) ;; *) bad_row "unknown class \`$c\` (engineering|post-deploy|external|ops)"; continue ;; esac
|
|
92
|
+
# #4724: the bucket token decides the class: owner actions never join the post-deploy rollup, verify-now needs no release.
|
|
93
|
+
case "$w" in
|
|
94
|
+
*"[bucket:owner]"*) [ "$c" = "ops" ] || c="external" ;;
|
|
95
|
+
*"[bucket:new-issue]"*) [ "$c" = "post-deploy" ] && c="engineering" ;;
|
|
96
|
+
*"[bucket:verify-now]"*) [ "$c" = "post-deploy" ] && c="verify-now" ;;
|
|
97
|
+
esac
|
|
98
|
+
if cut -d "$SEP" -f1 "$TMP/items" | grep -qxF -- "$id"; then continue; fi
|
|
51
99
|
case "${k:-}" in known-flaw|1|yes|true) k="known-flaw" ;; *) k="" ;; esac
|
|
52
|
-
|
|
53
|
-
printf '%s%s%s%s%s\n' "$p" "$SEP" "$w" "$SEP" "$k" >> "$TMP/items"
|
|
100
|
+
printf '%s%s%s%s%s%s%s%s%s\n' "$id" "$SEP" "$w" "$SEP" "$k" "$SEP" "${p:--}" "$SEP" "$c" >> "$TMP/items"
|
|
54
101
|
done < "$TMP/items.raw"
|
|
102
|
+
if [ "$BAD" -gt 0 ]; then echo "DEFERRED_TSV=INVALID"; exit 2; fi
|
|
55
103
|
TOTAL=$(wc -l < "$TMP/items" | tr -d ' ')
|
|
56
104
|
|
|
57
105
|
MARKER="<!-- FORGE:DEFERRED_FROM: #${PARENT} -->"
|
|
58
106
|
TRUSTED_ASSOC="${FORGE_TRAIL_TRUSTED_ASSOCIATIONS-OWNER,MEMBER,COLLABORATOR}"
|
|
59
107
|
TRUSTED_LOGINS="${FORGE_TRAIL_TRUSTED_LOGINS-}"
|
|
60
108
|
|
|
61
|
-
finish() { # $1 = batched_into
|
|
62
|
-
local into="none"
|
|
63
|
-
|
|
109
|
+
finish() { # $1 = batched_into numbers (space separated) or empty, $2 = filed count
|
|
110
|
+
local into="none" bare="none"
|
|
111
|
+
if [ -n "$1" ]; then into=$(printf '%s\n' $1 | sed 's/^/#/' | paste -sd, -); bare=$(printf '%s\n' $1 | paste -sd, -); fi
|
|
112
|
+
local cls; cls=$(cut -d "$SEP" -f5 "$TMP/items" | sort -u | paste -sd, -)
|
|
113
|
+
local bks; bks=$(awk -F"$SEP" '{ b = "none"; if (match($2, /\[bucket:[a-z-]+\]/)) b = substr($2, RSTART + 8, RLENGTH - 9); n[b]++ } END { for (b in n) printf "%s:%d\n", b, n[b] }' "$TMP/items" | sort | paste -sd, -)
|
|
114
|
+
local line="DEFERRED_TELEMETRY deferred_items=${TOTAL} folded=${FOLDED} batched_into=${into} filed=$2 classes=${cls:-none} buckets=${bks:-none}"
|
|
64
115
|
echo "$line"
|
|
65
116
|
# #3834 emitter (scripts/lib/telemetry.sh): optional, never blocks, never changes stdout or the exit code.
|
|
66
117
|
local d; d=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
67
118
|
if [ -f "$d/lib/telemetry.sh" ]; then
|
|
68
119
|
FORGE_ISSUE="${FORGE_ISSUE:-$PARENT}" FORGE_REPO="${FORGE_REPO:-$REPO}" \
|
|
69
120
|
bash "$d/lib/telemetry.sh" deferred_followups deferred_items="$TOTAL" folded="$FOLDED" \
|
|
70
|
-
batched_into="${
|
|
121
|
+
batched_into="$bare" filed="$2" classes="${cls:-none}" buckets="${bks:-none}" >/dev/null 2>&1 </dev/null || true
|
|
71
122
|
fi
|
|
72
123
|
}
|
|
73
124
|
|
|
@@ -79,7 +130,7 @@ JQ_PRE='def lst: split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length >
|
|
|
79
130
|
def trusted: ((.author_association // "") as $x | ($assoc | lst) | index($x) != null)
|
|
80
131
|
or ((.user.type // "") == "Bot")
|
|
81
132
|
or ((.user.login // "") as $x | ($logins | lst) | index($x) != null);'
|
|
82
|
-
CLAIM_RE='^\s*<!-- FORGE:DEFERRED_ISSUE: #(?<n>[0-9]+) -->'
|
|
133
|
+
CLAIM_RE='^\s*<!-- FORGE:DEFERRED_ISSUE: #(?<n>[0-9]+)(?: class=[a-z-]+)? -->'
|
|
83
134
|
claim_nums() {
|
|
84
135
|
gh api --paginate "repos/${REPO}/issues/${PARENT}/comments" 2>/dev/null \
|
|
85
136
|
| jq -s -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg re "$CLAIM_RE" "$JQ_PRE"'
|
|
@@ -91,12 +142,13 @@ search_nums() {
|
|
|
91
142
|
| jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
|
|
92
143
|
[.items[] | select(trusted) | select((.body // "") | contains($m)) | .number] | sort | .[]' 2>/dev/null
|
|
93
144
|
}
|
|
94
|
-
verify_num() { # number -> `number<TAB>state` when a trusted, non-PR issue 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; empty when not (a 404 is "not it")
|
|
95
146
|
local out rc
|
|
96
147
|
out=$(gh api "repos/${REPO}/issues/$1" 2>"$TMP/verr"); rc=$?
|
|
97
148
|
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"'
|
|
149
|
+
printf '%s' "$out" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" --arg cls "$CLS" "$JQ_PRE"'
|
|
99
150
|
select(.pull_request == null) | select(trusted) | select((.body // "") | contains($m))
|
|
151
|
+
| select((((.body // "") | capture("FORGE:DEFERRED_CLASS: (?<c>[a-z-]+)") | .c) // "engineering") == $cls)
|
|
100
152
|
| "\(.number)\t\(.state)"' 2>/dev/null
|
|
101
153
|
}
|
|
102
154
|
CANON=""; CSTATE=""
|
|
@@ -120,15 +172,127 @@ find_canonical() {
|
|
|
120
172
|
pick_verified $cands || return 1
|
|
121
173
|
return 0
|
|
122
174
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
175
|
+
# --- #4724: the post-deploy rollup: ONE open issue per release lane, found by marker (not by parent) ------------------
|
|
176
|
+
RELEASE_LANE="${FORGE_RELEASE_LANE:-default}"
|
|
177
|
+
ROLLUP_MARKER="<!-- FORGE:POSTDEPLOY_ROLLUP: ${RELEASE_LANE} -->"
|
|
178
|
+
verify_rollup() { # number -> `number<TAB>open` when a trusted, open, non-PR issue carries the rollup marker
|
|
179
|
+
local out rc
|
|
180
|
+
out=$(gh api "repos/${REPO}/issues/$1" 2>"$TMP/verr"); rc=$?
|
|
181
|
+
if [ "$rc" -ne 0 ]; then grep -qE '404|Not Found|410' "$TMP/verr" 2>/dev/null && return 0; return 1; fi
|
|
182
|
+
printf '%s' "$out" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$ROLLUP_MARKER" "$JQ_PRE"'
|
|
183
|
+
select(.pull_request == null) | select(trusted) | select(.state == "open") | select((.body // "") | contains($m))
|
|
184
|
+
| "\(.number)\t\(.state)"' 2>/dev/null
|
|
185
|
+
}
|
|
186
|
+
find_rollup() { # sets CANON/CSTATE to the lowest open rollup of the lane; non-zero only when a lookup failed
|
|
187
|
+
local cands n r
|
|
188
|
+
CANON=""; CSTATE=""
|
|
189
|
+
cands=$(gh api -X GET search/issues -f per_page=50 -f q="\"FORGE:POSTDEPLOY_ROLLUP: ${RELEASE_LANE}\" in:body repo:${REPO} is:issue is:open" 2>/dev/null \
|
|
190
|
+
| jq -r '[.items[].number] | sort | .[]' 2>/dev/null) || return 1
|
|
191
|
+
for n in $cands; do
|
|
192
|
+
r=$(verify_rollup "$n") || return 1
|
|
193
|
+
if [ -n "$r" ]; then CANON="${r%%$'\t'*}"; CSTATE="${r#*$'\t'}"; return 0; fi
|
|
194
|
+
done
|
|
195
|
+
return 0
|
|
196
|
+
}
|
|
197
|
+
# The root parent: a parent that is itself a follow-up (or the rollup) files its items under the parent it came from.
|
|
198
|
+
ROOT="$PARENT"
|
|
199
|
+
resolve_root() {
|
|
200
|
+
local pb first
|
|
201
|
+
pb=$(gh api "repos/${REPO}/issues/${PARENT}" 2>/dev/null | jq -r '.body // ""' 2>/dev/null) || return 0
|
|
202
|
+
first=$(printf '%s' "$pb" | grep -oE 'FORGE:DEFERRED_FROM: #[0-9]+' | head -n 1 | grep -oE '[0-9]+$')
|
|
203
|
+
[ -n "$first" ] && ROOT="$first"
|
|
204
|
+
return 0
|
|
205
|
+
}
|
|
206
|
+
norm() { # normalized item text: no `R<n>:` prefix, lower case, punctuation and spacing collapsed (dedup across lanes)
|
|
207
|
+
printf '%s' "$1" | sed -E 's/^R[0-9]+:[[:space:]]*//' | tr 'A-Z' 'a-z' | tr -c 'a-z0-9\n' ' ' | tr -s ' ' | sed -E 's/^ //; s/ $//'
|
|
208
|
+
}
|
|
209
|
+
# Keep only the items of $1 whose normalized text is not yet in $2 (a body); the kept ones are appended to "$TMP/add".
|
|
210
|
+
rollup_missing() { # items-file body-file
|
|
211
|
+
local p w k fp cl n
|
|
212
|
+
: > "$TMP/add"; : > "$TMP/have"
|
|
213
|
+
grep -E '^- \[[ xX]\] `' "$2" 2>/dev/null | sed -E 's/^- \[[ xX]\] `([^`]*)`.*/\1/' | while IFS= read -r l; do norm "$l"; echo; done > "$TMP/have"
|
|
214
|
+
while IFS="$SEP" read -r p w k fp cl; do
|
|
215
|
+
n=$(norm "$p")
|
|
216
|
+
grep -qxF -- "$n" "$TMP/have" && continue
|
|
217
|
+
entry "$p" "$w" "$k" >> "$TMP/add"; printf '%s\n' "$n" >> "$TMP/have"
|
|
218
|
+
done < "$1"
|
|
219
|
+
}
|
|
220
|
+
rollup_append() { # number ; non-zero = failure
|
|
221
|
+
local num="$1"
|
|
222
|
+
gh api "repos/${REPO}/issues/${num}" --jq '.body // ""' > "$TMP/body" 2>/dev/null || return 1
|
|
223
|
+
rollup_missing "$ITEMS_F" "$TMP/body"
|
|
224
|
+
[ -s "$TMP/add" ] || return 0
|
|
225
|
+
awk -v h="### #${ROOT}" -v root="$ROOT" -v addf="$TMP/add" '
|
|
226
|
+
BEGIN { while ((getline l < addf) > 0) add = add l "\n" }
|
|
227
|
+
{ if (insec && $0 ~ /^### /) { printf "%s\n", add; insec = 0; done = 1 } if ($0 == h) insec = 1; print }
|
|
228
|
+
END { if (insec) printf "%s", add; else if (!done) printf "\n%s\n<!-- FORGE:DEFERRED_FROM: #%s -->\n\n%s", h, root, add }' "$TMP/body" > "$TMP/newbody"
|
|
229
|
+
if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: gh issue edit $num (append $(wc -l < "$TMP/add" | tr -d ' ') item(s) under #${ROOT})" >&2
|
|
230
|
+
else gh issue edit "$num" -R "$REPO" --body-file "$TMP/newbody" >/dev/null 2>&1 || return 1; fi
|
|
231
|
+
return 0
|
|
232
|
+
}
|
|
233
|
+
file_rollup() { # items in $ITEMS_F; sets FILED_N, FILED_NEW
|
|
234
|
+
local n_items; n_items=$(wc -l < "$ITEMS_F" | tr -d ' ')
|
|
235
|
+
FILED_N=""; FILED_NEW=0
|
|
236
|
+
resolve_root
|
|
237
|
+
CANON=$(cut -f1 "$TMP/canon.$CLS")
|
|
238
|
+
if [ -n "$CANON" ]; then
|
|
239
|
+
rollup_append "$CANON" || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
240
|
+
FILED_N="$CANON"; return 0
|
|
241
|
+
fi
|
|
242
|
+
: > "$TMP/body"; rollup_missing "$ITEMS_F" "$TMP/body"
|
|
243
|
+
{
|
|
244
|
+
printf '## Problem\n\nPost-deploy checklist of release lane `%s`%s. Every merged lane appends its post-deploy items under a `### #<parent>` heading (deduplicated), so one read-only verification works through this one issue per release.\n\n' "$RELEASE_LANE" "${FORGE_RELEASE_SHA:+ (deploy target at $FORGE_RELEASE_SHA)}"
|
|
245
|
+
printf '## Acceptance Criteria\n\n### #%s\n<!-- FORGE:DEFERRED_FROM: #%s -->\n\n' "$ROOT" "$ROOT"
|
|
246
|
+
cat "$TMP/add"
|
|
247
|
+
printf '\n%s\n<!-- FORGE:DEFERRED_CLASS: post-deploy -->\n' "$ROLLUP_MARKER"
|
|
248
|
+
} > "$TMP/newissue"
|
|
249
|
+
local NUM
|
|
250
|
+
if [ -n "${DRY_RUN:-}" ]; then
|
|
251
|
+
echo "DRY_RUN: gh issue create (post-deploy rollup ${RELEASE_LANE}: ${n_items} item(s) from #${PARENT})" >&2
|
|
252
|
+
NUM=0
|
|
253
|
+
else
|
|
254
|
+
resolve_milestone
|
|
255
|
+
local args=() URL
|
|
256
|
+
gh label create "followup:post-deploy" -R "$REPO" --force --color BFD4F2 --description "Deferred post-deploy checks. Managed by ForgeDock." >/dev/null 2>&1 || true
|
|
257
|
+
args=(--label "followup:post-deploy")
|
|
258
|
+
[ -n "$MS_TITLE" ] && args+=(--milestone "$MS_TITLE")
|
|
259
|
+
URL=$(gh issue create -R "$REPO" --title "post-deploy: release checklist (${RELEASE_LANE})" --body-file "$TMP/newissue" "${args[@]}" 2>/dev/null) || URL=""
|
|
260
|
+
NUM=$(printf '%s' "$URL" | grep -oE '[0-9]+$' | tail -n 1)
|
|
261
|
+
[ -n "$NUM" ] || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
262
|
+
# Post-create re-check: a concurrent lane may have filed first. The lowest open rollup is canonical; the newer one is closed.
|
|
263
|
+
if find_rollup; then
|
|
264
|
+
if [ -n "$CANON" ] && [ "$CANON" -lt "$NUM" ]; then
|
|
265
|
+
if rollup_append "$CANON"; then
|
|
266
|
+
gh issue close "$NUM" -R "$REPO" --reason "not planned" --comment "Duplicate of #${CANON}: the post-deploy items of lane ${RELEASE_LANE} are tracked there." >/dev/null 2>&1 \
|
|
267
|
+
|| echo "file-deferred-followups: warning: could not close duplicate #${NUM}" >&2
|
|
268
|
+
NUM="$CANON"
|
|
269
|
+
else
|
|
270
|
+
echo "file-deferred-followups: warning: could not move items to #${CANON}; keeping #${NUM}" >&2
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
else
|
|
274
|
+
echo "file-deferred-followups: warning: post-create re-check failed; keeping #${NUM}" >&2
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
277
|
+
FILED_N="$NUM"; FILED_NEW=1
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
CLS_LIST=$(cut -d "$SEP" -f5 "$TMP/items" | sort -u)
|
|
281
|
+
CLS=""
|
|
282
|
+
# Phase 1: look up the canonical follow-up of every class before filing anything (a failed lookup files nothing).
|
|
283
|
+
for CLS in $CLS_LIST; do
|
|
284
|
+
LOOKUP_OK=""
|
|
285
|
+
for attempt in 1 2; do
|
|
286
|
+
if { [ "$CLS" = "post-deploy" ] && find_rollup; } || { [ "$CLS" != "post-deploy" ] && find_canonical; }; then LOOKUP_OK=1; break; fi
|
|
287
|
+
[ "$attempt" = 1 ] && sleep "${FORGE_DEFERRED_RETRY_SLEEP:-2}"
|
|
288
|
+
done
|
|
289
|
+
if [ -z "$LOOKUP_OK" ]; then
|
|
290
|
+
echo "DEFERRED_LOOKUP=FAILED"
|
|
291
|
+
exit 3
|
|
292
|
+
fi
|
|
293
|
+
printf '%s\t%s\n' "$CANON" "$CSTATE" > "$TMP/canon.$CLS"
|
|
294
|
+
awk -F"$SEP" -v c="$CLS" '$5 == c' "$TMP/items" > "$TMP/items.$CLS"
|
|
127
295
|
done
|
|
128
|
-
if [ -z "$LOOKUP_OK" ]; then
|
|
129
|
-
echo "DEFERRED_LOOKUP=FAILED"
|
|
130
|
-
exit 3
|
|
131
|
-
fi
|
|
132
296
|
|
|
133
297
|
# Checklist line for one item. Path always backticked so review-pr 6B.5 `contains` verification holds.
|
|
134
298
|
entry() { # path why flag
|
|
@@ -148,9 +312,9 @@ append_items() { # number state ; non-zero = failure
|
|
|
148
312
|
# Re-read the body immediately before editing (parallel builds of the same parent), then append what is missing.
|
|
149
313
|
gh api "repos/${REPO}/issues/${num}" --jq '.body // ""' > "$TMP/body" 2>/dev/null || return 1
|
|
150
314
|
: > "$TMP/add"
|
|
151
|
-
while IFS="$SEP" read -r p w k; do
|
|
315
|
+
while IFS="$SEP" read -r p w k fp cl; do
|
|
152
316
|
has_entry "$TMP/body" "$p" || entry "$p" "$w" "$k" >> "$TMP/add"
|
|
153
|
-
done < "$
|
|
317
|
+
done < "$ITEMS_F"
|
|
154
318
|
[ "$state" = "closed" ] && note=" (follow-up was closed; reopened)"
|
|
155
319
|
if [ -s "$TMP/add" ]; then
|
|
156
320
|
{ cat "$TMP/body"; printf '\n### Added by a later build of #%s%s\n\n' "$PARENT" "$note"; cat "$TMP/add"; } > "$TMP/newbody"
|
|
@@ -167,54 +331,99 @@ append_items() { # number state ; non-zero = failure
|
|
|
167
331
|
return 0
|
|
168
332
|
}
|
|
169
333
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
fi
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
while IFS="$SEP" read -r p w k; do printf -- '- `%s`\n' "$p"; done < "$TMP/items"
|
|
185
|
-
printf '\n## Acceptance Criteria\n\n'
|
|
186
|
-
while IFS="$SEP" read -r p w k; do entry "$p" "$w" "$k"; done < "$TMP/items"
|
|
187
|
-
printf '\n%s\n' "$MARKER"
|
|
188
|
-
} > "$TMP/newissue"
|
|
334
|
+
MS_DONE=""; MS_TITLE=""
|
|
335
|
+
resolve_milestone() { # sets MS_TITLE (empty = none): the parent's milestone, or for a shipped one the next open milestone, else the configured default
|
|
336
|
+
[ -n "$MS_DONE" ] && return 0
|
|
337
|
+
MS_DONE=1; MS_TITLE=""
|
|
338
|
+
local t="${FORGE_FOLLOWUP_MILESTONE:-}" all
|
|
339
|
+
if [ -z "$t" ]; then t=$(gh api "repos/${REPO}/issues/${PARENT}" --jq '.milestone.title // ""' 2>/dev/null) || t=""; fi
|
|
340
|
+
[ -n "$t" ] || return 0
|
|
341
|
+
all=$(gh api "repos/${REPO}/milestones?state=all&per_page=100" 2>/dev/null) || { MS_TITLE="$t"; return 0; }
|
|
342
|
+
MS_TITLE=$(printf '%s' "$all" | jq -r --arg t "$t" --arg d "${FORGE_FOLLOWUPS_DEFAULT_MILESTONE:-}" '
|
|
343
|
+
. as $all | ($all | map(select(.title == $t)) | first // null) as $cur
|
|
344
|
+
| if $cur == null or $cur.state == "open" then $t
|
|
345
|
+
else (($all | map(select(.state == "open" and .due_on != null and ($cur.due_on == null or .due_on > $cur.due_on))) | sort_by(.due_on) | (first // {}) | .title // "") as $next
|
|
346
|
+
| if $next != "" then $next elif ($d != "" and ($all | map(select(.title == $d and .state == "open")) | length) > 0) then $d else "" end) end' 2>/dev/null) || MS_TITLE="$t"
|
|
347
|
+
}
|
|
189
348
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
fi
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if [
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
NUM="$CANON"
|
|
210
|
-
else
|
|
211
|
-
echo "file-deferred-followups: warning: could not move items to #${CANON}; keeping #${NUM}" >&2
|
|
212
|
-
fi
|
|
349
|
+
# File one class ($CLS, items in $ITEMS_F): append to its canonical follow-up, or create one. Sets FILED_N, FILED_NEW.
|
|
350
|
+
file_class() {
|
|
351
|
+
local n_items; n_items=$(wc -l < "$ITEMS_F" | tr -d ' ')
|
|
352
|
+
FILED_N=""; FILED_NEW=0
|
|
353
|
+
CANON=$(cut -f1 "$TMP/canon.$CLS"); CSTATE=$(cut -f2 "$TMP/canon.$CLS")
|
|
354
|
+
if [ -n "$CANON" ]; then
|
|
355
|
+
append_items "$CANON" "$CSTATE" || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
356
|
+
FILED_N="$CANON"; return 0
|
|
357
|
+
fi
|
|
358
|
+
local claim="<!-- FORGE:DEFERRED_ISSUE: #%s -->" title="follow-up: ${n_items} deferred item(s) (deferred from #${PARENT})"
|
|
359
|
+
if [ "$CLS" != "engineering" ]; then claim="<!-- FORGE:DEFERRED_ISSUE: #%s class=${CLS} -->"; title="follow-up (${CLS}): ${n_items} deferred item(s) (deferred from #${PARENT})"; fi
|
|
360
|
+
local paths; paths=$(awk -F"$SEP" '$4 != "-" { print $4 }' "$ITEMS_F" | awk '!seen[$0]++')
|
|
361
|
+
{
|
|
362
|
+
printf '## Problem\n\n'
|
|
363
|
+
printf 'Deferred from #%s: the items below were declared out of scope in its contract and are tracked here, in one issue per class (%s), instead of surfacing later as review findings.\n\n' "$PARENT" "$CLS"
|
|
364
|
+
if [ "$CLS" = "external" ]; then
|
|
365
|
+
printf 'External prerequisite of #%s: these items wait on something outside the repo (credentials, an account, a third party).' "$PARENT"
|
|
366
|
+
[ -n "${FORGE_EXTERNAL_PREREQ:-}" ] && printf ' Linked prerequisite: #%s.' "${FORGE_EXTERNAL_PREREQ#\#}"
|
|
367
|
+
printf '\n\n'
|
|
213
368
|
fi
|
|
369
|
+
printf '## Affected Files\n\n'
|
|
370
|
+
if [ -n "$paths" ]; then printf '%s\n' "$paths" | while IFS= read -r fp; do printf -- '- `%s`\n' "$fp"; done
|
|
371
|
+
else printf -- '- None (requirement-level items, see the checklist)\n'; fi
|
|
372
|
+
printf '\n## Acceptance Criteria\n\n'
|
|
373
|
+
while IFS="$SEP" read -r p w k fp cl; do entry "$p" "$w" "$k"; done < "$ITEMS_F"
|
|
374
|
+
printf '\n%s\n<!-- FORGE:DEFERRED_CLASS: %s -->\n' "$MARKER" "$CLS"
|
|
375
|
+
} > "$TMP/newissue"
|
|
376
|
+
local NUM
|
|
377
|
+
if [ -n "${DRY_RUN:-}" ]; then
|
|
378
|
+
echo "DRY_RUN: gh issue create (follow-up ${CLS}: ${n_items} deferred item(s) from #${PARENT})" >&2
|
|
379
|
+
NUM=0
|
|
214
380
|
else
|
|
215
|
-
|
|
381
|
+
resolve_milestone
|
|
382
|
+
local lbl="followup:${CLS}" args=() URL
|
|
383
|
+
gh label create "$lbl" -R "$REPO" --force --color BFD4F2 --description "Deferred follow-up (${CLS}). Managed by ForgeDock." >/dev/null 2>&1 || true
|
|
384
|
+
args=(--label "$lbl")
|
|
385
|
+
if [ "$CLS" = "external" ]; then
|
|
386
|
+
gh label create external-prerequisite -R "$REPO" --force --color E99695 --description "External prerequisite. Managed by ForgeDock." >/dev/null 2>&1 || true
|
|
387
|
+
args+=(--label external-prerequisite)
|
|
388
|
+
fi
|
|
389
|
+
[ -n "$MS_TITLE" ] && args+=(--milestone "$MS_TITLE")
|
|
390
|
+
URL=$(gh issue create -R "$REPO" --title "$title" --body-file "$TMP/newissue" "${args[@]}" 2>/dev/null) || URL=""
|
|
391
|
+
NUM=$(printf '%s' "$URL" | grep -oE '[0-9]+$' | tail -n 1)
|
|
392
|
+
[ -n "$NUM" ] || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
393
|
+
fi
|
|
394
|
+
if [ -z "${DRY_RUN:-}" ]; then
|
|
395
|
+
# Claim the follow-up on the parent (REST comments are read-after-write consistent; best effort, the search still finds it).
|
|
396
|
+
# shellcheck disable=SC2059
|
|
397
|
+
gh issue comment "$PARENT" -R "$REPO" --body "$(printf "$claim" "$NUM")" >/dev/null 2>&1 \
|
|
398
|
+
|| echo "file-deferred-followups: warning: could not post claim marker for #${NUM} on #${PARENT}" >&2
|
|
399
|
+
# Post-create re-check: a concurrent build may have filed first. The lowest trusted number is canonical; the newer one is closed.
|
|
400
|
+
if find_canonical; then
|
|
401
|
+
if [ -n "$CANON" ] && [ "$CANON" -lt "$NUM" ]; then
|
|
402
|
+
if append_items "$CANON" "$CSTATE"; then
|
|
403
|
+
gh issue close "$NUM" -R "$REPO" --reason "not planned" --comment "Duplicate of #${CANON}: deferred items of #${PARENT} are tracked there." >/dev/null 2>&1 \
|
|
404
|
+
|| echo "file-deferred-followups: warning: could not close duplicate #${NUM}" >&2
|
|
405
|
+
NUM="$CANON"
|
|
406
|
+
else
|
|
407
|
+
echo "file-deferred-followups: warning: could not move items to #${CANON}; keeping #${NUM}" >&2
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
else
|
|
411
|
+
echo "file-deferred-followups: warning: post-create re-check failed; keeping #${NUM}" >&2
|
|
412
|
+
fi
|
|
216
413
|
fi
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
414
|
+
FILED_N="$NUM"; FILED_NEW=1
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
[ "$TOTAL" -gt 0 ] || { finish "" 0; exit 0; }
|
|
418
|
+
|
|
419
|
+
# Phase 2: one issue per class.
|
|
420
|
+
INTO=""; FILED=0
|
|
421
|
+
for CLS in $CLS_LIST; do
|
|
422
|
+
ITEMS_F="$TMP/items.$CLS"
|
|
423
|
+
if [ "$CLS" = "post-deploy" ]; then file_rollup; else file_class; fi
|
|
424
|
+
case " $INTO " in *" $FILED_N "*) ;; *) INTO="${INTO:+$INTO }$FILED_N" ;; esac
|
|
425
|
+
FILED=$((FILED + FILED_NEW))
|
|
426
|
+
while IFS="$SEP" read -r p w k fp cl; do printf '%s\t%s\n' "$p" "$FILED_N"; done < "$ITEMS_F"
|
|
427
|
+
done
|
|
428
|
+
finish "$INTO" "$FILED"
|
|
220
429
|
exit 0
|
|
@@ -16,7 +16,8 @@ echo "$*" >> "$GH_LOG"
|
|
|
16
16
|
case "$1 $2" in
|
|
17
17
|
"api -X") [ -n "${LOOKUP_FAIL:-}" ] && exit 1; cat "$SEARCH_JSON"; exit 0 ;;
|
|
18
18
|
"api --paginate") [ -n "${CLAIM_FAIL:-}" ] && exit 1; cat "$COMMENTS_JSON"; exit 0 ;;
|
|
19
|
-
"issue create") [ -n "${CREATE_FAIL:-}" ] && exit 1; echo "https://github.com/o/r/issues
|
|
19
|
+
"issue create") [ -n "${CREATE_FAIL:-}" ] && exit 1; N=$(( $(cat "$T_DIR/create.n" 2>/dev/null || echo 900) + 1 )); echo "$N" > "$T_DIR/create.n"; while [ $# -gt 0 ]; do [ "$1" = "--body-file" ] && cp "$2" "$T_DIR/created.$N.body"; shift; done; echo "https://github.com/o/r/issues/$N"; exit 0 ;;
|
|
20
|
+
"label create") exit 0 ;;
|
|
20
21
|
"issue comment") [ -n "${COMMENT_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
21
22
|
"issue reopen") [ -n "${REOPEN_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
22
23
|
"issue close") [ -n "${CLOSE_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
@@ -25,6 +26,8 @@ case "$1 $2" in
|
|
|
25
26
|
while [ $# -gt 0 ]; do [ "$1" = "--body-file" ] && cp "$2" "$ISSUE_BODY.edited"; shift; done; exit 0 ;;
|
|
26
27
|
esac
|
|
27
28
|
if [ "$1" = api ]; then
|
|
29
|
+
case "$*" in "api repos/o/r/milestones"*) cat "$T_DIR/milestones.json"; exit 0 ;; esac
|
|
30
|
+
case "$*" in "api repos/o/r/issues/10 --jq"*) cat "$T_DIR/parent.milestone"; exit 0 ;; esac
|
|
28
31
|
case "$*" in *--jq*) cat "$ISSUE_BODY"; exit 0 ;; esac
|
|
29
32
|
n="${2##*/}"; [ -f "$T_DIR/issue.$n.json" ] && { cat "$T_DIR/issue.$n.json"; exit 0; }
|
|
30
33
|
echo "HTTP 404: Not Found" >&2; exit 1
|
|
@@ -37,7 +40,8 @@ PASS=0; FAIL=0
|
|
|
37
40
|
ok() { PASS=$((PASS+1)); echo "PASS: $1"; }
|
|
38
41
|
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1"; }
|
|
39
42
|
check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; }
|
|
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
|
|
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
|
+
echo '[]' > "$T/milestones.json"; : > "$T/parent.milestone"
|
|
41
45
|
echo '{"items":[]}' > "$SEARCH_JSON"; echo '[]' > "$COMMENTS_JSON"; }
|
|
42
46
|
# issue_json NUMBER STATE ASSOCIATION [PARENT]: REST view of a follow-up carrying the DEFERRED_FROM marker
|
|
43
47
|
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"; }
|
|
@@ -195,5 +199,116 @@ printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
|
|
|
195
199
|
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
196
200
|
check "dry-run: no comment/reopen/close/edit" "! grep -qE '^issue (comment|reopen|close|edit|create)' '$GH_LOG'"
|
|
197
201
|
|
|
202
|
+
# 18. #4626: a post-deploy why's [check:<id>] token is kept verbatim in the checklist line
|
|
203
|
+
reset
|
|
204
|
+
printf 'x\n' > "$ISSUE_BODY"
|
|
205
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
206
|
+
printf '{"number":77,"state":"open","author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:POSTDEPLOY_ROLLUP: default -->\\n<!-- FORGE:DEFERRED_FROM: #10 -->\\n### #10\\n<!-- FORGE:DEFERRED_CLASS: post-deploy -->"}\n' > "$T/issue.77.json"
|
|
207
|
+
printf -- '-\tpost-deploy: [check:login-smoke] after a day\t\t\tR3: soak\n' > "$T/check.tsv"
|
|
208
|
+
bash "$S" --parent 10 --repo o/r --items "$T/check.tsv" >/dev/null 2>&1
|
|
209
|
+
check "check token kept in the checklist line" "grep -q 'post-deploy: \[check:login-smoke\] after a day' '$ISSUE_BODY.edited'"
|
|
210
|
+
|
|
211
|
+
# 19. #4664: milestone inheritance
|
|
212
|
+
MS='[{"title":"M1","state":"open","due_on":"2026-01-01T00:00:00Z"},{"title":"M0","state":"closed","due_on":"2025-06-01T00:00:00Z"},{"title":"M2","state":"open","due_on":"2026-06-01T00:00:00Z"},{"title":"Dflt","state":"open","due_on":null}]'
|
|
213
|
+
reset; echo "$MS" > "$T/milestones.json"; export FORGE_FOLLOWUP_MILESTONE=M1
|
|
214
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
215
|
+
check "milestone: open parent milestone inherited" "grep '^issue create' '$GH_LOG' | grep -q -- '--milestone M1'"
|
|
216
|
+
reset; echo "$MS" > "$T/milestones.json"; echo M1 > "$T/parent.milestone"
|
|
217
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
218
|
+
check "milestone: read from the parent when not passed" "grep '^issue create' '$GH_LOG' | grep -q -- '--milestone M1'"
|
|
219
|
+
reset; echo "$MS" > "$T/milestones.json"; export FORGE_FOLLOWUP_MILESTONE=M0
|
|
220
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
221
|
+
check "milestone: shipped (closed) one -> next open by due date" "grep '^issue create' '$GH_LOG' | grep -q -- '--milestone M1'"
|
|
222
|
+
reset; echo '[{"title":"M0","state":"closed","due_on":"2025-06-01T00:00:00Z"},{"title":"Dflt","state":"open","due_on":null}]' > "$T/milestones.json"
|
|
223
|
+
export FORGE_FOLLOWUP_MILESTONE=M0 FORGE_FOLLOWUPS_DEFAULT_MILESTONE=Dflt
|
|
224
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
225
|
+
check "milestone: shipped, no next -> followups.default_milestone" "grep '^issue create' '$GH_LOG' | grep -q -- '--milestone Dflt'"
|
|
226
|
+
reset; echo '[]' > "$T/milestones.json"
|
|
227
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
228
|
+
check "milestone: parent without one -> none passed" "grep -q '^issue create' '$GH_LOG' && ! grep '^issue create' '$GH_LOG' | grep -q -- '--milestone'"
|
|
229
|
+
|
|
230
|
+
# 20. #4664: one issue per class, class labels
|
|
231
|
+
printf 'a/x.sh\twhy a\t\nb/y.sh\tpost-deploy: [check:smoke] later\t\t\nc/z.sh\texternal: needs a real credential\t\t\nd/w.sh\tops drill\t\tops\n-\tdefer: plain\t\t\tR9: plain item\n' > "$T/classes.tsv"
|
|
232
|
+
reset
|
|
233
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/classes.tsv"); RC=$?
|
|
234
|
+
check "classes: rc 0, one create per class (4)" "[ $RC -eq 0 ] && [ \$(grep -c '^issue create' '$GH_LOG') -eq 4 ]"
|
|
235
|
+
for c in engineering post-deploy external ops; do check "classes: label followup:$c applied" "grep '^issue create' '$GH_LOG' | grep -q -- '--label followup:$c'"; done
|
|
236
|
+
check "classes: external also labelled external-prerequisite" "grep '^issue create' '$GH_LOG' | grep -- 'followup:external' | grep -q -- '--label external-prerequisite'"
|
|
237
|
+
check "classes: engineering has the old title, others carry the class" "grep '^issue create' '$GH_LOG' | grep -q 'follow-up (ops): 1 deferred' && grep '^issue create' '$GH_LOG' | grep -q 'follow-up: 2 deferred'"
|
|
238
|
+
check "classes: telemetry lists every issue and class" "printf '%s' \"\$OUT\" | grep -q 'batched_into=#901,#902,#903,#904 filed=4 classes=engineering,external,ops,post-deploy'"
|
|
239
|
+
check "classes: rows point at their class issue (a/x.sh and the R9 item share one)" "printf '%s\n' \"\$OUT\" | grep -q '^c/z.sh 90[0-9]\$' && [ \$(printf '%s\n' \"\$OUT\" | grep -E '^(a/x.sh|R9: plain item) ' | cut -f2 | sort -u | wc -l) -eq 1 ]"
|
|
240
|
+
check "classes: claim markers carry the class" "grep '^issue comment 10' '$GH_LOG' | grep -q 'class=external'"
|
|
241
|
+
# an existing engineering follow-up is not reused for another class
|
|
242
|
+
reset
|
|
243
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 open OWNER; claim_json OWNER 77
|
|
244
|
+
printf 'c/z.sh\texternal: needs a credential\t\n' > "$T/ext.tsv"
|
|
245
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/ext.tsv"); RC=$?
|
|
246
|
+
check "classes: engineering follow-up #77 not reused for an external item" "[ $RC -eq 0 ] && grep -q '^issue create' '$GH_LOG' && ! grep -q '^issue edit 77' '$GH_LOG'"
|
|
247
|
+
# a labelled-class follow-up is reused for its class
|
|
248
|
+
reset
|
|
249
|
+
printf 'x\n' > "$ISSUE_BODY"
|
|
250
|
+
printf '{"number":78,"state":"open","author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->\\n<!-- FORGE:DEFERRED_CLASS: external -->"}\n' > "$T/issue.78.json"; claim_json OWNER 78
|
|
251
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/ext.tsv"); RC=$?
|
|
252
|
+
check "classes: external follow-up #78 reused for external items" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG' && grep -q '^issue edit 78' '$GH_LOG'"
|
|
253
|
+
|
|
254
|
+
# 21. #4664: requirement-text rows are rejected, nothing filed
|
|
255
|
+
for row in 'R1: a saved mapping per entity\twhy\t' 'R1: R2: a saved mapping\twhy\t' 'add a saved mapping\twhy\t' 'a/x.sh\twhy\t\tbogus'; do
|
|
256
|
+
reset; printf "$row\n" > "$T/bad.tsv"
|
|
257
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/bad.tsv" 2>/dev/null); RC=$?
|
|
258
|
+
check "rejected row ($(printf "$row" | cut -c1-24)): rc 2, DEFERRED_TSV=INVALID, no gh call" "[ $RC -eq 2 ] && printf '%s' \"\$OUT\" | grep -q DEFERRED_TSV=INVALID && [ ! -s '$GH_LOG' ]"
|
|
259
|
+
done
|
|
260
|
+
reset; printf 'a/x.sh\twhy\t\nR2: nope\twhy\t\n' > "$T/bad.tsv"
|
|
261
|
+
bash "$S" --parent 10 --repo o/r --items "$T/bad.tsv" >/dev/null 2>&1
|
|
262
|
+
check "one malformed row among valid ones files nothing" "[ $? -eq 2 ] && ! grep -q '^issue create' '$GH_LOG'"
|
|
263
|
+
# `-` + label is a valid requirement-level row; a doubled R-prefix in the label collapses; Affected Files lists only paths
|
|
264
|
+
reset; printf '%s\n' "$(printf -- '-\twhy\t\t\tR1: R2: a saved mapping')" "$(printf 'src/q.ts\twhy q\t')" > "$T/ok.tsv"
|
|
265
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/ok.tsv"); RC=$?
|
|
266
|
+
check "label row accepted, prefix collapsed in stdout" "[ $RC -eq 0 ] && printf '%s\n' \"\$OUT\" | grep -q '^R1: a saved mapping 901'"
|
|
267
|
+
|
|
268
|
+
check "created body: Affected Files lists only paths, checklist keeps the collapsed label" "grep -q '^- \`src/q.ts\`\$' '$T/created.901.body' && ! grep -q '^- \`R1' '$T/created.901.body' && grep -q '^- \[ \] \`R1: a saved mapping\`' '$T/created.901.body' && ! grep -q 'R1: R2:' '$T/created.901.body'"
|
|
269
|
+
check "created body: DEFERRED_FROM and DEFERRED_CLASS markers" "grep -q 'FORGE:DEFERRED_FROM: #10 -->' '$T/created.901.body' && grep -q 'FORGE:DEFERRED_CLASS: engineering' '$T/created.901.body'"
|
|
270
|
+
# #4739: legal path characters (parentheses, space, =, #, :) are accepted; one such row no longer aborts the batch
|
|
271
|
+
reset; printf 'app/(marketing)/page.tsx\twhy\t\ndocs/My Notes.md\twhy\t\nsrc/a=b.ts\twhy\t\nsrc/#x.ts\twhy\t\nsrc/foo:bar.ts\twhy\t\n' > "$T/legal.tsv"
|
|
272
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/legal.tsv" 2>/dev/null); RC=$?
|
|
273
|
+
check "legal paths with ( ) space = # : are accepted and filed" "[ $RC -eq 0 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TSV=INVALID && grep -q '^issue create' '$GH_LOG' && grep -qF -- '- \`app/(marketing)/page.tsx\`' '$T/created.901.body' && grep -qF -- '- \`docs/My Notes.md\`' '$T/created.901.body' && grep -qF -- '- \`src/a=b.ts\`' '$T/created.901.body' && grep -qF -- '- \`src/#x.ts\`' '$T/created.901.body' && grep -qF -- '- \`src/foo:bar.ts\`' '$T/created.901.body'"
|
|
274
|
+
reset; printf -- '-\tpost-deploy: [check:login-smoke] soon\t\t\tR3: soak\n' > "$T/pd.tsv"
|
|
275
|
+
bash "$S" --parent 10 --repo o/r --items "$T/pd.tsv" >/dev/null 2>&1
|
|
276
|
+
check "post-deploy rollup: created with the lane marker, [check:] kept, section for the parent" "grep -q 'FORGE:POSTDEPLOY_ROLLUP: default' '$T/created.901.body' && grep -q 'post-deploy: \[check:login-smoke\] soon' '$T/created.901.body' && grep -q '^### #10\$' '$T/created.901.body'"
|
|
277
|
+
|
|
278
|
+
# 23. #4724: N lanes -> ONE rollup; sections per parent; duplicates stored once; owner items never join it
|
|
279
|
+
reset
|
|
280
|
+
printf -- '-\tpost-deploy: [bucket:post-deploy] [check:a-smoke] first\t\t\tR1: the login page renders\n-\towner: [bucket:owner] rotate the key. why\t\t\tR2: rotate the credential\n' > "$T/l1.tsv"
|
|
281
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/l1.tsv"); RC=$?
|
|
282
|
+
check "rollup: lane 1 creates one external follow-up (#901) and one rollup (#902)" "[ $RC -eq 0 ] && [ \$(grep -c '^issue create' '$GH_LOG') -eq 2 ] && grep -q 'FORGE:POSTDEPLOY_ROLLUP: default' '$T/created.902.body' && grep -q 'FORGE:DEFERRED_CLASS: external' '$T/created.901.body'"
|
|
283
|
+
check "rollup: the owner item is not in the rollup" "! grep -q 'rotate the credential' '$T/created.902.body' && grep -q 'rotate the credential' '$T/created.901.body'"
|
|
284
|
+
check "rollup: telemetry has per-bucket counts" "printf '%s' \"\$OUT\" | grep -q 'buckets=owner:1,post-deploy:1'"
|
|
285
|
+
cp "$T/created.902.body" "$ISSUE_BODY"; : > "$GH_LOG"
|
|
286
|
+
echo '{"items":[{"number":902}]}' > "$SEARCH_JSON"
|
|
287
|
+
printf '{"number":902,"state":"open","author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:POSTDEPLOY_ROLLUP: default -->"}\n' > "$T/issue.902.json"
|
|
288
|
+
printf -- '-\tpost-deploy: [bucket:post-deploy] [check:a-smoke] again\t\t\tR7: The Login page renders!\n-\tpost-deploy: [bucket:date] due:2026-12-01 from:2026-10-27 later\t\t\tR8: soak for a week\n' > "$T/l2.tsv"
|
|
289
|
+
OUT=$(bash "$S" --parent 11 --repo o/r --items "$T/l2.tsv"); RC=$?
|
|
290
|
+
check "rollup: lane 2 appends to #902, creates nothing" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG' && grep -q '^issue edit 902' '$GH_LOG'"
|
|
291
|
+
check "rollup: own section for #11, soak item added" "grep -q '^### #11\$' '$ISSUE_BODY.edited' && grep -q 'R8: soak for a week' '$ISSUE_BODY.edited' && grep -q 'FORGE:DEFERRED_FROM: #11' '$ISSUE_BODY.edited'"
|
|
292
|
+
check "rollup: duplicate text from another lane stored once" "[ \$(grep -ci 'login page' '$ISSUE_BODY.edited') -eq 1 ] && printf '%s\n' \"\$OUT\" | grep -q '^R7: The Login page renders!.902'"
|
|
293
|
+
# a chain: parent 20 is itself a follow-up of #10 -> the items go under the root #10, no new issue
|
|
294
|
+
reset
|
|
295
|
+
cp "$T/created.902.body" "$ISSUE_BODY" 2>/dev/null || true
|
|
296
|
+
echo '{"items":[{"number":901}]}' > "$SEARCH_JSON"
|
|
297
|
+
printf '{"number":901,"state":"open","author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:POSTDEPLOY_ROLLUP: default -->"}\n' > "$T/issue.901.json"
|
|
298
|
+
printf '{"number":20,"state":"open","author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}\n' > "$T/issue.20.json"
|
|
299
|
+
printf -- '-\tpost-deploy: [bucket:post-deploy] [check:b-smoke] chain\t\t\tR9: a chained check\n' > "$T/l3.tsv"
|
|
300
|
+
bash "$S" --parent 20 --repo o/r --items "$T/l3.tsv" >/dev/null 2>&1
|
|
301
|
+
check "rollup: a follow-up of a follow-up appends under its root parent, no new issue" "! grep -q '^issue create' '$GH_LOG' && grep -q 'a chained check' '$ISSUE_BODY.edited' && ! grep -q '^### #20' '$ISSUE_BODY.edited'"
|
|
302
|
+
# verify-now: its own per-parent follow-up, never the rollup
|
|
303
|
+
reset
|
|
304
|
+
printf -- '-\tpost-deploy: [bucket:verify-now] on default already\t\t\tR1: check it now\n' > "$T/vn.tsv"
|
|
305
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/vn.tsv")
|
|
306
|
+
check "verify-now: separate per-parent issue, class verify-now, no rollup marker" "grep -q 'FORGE:DEFERRED_CLASS: verify-now' '$T/created.901.body' && ! grep -q POSTDEPLOY_ROLLUP '$T/created.901.body' && printf '%s' \"\$OUT\" | grep -q 'buckets=verify-now:1'"
|
|
307
|
+
# an owner bucket on a post-deploy class row is rerouted to external
|
|
308
|
+
reset
|
|
309
|
+
printf -- 'x/y.sh\towner: [bucket:owner] spend\t\tpost-deploy\n' > "$T/ow.tsv"
|
|
310
|
+
bash "$S" --parent 10 --repo o/r --items "$T/ow.tsv" >/dev/null 2>&1
|
|
311
|
+
check "owner bucket never lands in post-deploy, even when the row says so" "grep -q 'FORGE:DEFERRED_CLASS: external' '$T/created.901.body' && ! grep -q POSTDEPLOY_ROLLUP '$T/created.901.body'"
|
|
312
|
+
|
|
198
313
|
echo "passed=$PASS failed=$FAIL"
|
|
199
314
|
[ "$FAIL" -eq 0 ]
|