forgedock 1.4.0 → 1.5.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/runlog.mjs +11 -1
- package/bin/engine-cli.mjs +6 -0
- package/bin/engine.mjs +194 -59
- package/bin/forgedock.mjs +65 -4
- package/bin/hooks/pre-tool-use.mjs +29 -7
- package/bin/journey.mjs +578 -42
- package/bin/runner.mjs +159 -2
- package/bin/watch-utils.mjs +4 -1
- package/commands/cleanup.md +27 -2
- package/commands/issue.md +14 -4
- package/commands/orchestrate/config.md +3 -1
- package/commands/orchestrate/phase-1-resolve.md +125 -14
- package/commands/orchestrate/phase-3-dependency.md +254 -40
- package/commands/orchestrate/phase-4-execution.md +402 -39
- package/commands/orchestrate/phase-5-cleanup.md +2 -0
- package/commands/review-pr-staging.md +51 -8
- package/commands/review-pr.md +97 -32
- package/commands/work-on/remediate.md +53 -4
- package/package.json +1 -1
- package/packages/protocol/src/validate.js +24 -1
- package/scripts/check-phase-registry-drift.mjs +23 -1
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-lane.sh +29 -3
- package/scripts/derive-finding-milestone.sh +184 -0
- package/scripts/doctor-pipeline-state.sh +1 -0
- package/scripts/extract-affected-files.sh +232 -0
- package/scripts/extract-affected-files.test.sh +331 -0
- package/scripts/forge-run.sh +4 -0
- package/scripts/issue-dedup.sh +69 -8
- package/scripts/issue-dedup.test.sh +142 -0
- package/scripts/severity-to-priority.sh +81 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# derive-finding-milestone.sh — Single source of truth for deriving the
|
|
6
|
+
# milestone a review-finding issue should
|
|
7
|
+
# inherit from the PR that spawned it.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# derive-finding-milestone.sh <PR_NUMBER> [-R <owner/repo>]
|
|
11
|
+
#
|
|
12
|
+
# PR_NUMBER The number of the PR being reviewed (the one /review-pr or
|
|
13
|
+
# /review-pr-staging is creating findings against).
|
|
14
|
+
# -R owner/repo Optional. Defaults to the current repo (gh's own default
|
|
15
|
+
# resolution) when omitted.
|
|
16
|
+
#
|
|
17
|
+
# Resolution (3-tier, first match wins — do NOT add a second copy of this
|
|
18
|
+
# logic anywhere else; both commands/review-pr.md and
|
|
19
|
+
# commands/review-pr-staging.md call this script rather than hand-rolling
|
|
20
|
+
# their own copy, specifically so the two specs cannot independently drift
|
|
21
|
+
# the way they did before this fix — see forge#2447 precedent
|
|
22
|
+
# (scripts/severity-to-priority.sh) and forge#2443 (this script)):
|
|
23
|
+
#
|
|
24
|
+
# Tier 1 — PR's own milestone: `gh pr view <N> --json milestone`. Most
|
|
25
|
+
# direct signal — if the PR itself was filed under a milestone,
|
|
26
|
+
# that is authoritative.
|
|
27
|
+
# Tier 2 — Originating issue's milestone: parse the PR body for
|
|
28
|
+
# `Closes/Fixes/Resolves #N` (case-insensitive), then read that
|
|
29
|
+
# issue's milestone. Covers the common /work-on shape where the
|
|
30
|
+
# PR body says `Closes #1234` but the PR itself was never
|
|
31
|
+
# assigned a milestone directly.
|
|
32
|
+
# Tier 3 — Branch-slug match: if the PR's base or head branch starts with
|
|
33
|
+
# `milestone/`, slugify the branch name (strip the `milestone/`
|
|
34
|
+
# prefix) and look for a GitHub milestone whose slugified title
|
|
35
|
+
# matches — exact match first, substring match as a fallback.
|
|
36
|
+
# This is the path that fixes forge#2443's Instance B/C: a
|
|
37
|
+
# milestone-lane PR whose findings had no `Closes #N` and no
|
|
38
|
+
# milestone set directly, but whose *branch* names the
|
|
39
|
+
# milestone.
|
|
40
|
+
#
|
|
41
|
+
# Output: the resolved milestone TITLE on stdout, or nothing (empty stdout)
|
|
42
|
+
# if no tier resolves — this is a normal, common outcome (most
|
|
43
|
+
# findings are fast-lane, no milestone), not an error.
|
|
44
|
+
#
|
|
45
|
+
# Exit codes:
|
|
46
|
+
# 0 success — milestone title printed to stdout, or nothing if none found
|
|
47
|
+
# 1 error (missing/invalid argument, or `gh` call failed outright —
|
|
48
|
+
# fails loud rather than silently guessing)
|
|
49
|
+
#
|
|
50
|
+
# Example:
|
|
51
|
+
# MILESTONE_TITLE=$(scripts/derive-finding-milestone.sh "$PR_NUMBER")
|
|
52
|
+
# MILESTONE_FLAG=""
|
|
53
|
+
# [ -n "$MILESTONE_TITLE" ] && MILESTONE_FLAG="--milestone $MILESTONE_TITLE"
|
|
54
|
+
#
|
|
55
|
+
# <!-- Added: forge#2443 -->
|
|
56
|
+
|
|
57
|
+
set -euo pipefail
|
|
58
|
+
|
|
59
|
+
PR_NUMBER="${1:-}"
|
|
60
|
+
GH_REPO_ARGS=()
|
|
61
|
+
|
|
62
|
+
if [ -z "$PR_NUMBER" ]; then
|
|
63
|
+
echo "ERROR: Usage: derive-finding-milestone.sh <PR_NUMBER> [-R <owner/repo>]" >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
68
|
+
echo "ERROR: PR_NUMBER must be numeric, got: $PR_NUMBER" >&2
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
shift
|
|
73
|
+
while [ $# -gt 0 ]; do
|
|
74
|
+
case "$1" in
|
|
75
|
+
-R)
|
|
76
|
+
if [ $# -lt 2 ]; then
|
|
77
|
+
echo "ERROR: -R requires a value <owner/repo>" >&2
|
|
78
|
+
echo "Usage: derive-finding-milestone.sh <PR_NUMBER> [-R <owner/repo>]" >&2
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
if ! [[ "$2" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
|
82
|
+
echo "ERROR: -R value must be owner/repo format, got: $2" >&2
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
GH_REPO_ARGS=(-R "$2")
|
|
86
|
+
shift 2
|
|
87
|
+
;;
|
|
88
|
+
*)
|
|
89
|
+
echo "ERROR: unknown argument: $1" >&2
|
|
90
|
+
exit 1
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
# --- Tier 1: PR's own milestone -------------------------------------------
|
|
96
|
+
# stderr is routed to a separate temp file (not merged via 2>&1) so that a
|
|
97
|
+
# 0-exit gh call's stdout is never polluted with incidental stderr text
|
|
98
|
+
# (e.g. a gh CLI update notice) ahead of the jq -r parse below. Mirrors the
|
|
99
|
+
# GH_STDERR_TMP pattern already used in scripts/classify-lane.sh.
|
|
100
|
+
# Restricted to owner-only permissions (chmod 600) since the file transiently
|
|
101
|
+
# holds gh CLI stderr text; and trap-cleaned on EXIT (mirrors the idiom
|
|
102
|
+
# already in production at scripts/doctor-pipeline-state.sh) so a
|
|
103
|
+
# signal-interrupted run (SIGINT/SIGTERM while `gh pr view` is in flight)
|
|
104
|
+
# doesn't leak the temp file — the two explicit rm -f calls below still run
|
|
105
|
+
# on their normal/error paths; the trap is a no-op in those cases and only
|
|
106
|
+
# matters for exit paths those calls don't cover. <!-- Fixes: forge#2532, forge#2533 -->
|
|
107
|
+
GH_STDERR_TMP=$(mktemp)
|
|
108
|
+
chmod 600 "$GH_STDERR_TMP"
|
|
109
|
+
trap 'rm -f "$GH_STDERR_TMP"' EXIT
|
|
110
|
+
PR_JSON=$(gh pr view "$PR_NUMBER" "${GH_REPO_ARGS[@]+"${GH_REPO_ARGS[@]}"}" \
|
|
111
|
+
--json milestone,body,baseRefName,headRefName 2>"$GH_STDERR_TMP") || {
|
|
112
|
+
echo "ERROR: failed to fetch PR #$PR_NUMBER — check PR number and repo flag" >&2
|
|
113
|
+
cat "$GH_STDERR_TMP" >&2
|
|
114
|
+
rm -f "$GH_STDERR_TMP"
|
|
115
|
+
exit 1
|
|
116
|
+
}
|
|
117
|
+
rm -f "$GH_STDERR_TMP"
|
|
118
|
+
|
|
119
|
+
MILESTONE_TITLE=$(echo "$PR_JSON" | jq -r '.milestone.title // empty')
|
|
120
|
+
if [ -n "$MILESTONE_TITLE" ]; then
|
|
121
|
+
echo "$MILESTONE_TITLE"
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# --- Tier 2: originating issue's milestone (Closes/Fixes/Resolves #N) -----
|
|
126
|
+
# Portable (non-PCRE) extraction — grep -oP is not supported by Git Bash's
|
|
127
|
+
# grep build on Windows, so use a bash regex + BASH_REMATCH instead of a
|
|
128
|
+
# lookbehind (same convention as scripts/code-index.sh).
|
|
129
|
+
PR_BODY=$(echo "$PR_JSON" | jq -r '.body // empty')
|
|
130
|
+
SOURCE_ISSUE=""
|
|
131
|
+
PR_BODY_LOWER=$(echo "$PR_BODY" | tr '[:upper:]' '[:lower:]')
|
|
132
|
+
if [[ "$PR_BODY_LOWER" =~ (closes|fixes|resolves)[[:space:]]*#([0-9]+) ]]; then
|
|
133
|
+
SOURCE_ISSUE="${BASH_REMATCH[2]}"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
if [ -n "$SOURCE_ISSUE" ]; then
|
|
137
|
+
ISSUE_MILESTONE=$(gh issue view "$SOURCE_ISSUE" "${GH_REPO_ARGS[@]+"${GH_REPO_ARGS[@]}"}" \
|
|
138
|
+
--json milestone --jq '.milestone.title // empty' 2>/dev/null || echo "")
|
|
139
|
+
if [ -n "$ISSUE_MILESTONE" ]; then
|
|
140
|
+
echo "$ISSUE_MILESTONE"
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# --- Tier 3: branch-slug match ---------------------------------------------
|
|
146
|
+
BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.baseRefName // empty')
|
|
147
|
+
HEAD_BRANCH=$(echo "$PR_JSON" | jq -r '.headRefName // empty')
|
|
148
|
+
|
|
149
|
+
MILESTONE_BRANCH=""
|
|
150
|
+
if echo "$BASE_BRANCH" | grep -qE "^milestone/"; then
|
|
151
|
+
MILESTONE_BRANCH="$BASE_BRANCH"
|
|
152
|
+
elif echo "$HEAD_BRANCH" | grep -qE "^milestone/"; then
|
|
153
|
+
MILESTONE_BRANCH="$HEAD_BRANCH"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if [ -n "$MILESTONE_BRANCH" ]; then
|
|
157
|
+
BRANCH_SLUG=$(echo "$MILESTONE_BRANCH" | sed 's|^milestone/||')
|
|
158
|
+
|
|
159
|
+
# Resolve owner/repo for the milestones API call: use -R's value if given,
|
|
160
|
+
# otherwise gh's own default-repo resolution (the `:owner/:repo` REST
|
|
161
|
+
# placeholder gh substitutes from the current git remote / repo context).
|
|
162
|
+
if [ ${#GH_REPO_ARGS[@]} -gt 0 ]; then
|
|
163
|
+
MILESTONES_REPO="${GH_REPO_ARGS[1]}"
|
|
164
|
+
else
|
|
165
|
+
MILESTONES_REPO=":owner/:repo"
|
|
166
|
+
fi
|
|
167
|
+
ALL_MILESTONES=$(gh api "repos/${MILESTONES_REPO}/milestones" 2>/dev/null || echo "")
|
|
168
|
+
|
|
169
|
+
if [ -n "$ALL_MILESTONES" ] && [ "$ALL_MILESTONES" != "[]" ]; then
|
|
170
|
+
MATCH=$(echo "$ALL_MILESTONES" | jq -r --arg slug "$BRANCH_SLUG" \
|
|
171
|
+
'.[] | select((.title | ascii_downcase | gsub("[^a-z0-9]+"; "-")) == $slug) | .title' | head -1)
|
|
172
|
+
if [ -z "$MATCH" ]; then
|
|
173
|
+
MATCH=$(echo "$ALL_MILESTONES" | jq -r --arg slug "$BRANCH_SLUG" \
|
|
174
|
+
'.[] | select((.title | ascii_downcase | gsub("[^a-z0-9]+"; "-")) | test($slug)) | .title' | head -1)
|
|
175
|
+
fi
|
|
176
|
+
if [ -n "$MATCH" ]; then
|
|
177
|
+
echo "$MATCH"
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# No tier resolved — normal outcome, empty stdout, success exit.
|
|
184
|
+
exit 0
|
|
@@ -119,6 +119,7 @@ GH_FLAG="-R $GH_REPO"
|
|
|
119
119
|
# the diagnostic stderr text for the WARNING message on actual failure.
|
|
120
120
|
# ---------------------------------------------------------------------------
|
|
121
121
|
GH_STDERR_TMP=$(mktemp)
|
|
122
|
+
chmod 600 "$GH_STDERR_TMP"
|
|
122
123
|
trap 'rm -f "$GH_STDERR_TMP"' EXIT
|
|
123
124
|
|
|
124
125
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# extract-affected-files.sh — Positionally-scoped affected-file extraction for
|
|
3
|
+
# /orchestrate Phase 3C Layer 1 (see commands/orchestrate/phase-3-dependency.md)
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# extract-affected-files.sh <issue_number> -R <owner/repo>
|
|
7
|
+
# extract-affected-files.sh <issue_number> "-R <owner/repo>" (single pre-joined token also accepted)
|
|
8
|
+
#
|
|
9
|
+
# Output (stdout):
|
|
10
|
+
# Line 1: PROVENANCE=affected-files-section | body-fallback | none | error
|
|
11
|
+
# Line 2+: one extracted file path per line (zero lines when PROVENANCE=none or error)
|
|
12
|
+
#
|
|
13
|
+
# Exit codes:
|
|
14
|
+
# 0 — Extraction completed (including the zero-files/PROVENANCE=none case — that is
|
|
15
|
+
# a valid, expected outcome, NOT an error; callers must not treat exit 0 as proof
|
|
16
|
+
# that files were found).
|
|
17
|
+
# 2 — Usage error (missing issue number, malformed -R value).
|
|
18
|
+
#
|
|
19
|
+
# Extraction rules (forge#2436):
|
|
20
|
+
# 1. Primary path — the issue's FORGE:INVESTIGATOR comment (if one exists), scoped to
|
|
21
|
+
# ONLY its own "### Affected Files" section. Capture stops at the next markdown
|
|
22
|
+
# heading of any level, so paths mentioned in "### Evidence", "### Root Cause",
|
|
23
|
+
# "### Related Issues", etc. are never collected.
|
|
24
|
+
# -> PROVENANCE=affected-files-section
|
|
25
|
+
# 2. Fallback path — used ONLY when no FORGE:INVESTIGATOR comment exists at all.
|
|
26
|
+
# Scoped to a deliverables-shaped heading in the raw issue body:
|
|
27
|
+
# "## Affected Files", "## Deliverables", or "### Files to change". Capture stops
|
|
28
|
+
# at the next markdown heading of any level, so "## Context", "## Prior art",
|
|
29
|
+
# "## Related", "## Root Cause", etc. are never scanned.
|
|
30
|
+
# -> PROVENANCE=body-fallback
|
|
31
|
+
# 3. If neither path yields a scoped section containing a recognized file path:
|
|
32
|
+
# PROVENANCE=none, zero files. This is intentional — phase-3-dependency.md's
|
|
33
|
+
# Layer 4 conservative-serialization fallback fires when file extraction yields
|
|
34
|
+
# fewer than 2 paths, and a confident-but-wrong list (the pre-fix behavior of
|
|
35
|
+
# scraping the whole body/comment) defeated that safety net. Yielding nothing
|
|
36
|
+
# when there is nothing to justify is strictly safer than yielding something
|
|
37
|
+
# wrong (forge#2436).
|
|
38
|
+
# 4. PROVENANCE=error — distinct from `none` (forge#2504): emitted when a `gh`
|
|
39
|
+
# invocation itself fails (non-zero exit — network error, auth expiry, rate
|
|
40
|
+
# limit) AND no file path was recovered from any path (including a fallback
|
|
41
|
+
# attempted after a failed primary call). `none` means "we confirmed there is
|
|
42
|
+
# nothing to extract"; `error` means "we could not confirm that — a `gh` call
|
|
43
|
+
# failed, so this result is inconclusive, not a verified empty result." A `gh`
|
|
44
|
+
# failure that is followed by a *successful* fallback yielding real files still
|
|
45
|
+
# reports the normal `body-fallback` provenance — `error` only fires when the
|
|
46
|
+
# failure actually cost us data.
|
|
47
|
+
#
|
|
48
|
+
# Extension regex covers: py|tsx?|jsx?|sql|json|ya?ml|mjs|js|sh|md — the repo's
|
|
49
|
+
# dominant file types. mjs/js/sh/md were previously missing, which meant the
|
|
50
|
+
# "primary" INVESTIGATOR-comment path silently extracted nothing for most
|
|
51
|
+
# ForgeDock issues (a .mjs/.md-heavy repo), so the unscoped body fallback was
|
|
52
|
+
# the path that actually determined DAG edges in practice (forge#2436).
|
|
53
|
+
#
|
|
54
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
55
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
56
|
+
|
|
57
|
+
set -euo pipefail
|
|
58
|
+
|
|
59
|
+
EXT_REGEX='`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml|mjs|js|sh|md)`'
|
|
60
|
+
|
|
61
|
+
# --------------------------------------------------------------------------- #
|
|
62
|
+
# Argument parsing — mirrors scripts/issue-dedup.sh's -R / -R\ * case arms
|
|
63
|
+
# exactly (forge#1533, forge#1563: trailing/malformed -R values must be a
|
|
64
|
+
# usage error, never silently misread by callers as "extraction found
|
|
65
|
+
# nothing").
|
|
66
|
+
# --------------------------------------------------------------------------- #
|
|
67
|
+
NUM=""
|
|
68
|
+
REPO=""
|
|
69
|
+
|
|
70
|
+
while [[ $# -gt 0 ]]; do
|
|
71
|
+
case "$1" in
|
|
72
|
+
"")
|
|
73
|
+
# Empty-string token — e.g. a caller quoting an unset/empty $GH_FLAG.
|
|
74
|
+
# Treat as "no repo flag supplied", not as the issue number.
|
|
75
|
+
shift
|
|
76
|
+
;;
|
|
77
|
+
-R)
|
|
78
|
+
shift
|
|
79
|
+
if [[ $# -eq 0 ]]; then
|
|
80
|
+
echo "Missing value for -R" >&2
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
83
|
+
REPO="$1"
|
|
84
|
+
shift
|
|
85
|
+
;;
|
|
86
|
+
-R\ *)
|
|
87
|
+
# Single pre-joined token, e.g. "-R owner/repo" — produced when a caller
|
|
88
|
+
# quotes an already-composed $GH_FLAG variable ("$GH_FLAG") instead of
|
|
89
|
+
# passing -R and the repo as two separate argv tokens.
|
|
90
|
+
#
|
|
91
|
+
# Guard: a valid repo token always contains '/' (owner/repo format).
|
|
92
|
+
# If the value after "-R " has no '/', treat the whole token as
|
|
93
|
+
# malformed input rather than guessing (this script takes no other
|
|
94
|
+
# positional string argument that could plausibly start with "-R ").
|
|
95
|
+
_rval="${1#-R }"
|
|
96
|
+
if [[ "$_rval" == */* ]]; then
|
|
97
|
+
REPO="$_rval"
|
|
98
|
+
shift
|
|
99
|
+
else
|
|
100
|
+
echo "Malformed -R value: $1" >&2
|
|
101
|
+
exit 2
|
|
102
|
+
fi
|
|
103
|
+
;;
|
|
104
|
+
-*)
|
|
105
|
+
echo "Unknown flag: $1" >&2
|
|
106
|
+
exit 2
|
|
107
|
+
;;
|
|
108
|
+
*)
|
|
109
|
+
NUM="$1"
|
|
110
|
+
shift
|
|
111
|
+
;;
|
|
112
|
+
esac
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
if [[ -z "$NUM" ]]; then
|
|
116
|
+
echo "Usage: extract-affected-files.sh <issue_number> -R <owner/repo>" >&2
|
|
117
|
+
exit 2
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# --------------------------------------------------------------------------- #
|
|
121
|
+
# Scoped section extraction — turn capture on at a target heading, off at the
|
|
122
|
+
# next markdown heading of any level. Same sentinel-based awk pattern already
|
|
123
|
+
# used elsewhere in this command family (see phase-3-dependency.md's sibling
|
|
124
|
+
# command work-on/build.md Phase 3C.5: `awk '/^### Deliverables/{p=1; next}
|
|
125
|
+
# /^### /{p=0} p'`), generalized here to stop at ANY heading level so a
|
|
126
|
+
# deeper sub-heading inside a deliverables section still closes capture
|
|
127
|
+
# rather than leaking into unrelated prose.
|
|
128
|
+
# --------------------------------------------------------------------------- #
|
|
129
|
+
extract_investigator_section() {
|
|
130
|
+
awk '
|
|
131
|
+
/^### Affected Files/ { p=1; next }
|
|
132
|
+
/^#/ { p=0 }
|
|
133
|
+
p { print }
|
|
134
|
+
' <<< "$1"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
extract_body_fallback_section() {
|
|
138
|
+
awk '
|
|
139
|
+
/^## Affected Files/ || /^## Deliverables/ || /^### Files to change/ { p=1; next }
|
|
140
|
+
/^#/ { p=0 }
|
|
141
|
+
p { print }
|
|
142
|
+
' <<< "$1"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
extract_paths() {
|
|
146
|
+
# -E (POSIX extended regex), not -P (PCRE): the pattern below needs only
|
|
147
|
+
# alternation and `?` grouping, both supported by -E, and -P depends on a
|
|
148
|
+
# UTF-8 locale being active (`grep: -P supports only unibyte and UTF-8
|
|
149
|
+
# locales` otherwise) — a portability trap the original inline pseudocode
|
|
150
|
+
# in phase-3-dependency.md carried (forge#2436) and that this script fixes
|
|
151
|
+
# by not needing PCRE at all.
|
|
152
|
+
grep -oE "$EXT_REGEX" <<< "$1" 2>/dev/null | tr -d '`' | sort -u || true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# --------------------------------------------------------------------------- #
|
|
156
|
+
# Primary path: FORGE:INVESTIGATOR comment, scoped to its own Affected Files section
|
|
157
|
+
# --------------------------------------------------------------------------- #
|
|
158
|
+
PROVENANCE="none"
|
|
159
|
+
FILES=""
|
|
160
|
+
GH_CALL_FAILED=0
|
|
161
|
+
|
|
162
|
+
# NOTE: deliberately no `| tail -1` here — `gh api --jq`'s raw-string output
|
|
163
|
+
# for a multi-line `.body` field embeds literal newlines, so isolating "the
|
|
164
|
+
# last comment" by taking the last output LINE would instead truncate to
|
|
165
|
+
# only the last line of the (possibly only) comment's body, breaking the
|
|
166
|
+
# heading-scoped awk extraction below. In practice Phase 1 of the pipeline
|
|
167
|
+
# deletes any partial FORGE:INVESTIGATOR comment before reposting (see
|
|
168
|
+
# work-on/investigate.md), so at most one such comment exists per issue at
|
|
169
|
+
# any time — matching the original Layer 1 pseudocode, which also consumed
|
|
170
|
+
# this stream directly with no last-comment isolation.
|
|
171
|
+
#
|
|
172
|
+
# forge#2504: `VAR=$(cmd 2>/dev/null || true)` discarded gh's exit code
|
|
173
|
+
# entirely, so a genuine `gh` failure (network error, auth expiry, rate
|
|
174
|
+
# limit) was indistinguishable from "gh succeeded, output legitimately
|
|
175
|
+
# empty." Use `if ! VAR=$(cmd); then ... fi` instead — exempt from
|
|
176
|
+
# `set -e` because it's an `if` condition, so no `|| true` is needed — and
|
|
177
|
+
# track the failure in GH_CALL_FAILED so it can be reflected in PROVENANCE
|
|
178
|
+
# below rather than silently defaulting to "none". Same idiom already
|
|
179
|
+
# established in scripts/transition-label.sh (forge#1991/PR #1996), adapted
|
|
180
|
+
# here because this script must always complete (exit 0) rather than abort,
|
|
181
|
+
# so failures are surfaced via a new PROVENANCE=error value instead of a
|
|
182
|
+
# non-zero exit.
|
|
183
|
+
if ! INVESTIGATOR_BODY=$(gh api "repos/${REPO}/issues/${NUM}/comments" \
|
|
184
|
+
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body' 2>/dev/null); then
|
|
185
|
+
GH_CALL_FAILED=1
|
|
186
|
+
echo "WARNING: gh api call failed while fetching comments for issue #$NUM (repo: $REPO) — cannot confirm whether a FORGE:INVESTIGATOR comment exists; treating as inconclusive, not empty" >&2
|
|
187
|
+
INVESTIGATOR_BODY=""
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
if [[ -n "$INVESTIGATOR_BODY" ]]; then
|
|
191
|
+
SCOPED=$(extract_investigator_section "$INVESTIGATOR_BODY")
|
|
192
|
+
FILES=$(extract_paths "$SCOPED")
|
|
193
|
+
if [[ -n "$FILES" ]]; then
|
|
194
|
+
PROVENANCE="affected-files-section"
|
|
195
|
+
fi
|
|
196
|
+
else
|
|
197
|
+
# --------------------------------------------------------------------------- #
|
|
198
|
+
# Fallback path: raw issue body, scoped to a deliverables-shaped heading.
|
|
199
|
+
# Reached both when NO FORGE:INVESTIGATOR comment exists (the original Layer 1
|
|
200
|
+
# contract: "For issues WITHOUT an investigation comment, fall back to parsing
|
|
201
|
+
# the issue body") AND when the primary gh call above failed (in which case we
|
|
202
|
+
# genuinely don't know if a comment exists, so attempting the fallback gives a
|
|
203
|
+
# chance at real data rather than giving up immediately).
|
|
204
|
+
# --------------------------------------------------------------------------- #
|
|
205
|
+
if ! ISSUE_BODY=$(gh issue view "$NUM" -R "$REPO" --json body --jq '.body' 2>/dev/null); then
|
|
206
|
+
GH_CALL_FAILED=1
|
|
207
|
+
echo "WARNING: gh issue view call failed for issue #$NUM (repo: $REPO) — cannot confirm the issue body's Affected Files section; treating as inconclusive, not empty" >&2
|
|
208
|
+
ISSUE_BODY=""
|
|
209
|
+
fi
|
|
210
|
+
if [[ -n "$ISSUE_BODY" ]]; then
|
|
211
|
+
SCOPED=$(extract_body_fallback_section "$ISSUE_BODY")
|
|
212
|
+
FILES=$(extract_paths "$SCOPED")
|
|
213
|
+
if [[ -n "$FILES" ]]; then
|
|
214
|
+
PROVENANCE="body-fallback"
|
|
215
|
+
fi
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
# forge#2504: only escalate to "error" when neither path yielded real data AND
|
|
220
|
+
# at least one gh call genuinely failed along the way. A failed primary call
|
|
221
|
+
# followed by a successful fallback that found files must still report
|
|
222
|
+
# body-fallback (real data recovered) — never overwrite that with "error".
|
|
223
|
+
if [[ "$PROVENANCE" == "none" && "$GH_CALL_FAILED" -eq 1 ]]; then
|
|
224
|
+
PROVENANCE="error"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
echo "PROVENANCE=$PROVENANCE"
|
|
228
|
+
if [[ -n "$FILES" ]]; then
|
|
229
|
+
printf '%s\n' "$FILES"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
exit 0
|