forgedock 1.1.6 → 1.1.8
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 +3 -1
- package/bin/engine/invariants.mjs +361 -0
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/journey.mjs +77 -7
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +540 -0
- package/commands/autopilot.md +959 -78
- package/commands/cleanup.md +52 -3
- package/commands/optimize.md +585 -0
- package/commands/orchestrate/phase-1-resolve.md +35 -25
- package/commands/orchestrate/phase-3-dependency.md +465 -34
- package/commands/orchestrate/phase-4-execution.md +448 -29
- package/commands/orchestrate/phase-6-report.md +198 -9
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/quality-gate.md +417 -0
- package/commands/review-pr.md +391 -13
- package/commands/spec-doctor.md +429 -0
- package/commands/validate.md +52 -0
- package/commands/work-on/build/architect.md +77 -0
- package/commands/work-on/build/context.md +357 -22
- package/commands/work-on/build/implement.md +2 -0
- package/commands/work-on/build/validate.md +28 -0
- package/commands/work-on/build.md +44 -0
- package/commands/work-on/close.md +621 -8
- package/commands/work-on/investigate.md +221 -0
- package/commands/work-on.md +15 -14
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/design/awesome-list-submissions.md +105 -0
- package/docs/design/f5bot-monitoring-setup.md +43 -0
- package/docs/design/growth-strategy-2026.md +579 -0
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +2 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/danger-zones.mjs +782 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/refresh-bot-token.sh +97 -0
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +157 -6
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
# are silently disabled and warrants investigating the underlying `gh` failure.
|
|
30
30
|
#
|
|
31
31
|
# Exit codes:
|
|
32
|
-
# 0 = no findings (pipeline healthy)
|
|
32
|
+
# 0 = no findings (pipeline healthy — all checks ran and passed)
|
|
33
33
|
# 1 = findings present
|
|
34
34
|
# 2 = error (missing deps, bad args)
|
|
35
|
+
# 3 = inconclusive — checks were skipped due to gh API failures (total=0 but checks_skipped>0);
|
|
36
|
+
# pipeline health cannot be confirmed; treat as UNKNOWN, not healthy
|
|
35
37
|
#
|
|
36
38
|
# Depends on: gh (GitHub CLI), jq, date
|
|
37
39
|
#
|
|
@@ -43,6 +45,8 @@
|
|
|
43
45
|
# I5 Issues in workflow:in-review with no open PR
|
|
44
46
|
# I6 PRs open without a FORGE:CONTRACT or FORGE:BUILDER annotation
|
|
45
47
|
# I7 Orphaned worktree branches (local branches fix/* with no open PR)
|
|
48
|
+
# I8 FORGE-referenced gists with public visibility (KNOWLEDGE_GIST, MILESTONE_INDEX,
|
|
49
|
+
# PRIOR_GIST annotations in recent issue comments)
|
|
46
50
|
#
|
|
47
51
|
# /pipeline-health should consume this script's JSON output rather than
|
|
48
52
|
# re-discovering state from scratch.
|
|
@@ -401,6 +405,79 @@ while IFS= read -r row; do
|
|
|
401
405
|
fi
|
|
402
406
|
done < <(echo "$OPEN_PRS" | jq -c '.[]')
|
|
403
407
|
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# I8: FORGE-referenced gists with public visibility
|
|
410
|
+
#
|
|
411
|
+
# Scan the last 100 issues (open + closed) for comments containing
|
|
412
|
+
# FORGE:KNOWLEDGE_GIST, FORGE:MILESTONE_INDEX, or FORGE:PRIOR_GIST annotations.
|
|
413
|
+
# Extract the gist ID from each URL and query gh api gists/{id} --jq .public.
|
|
414
|
+
# A "true" result means a pipeline memory gist is world-readable — CRITICAL finding.
|
|
415
|
+
#
|
|
416
|
+
# Fail-open on gh API errors: a network blip or rate-limit must NOT be reported
|
|
417
|
+
# as a confirmed public gist. Uses add_skip (same pattern as I4/I5) so persistent
|
|
418
|
+
# API degradation surfaces in --json degraded[] without inflating CRITICAL count.
|
|
419
|
+
# ---------------------------------------------------------------------------
|
|
420
|
+
RECENT_ISSUES=$(gh issue list $GH_FLAG \
|
|
421
|
+
--state all \
|
|
422
|
+
--limit 100 \
|
|
423
|
+
--json number \
|
|
424
|
+
--jq '.[].number' 2>"$GH_STDERR_TMP" || true)
|
|
425
|
+
|
|
426
|
+
I8_FETCH_ERR=$(cat "$GH_STDERR_TMP")
|
|
427
|
+
if [ -z "$RECENT_ISSUES" ] && [ -n "$I8_FETCH_ERR" ]; then
|
|
428
|
+
echo "WARNING: gh issue list failed for I8 gist-visibility scan — check inconclusive, skipping: $I8_FETCH_ERR" >&2
|
|
429
|
+
add_skip "I8" "all" "gh issue list failed: $(bounded_reason "$I8_FETCH_ERR")"
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
for issue_num in $RECENT_ISSUES; do
|
|
433
|
+
# Fetch comments and extract FORGE gist annotation URLs
|
|
434
|
+
if ! ISSUE_COMMENTS=$(gh issue view "$issue_num" $GH_FLAG \
|
|
435
|
+
--json comments \
|
|
436
|
+
--jq '[.comments[].body] | join("\n")' 2>"$GH_STDERR_TMP"); then
|
|
437
|
+
echo "WARNING: gh issue view failed for #$issue_num (I8) — skipping: $(cat "$GH_STDERR_TMP")" >&2
|
|
438
|
+
add_skip "I8" "$issue_num" "gh issue view failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
|
|
439
|
+
continue
|
|
440
|
+
fi
|
|
441
|
+
|
|
442
|
+
# Extract gist URLs from all three FORGE annotation types.
|
|
443
|
+
# The character class [^\s >]+ matches any non-whitespace, non-space, non-> char,
|
|
444
|
+
# which covers standard usernames, bot accounts (e.g. rapiercraft-forge[bot]),
|
|
445
|
+
# and gist hashes — all valid in a gist URL before the closing ' -->'.
|
|
446
|
+
GIST_URLS=$(echo "$ISSUE_COMMENTS" \
|
|
447
|
+
| grep -oE '<!-- FORGE:(KNOWLEDGE_GIST|PRIOR_GIST): https://gist\.github\.com/[^ >]+ -->' \
|
|
448
|
+
| grep -oE 'https://gist\.github\.com/[^ >]+' || true)
|
|
449
|
+
# FORGE:MILESTONE_INDEX may appear in milestone description, not comments, but
|
|
450
|
+
# some pipelines also post it as a comment — include it in the scan.
|
|
451
|
+
MILESTONE_GIST_URLS=$(echo "$ISSUE_COMMENTS" \
|
|
452
|
+
| grep -oE '<!-- FORGE:MILESTONE_INDEX: https://gist\.github\.com/[^ >]+ -->' \
|
|
453
|
+
| grep -oE 'https://gist\.github\.com/[^ >]+' || true)
|
|
454
|
+
ALL_GIST_URLS=$(printf '%s\n%s\n' "$GIST_URLS" "$MILESTONE_GIST_URLS" | grep -v '^$' | sort -u || true)
|
|
455
|
+
|
|
456
|
+
for gist_url in $ALL_GIST_URLS; do
|
|
457
|
+
# Extract gist ID: last path component after the username segment
|
|
458
|
+
gist_id=$(echo "$gist_url" | sed 's|.*/||')
|
|
459
|
+
if [ -z "$gist_id" ]; then
|
|
460
|
+
continue
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
# Query gist visibility — fail-open on API error (rate limit, auth, network)
|
|
464
|
+
if ! IS_PUBLIC=$(gh api "gists/${gist_id}" --jq '.public' 2>"$GH_STDERR_TMP"); then
|
|
465
|
+
echo "WARNING: gh api gists/${gist_id} failed (I8) — skipping: $(cat "$GH_STDERR_TMP")" >&2
|
|
466
|
+
add_skip "I8" "$issue_num" "gh api gists/${gist_id} failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
|
|
467
|
+
continue
|
|
468
|
+
fi
|
|
469
|
+
|
|
470
|
+
if [ "$IS_PUBLIC" = "true" ]; then
|
|
471
|
+
add_finding \
|
|
472
|
+
"public_forge_gist" "critical" \
|
|
473
|
+
"$issue_num" "gist_visibility" "0" \
|
|
474
|
+
"FORGE:KNOWLEDGE_GIST/FORGE:MILESTONE_INDEX/FORGE:PRIOR_GIST" \
|
|
475
|
+
"gh api gists/${gist_id} --jq '.public'" \
|
|
476
|
+
"Issue #${issue_num} references a public FORGE gist: ${gist_url} — gist ID: ${gist_id}. Pipeline memory gists (FORGE:KNOWLEDGE_GIST, FORGE:MILESTONE_INDEX, FORGE:PRIOR_GIST) MUST be secret. Public gists expose root causes, file paths, and security findings to the open internet. Invariant I8 broken. Migration: gh gist view ${gist_id} > /tmp/gist_migrate.md && gh gist delete ${gist_id} --yes && gh gist create /tmp/gist_migrate.md --desc '<original description>' (no --public). Then update the annotation URL on issue #${issue_num}."
|
|
477
|
+
fi
|
|
478
|
+
done
|
|
479
|
+
done
|
|
480
|
+
|
|
404
481
|
# ---------------------------------------------------------------------------
|
|
405
482
|
# Summary
|
|
406
483
|
# ---------------------------------------------------------------------------
|
|
@@ -429,6 +506,12 @@ if $JSON_MODE; then
|
|
|
429
506
|
--argjson findings "$FINDINGS_JSON" \
|
|
430
507
|
--argjson summary "$SUMMARY_JSON" \
|
|
431
508
|
'{findings:$findings,summary:$summary}'
|
|
509
|
+
# WIRE:PROVEN — reachable when gh API fails during a doctor run with no prior findings:
|
|
510
|
+
# all I1/I2/I3/I6 checks fail and add_skip() is called; CHECKS_SKIPPED > 0; TOTAL stays 0.
|
|
511
|
+
# Verified by the scenario in #1590: total gh outage → all list checks skipped → previously exit 0.
|
|
512
|
+
if [ "$TOTAL" -eq 0 ] && [ "$CHECKS_SKIPPED" -gt 0 ]; then
|
|
513
|
+
exit 3 # INCONCLUSIVE — checks skipped due to gh failures; cannot confirm healthy
|
|
514
|
+
fi
|
|
432
515
|
[ "$TOTAL" -eq 0 ] && exit 0 || exit 1
|
|
433
516
|
fi
|
|
434
517
|
|
|
@@ -443,11 +526,14 @@ if [ "$CHECKS_SKIPPED" -gt 0 ]; then
|
|
|
443
526
|
fi
|
|
444
527
|
|
|
445
528
|
if [ "$TOTAL" -eq 0 ]; then
|
|
529
|
+
# WIRE:PROVEN — same trigger as JSON mode branch above: TOTAL=0 and CHECKS_SKIPPED>0.
|
|
530
|
+
# This branch executes when the outer 'if [ "$TOTAL" -eq 0 ]' block is entered
|
|
531
|
+
# and CHECKS_SKIPPED > 0, i.e., at least one gh API call failed during the run.
|
|
446
532
|
if [ "$CHECKS_SKIPPED" -gt 0 ]; then
|
|
447
533
|
echo " No stalls detected in checks that ran, but $CHECKS_SKIPPED check(s) were skipped"
|
|
448
534
|
echo " due to gh API failures — pipeline health is INCONCLUSIVE, not confirmed healthy."
|
|
449
535
|
echo ""
|
|
450
|
-
exit
|
|
536
|
+
exit 3 # INCONCLUSIVE — cannot confirm healthy when checks were skipped
|
|
451
537
|
fi
|
|
452
538
|
echo " No stalls detected. Pipeline is healthy."
|
|
453
539
|
echo ""
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# scripts/refresh-bot-token.sh — refresh the rapiercraft-forgedock[bot] installation token.
|
|
6
|
+
#
|
|
7
|
+
# Generates a JWT from the app's private key, exchanges it for a 1-hour
|
|
8
|
+
# installation token, and re-auths the gh CLI. Idempotent — safe to run
|
|
9
|
+
# on every session start or from cron.
|
|
10
|
+
#
|
|
11
|
+
# Supports two installations:
|
|
12
|
+
# RapierCraftStudios (org) — installation 144998831 (default)
|
|
13
|
+
# RapierCraft (personal) — installation 140233364
|
|
14
|
+
#
|
|
15
|
+
# Usage: scripts/refresh-bot-token.sh [--pem /path/to/key.pem] [--personal]
|
|
16
|
+
#
|
|
17
|
+
# Env: FORGEDOCK_APP_PEM — path to private key (default: secrets/rapiercraft-forgedock.pem)
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
FORGE_HOME="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
23
|
+
|
|
24
|
+
APP_ID="4051319"
|
|
25
|
+
ORG_INSTALLATION_ID="144998831"
|
|
26
|
+
PERSONAL_INSTALLATION_ID="140233364"
|
|
27
|
+
DEFAULT_PEM="$FORGE_HOME/secrets/rapiercraft-forgedock.pem"
|
|
28
|
+
|
|
29
|
+
# --- Parse args ---
|
|
30
|
+
PEM_PATH="${FORGEDOCK_APP_PEM:-$DEFAULT_PEM}"
|
|
31
|
+
INSTALLATION_ID="$ORG_INSTALLATION_ID"
|
|
32
|
+
while [[ $# -gt 0 ]]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
--pem) PEM_PATH="$2"; shift 2 ;;
|
|
35
|
+
--personal) INSTALLATION_ID="$PERSONAL_INSTALLATION_ID"; shift ;;
|
|
36
|
+
*) echo "Unknown arg: $1" >&2; exit 1 ;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
if [[ ! -f "$PEM_PATH" ]]; then
|
|
41
|
+
echo "ERROR: Private key not found at $PEM_PATH" >&2
|
|
42
|
+
echo " Set FORGEDOCK_APP_PEM or pass --pem /path/to/key.pem" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# --- Generate JWT and exchange for installation token ---
|
|
47
|
+
INSTALL_TOKEN=$(node -e "
|
|
48
|
+
var crypto = require('crypto');
|
|
49
|
+
var fs = require('fs');
|
|
50
|
+
var https = require('https');
|
|
51
|
+
var pem = fs.readFileSync(process.argv[1], 'utf8');
|
|
52
|
+
var now = Math.floor(Date.now() / 1000);
|
|
53
|
+
var h = Buffer.from(JSON.stringify({alg:'RS256',typ:'JWT'})).toString('base64url');
|
|
54
|
+
var p = Buffer.from(JSON.stringify({iat:now-60,exp:now+(9*60),iss:process.argv[2]})).toString('base64url');
|
|
55
|
+
var s = crypto.createSign('RSA-SHA256');
|
|
56
|
+
s.update(h+'.'+p);
|
|
57
|
+
var jwt = h+'.'+p+'.'+s.sign(pem,'base64url');
|
|
58
|
+
var opts = {
|
|
59
|
+
hostname: 'api.github.com',
|
|
60
|
+
path: '/app/installations/'+process.argv[3]+'/access_tokens',
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'Authorization': 'Bearer '+jwt,
|
|
64
|
+
'Accept': 'application/vnd.github+json',
|
|
65
|
+
'User-Agent': 'forgedock-refresh'
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var req = https.request(opts, function(res) {
|
|
69
|
+
var body = '';
|
|
70
|
+
res.on('data', function(c) { body += c; });
|
|
71
|
+
res.on('end', function() {
|
|
72
|
+
var t = JSON.parse(body).token;
|
|
73
|
+
if (!t) { process.stderr.write('API error: '+body); process.exit(1); }
|
|
74
|
+
process.stdout.write(t);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
req.end();
|
|
78
|
+
" "$PEM_PATH" "$APP_ID" "$INSTALLATION_ID")
|
|
79
|
+
|
|
80
|
+
if [[ -z "$INSTALL_TOKEN" ]]; then
|
|
81
|
+
echo "ERROR: Failed to obtain installation token" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# --- Auth gh CLI ---
|
|
86
|
+
echo "$INSTALL_TOKEN" | gh auth login --with-token 2>/dev/null
|
|
87
|
+
|
|
88
|
+
# --- Clean up stale old bot if present ---
|
|
89
|
+
if gh auth status 2>&1 | grep -q "rapiercraft-forge\[bot\]"; then
|
|
90
|
+
gh auth logout -h github.com -u "rapiercraft-forge[bot]" 2>/dev/null || true
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [[ "$INSTALLATION_ID" == "$ORG_INSTALLATION_ID" ]]; then
|
|
94
|
+
echo "rapiercraft-forgedock[bot] token refreshed — org (RapierCraftStudios) ~1h"
|
|
95
|
+
else
|
|
96
|
+
echo "rapiercraft-forgedock[bot] token refreshed — personal (RapierCraft) ~1h"
|
|
97
|
+
fi
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# select-fix-targets.sh — Deterministic priority-ranked issue selection for autopilot
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# select-fix-targets.sh [-R <owner/repo>] [--limit <n>] [--fixture-test]
|
|
6
|
+
#
|
|
7
|
+
# -R <owner/repo> : GitHub repository (optional, defaults to current repo)
|
|
8
|
+
# --limit <n> : Maximum number of issues to output (optional, default: 10)
|
|
9
|
+
# --fixture-test : Run against synthetic fixture data instead of GitHub API.
|
|
10
|
+
# Exits 0 if output matches expected ranking, 1 otherwise.
|
|
11
|
+
# Use this for wire-through proof per #1731.
|
|
12
|
+
#
|
|
13
|
+
# Output:
|
|
14
|
+
# One issue number per line, in priority order (highest priority first).
|
|
15
|
+
# Suitable for: for ISSUE_NUM in $(select-fix-targets.sh); do ... done
|
|
16
|
+
#
|
|
17
|
+
# Ranking algorithm (deterministic — no LLM required):
|
|
18
|
+
# 1. P0 issues first (priority:P0 label)
|
|
19
|
+
# 2. P1 issues second (priority:P1 label)
|
|
20
|
+
# 3. P2 issues third (priority:P2 label)
|
|
21
|
+
# 4. Unlabeled issues (no priority:P* label) are EXCLUDED — never selected autonomously
|
|
22
|
+
#
|
|
23
|
+
# Tie-breaker within each priority band: oldest createdAt wins.
|
|
24
|
+
# This ensures fresh self-filed findings (from /autopilot recon) never outrank
|
|
25
|
+
# older human-filed issues at the same priority level. See: #1752.
|
|
26
|
+
#
|
|
27
|
+
# Exclusions (issues never selected, regardless of priority):
|
|
28
|
+
# - Issues with needs-human label
|
|
29
|
+
# - Issues with a milestone (feature-lane issues — autopilot handles these in Phase 3)
|
|
30
|
+
# - Issues with terminal workflow labels (workflow:merged, workflow:invalid, workflow:decomposed)
|
|
31
|
+
# - Issues with active workflow labels (workflow:building, workflow:in-review) — already in flight
|
|
32
|
+
#
|
|
33
|
+
# Exit codes: 0 = success (or fixture test passed), 1 = error (or fixture test failed)
|
|
34
|
+
#
|
|
35
|
+
# Added: forge#1752
|
|
36
|
+
|
|
37
|
+
set -euo pipefail
|
|
38
|
+
|
|
39
|
+
GH_REPO_ARGS=()
|
|
40
|
+
LIMIT=10
|
|
41
|
+
FIXTURE_TEST=false
|
|
42
|
+
|
|
43
|
+
# Parse arguments
|
|
44
|
+
while [ $# -gt 0 ]; do
|
|
45
|
+
case "$1" in
|
|
46
|
+
-R)
|
|
47
|
+
if [ $# -lt 2 ]; then
|
|
48
|
+
echo "ERROR: -R requires a value <owner/repo>" >&2
|
|
49
|
+
echo "Usage: select-fix-targets.sh [-R <owner/repo>] [--limit <n>] [--fixture-test]" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
if ! [[ "$2" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
|
53
|
+
echo "ERROR: -R value must be owner/repo format, got: $2" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
GH_REPO_ARGS=(-R "$2")
|
|
57
|
+
shift 2
|
|
58
|
+
;;
|
|
59
|
+
--limit)
|
|
60
|
+
if [ $# -lt 2 ] || ! [[ "$2" =~ ^[0-9]+$ ]]; then
|
|
61
|
+
echo "ERROR: --limit requires a positive integer" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
LIMIT="$2"
|
|
65
|
+
shift 2
|
|
66
|
+
;;
|
|
67
|
+
--fixture-test)
|
|
68
|
+
FIXTURE_TEST=true
|
|
69
|
+
shift
|
|
70
|
+
;;
|
|
71
|
+
*)
|
|
72
|
+
echo "ERROR: unknown argument: $1" >&2
|
|
73
|
+
echo "Usage: select-fix-targets.sh [-R <owner/repo>] [--limit <n>] [--fixture-test]" >&2
|
|
74
|
+
exit 1
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Fixture test mode (wire-through proof per #1731)
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Runs the ranking logic against synthetic JSON that exercises every edge case:
|
|
83
|
+
# - P0 issue (should be first)
|
|
84
|
+
# - P1 issue (should be second)
|
|
85
|
+
# - Unlabeled issue (should be excluded)
|
|
86
|
+
# - Fresh P2 self-filed finding (should be last among selected, after P0 and P1)
|
|
87
|
+
# - Older P2 issue (should appear before fresh P2 within the P2 band)
|
|
88
|
+
# Expected output order: 100 (P0), 101 (P1, older), 103 (P2, older), 104 (P2, newer)
|
|
89
|
+
# Issues 102 (unlabeled) and 105 (needs-human) must be absent from output.
|
|
90
|
+
#
|
|
91
|
+
# This test does NOT make GitHub API calls — it validates the jq pipeline directly.
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
if [ "$FIXTURE_TEST" = "true" ]; then
|
|
94
|
+
FIXTURE_JSON='[
|
|
95
|
+
{
|
|
96
|
+
"number": 100,
|
|
97
|
+
"createdAt": "2026-01-01T00:00:00Z",
|
|
98
|
+
"milestone": null,
|
|
99
|
+
"labels": [{"name": "priority:P0"}, {"name": "bug"}]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"number": 101,
|
|
103
|
+
"createdAt": "2026-01-02T00:00:00Z",
|
|
104
|
+
"milestone": null,
|
|
105
|
+
"labels": [{"name": "priority:P1"}]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"number": 102,
|
|
109
|
+
"createdAt": "2026-01-03T00:00:00Z",
|
|
110
|
+
"milestone": null,
|
|
111
|
+
"labels": [{"name": "bug"}]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"number": 103,
|
|
115
|
+
"createdAt": "2026-03-01T00:00:00Z",
|
|
116
|
+
"milestone": null,
|
|
117
|
+
"labels": [{"name": "priority:P2"}]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"number": 104,
|
|
121
|
+
"createdAt": "2026-07-08T01:30:00Z",
|
|
122
|
+
"milestone": null,
|
|
123
|
+
"labels": [{"name": "priority:P2"}, {"name": "bug"}]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"number": 105,
|
|
127
|
+
"createdAt": "2026-01-01T00:00:00Z",
|
|
128
|
+
"milestone": null,
|
|
129
|
+
"labels": [{"name": "priority:P0"}, {"name": "needs-human"}]
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"number": 106,
|
|
133
|
+
"createdAt": "2026-01-01T00:00:00Z",
|
|
134
|
+
"milestone": {"title": "v2.0"},
|
|
135
|
+
"labels": [{"name": "priority:P0"}]
|
|
136
|
+
}
|
|
137
|
+
]'
|
|
138
|
+
|
|
139
|
+
# Expected order: 100, 101, 103, 104
|
|
140
|
+
# Excluded: 102 (no priority label), 105 (needs-human), 106 (has milestone)
|
|
141
|
+
EXPECTED="100
|
|
142
|
+
101
|
|
143
|
+
103
|
|
144
|
+
104"
|
|
145
|
+
|
|
146
|
+
ACTUAL=$(echo "$FIXTURE_JSON" | jq -r '
|
|
147
|
+
# Step 1: Filter out exclusions
|
|
148
|
+
[.[] | select(
|
|
149
|
+
.milestone == null and
|
|
150
|
+
(.labels | map(.name) | any(. == "needs-human" or
|
|
151
|
+
. == "workflow:merged" or . == "workflow:invalid" or
|
|
152
|
+
. == "workflow:decomposed" or . == "workflow:building" or
|
|
153
|
+
. == "workflow:in-review") | not) and
|
|
154
|
+
# Only include issues with an explicit priority:P* label (unlabeled = excluded)
|
|
155
|
+
(.labels | map(.name) | any(startswith("priority:P")))
|
|
156
|
+
)] |
|
|
157
|
+
# Step 2: Assign numeric priority rank (lower = higher priority)
|
|
158
|
+
# P0 → 0, P1 → 1, P2 → 2, any other priority:P* → 9 (future-proof)
|
|
159
|
+
map(. + {
|
|
160
|
+
"_rank": (
|
|
161
|
+
.labels | map(.name) |
|
|
162
|
+
if any(. == "priority:P0") then 0
|
|
163
|
+
elif any(. == "priority:P1") then 1
|
|
164
|
+
elif any(. == "priority:P2") then 2
|
|
165
|
+
else 9
|
|
166
|
+
end
|
|
167
|
+
)
|
|
168
|
+
}) |
|
|
169
|
+
# Step 3: Sort by rank ASC, then by createdAt ASC (oldest wins within same band)
|
|
170
|
+
sort_by([._rank, .createdAt]) |
|
|
171
|
+
# Step 4: Output issue numbers
|
|
172
|
+
.[].number
|
|
173
|
+
')
|
|
174
|
+
|
|
175
|
+
if [ "$ACTUAL" = "$EXPECTED" ]; then
|
|
176
|
+
echo "FIXTURE TEST PASSED — ranking: $(echo "$ACTUAL" | tr '\n' ' ')"
|
|
177
|
+
echo "Excluded correctly: 102 (unlabeled), 105 (needs-human), 106 (has milestone)"
|
|
178
|
+
exit 0
|
|
179
|
+
else
|
|
180
|
+
echo "FIXTURE TEST FAILED" >&2
|
|
181
|
+
echo "Expected order: $(echo "$EXPECTED" | tr '\n' ' ')" >&2
|
|
182
|
+
echo "Actual order: $(echo "$ACTUAL" | tr '\n' ' ')" >&2
|
|
183
|
+
echo "" >&2
|
|
184
|
+
echo "Diff:" >&2
|
|
185
|
+
diff <(echo "$EXPECTED") <(echo "$ACTUAL") >&2
|
|
186
|
+
exit 1
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Live mode: query GitHub and apply ranking
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
# Fetch open issues — get all candidates in one API call.
|
|
195
|
+
# JSON fields: number, createdAt, milestone (null or object), labels (array of name objects).
|
|
196
|
+
# Limit to 200 issues to match the rest of autopilot's queries; ranking is applied in jq.
|
|
197
|
+
ISSUES_JSON=$(gh issue list "${GH_REPO_ARGS[@]}" \
|
|
198
|
+
--state open \
|
|
199
|
+
--limit 200 \
|
|
200
|
+
--json number,createdAt,milestone,labels \
|
|
201
|
+
2>/dev/null || echo '[]')
|
|
202
|
+
|
|
203
|
+
if [ "$ISSUES_JSON" = "[]" ] || [ -z "$ISSUES_JSON" ]; then
|
|
204
|
+
echo "" # No issues — empty output is valid (caller loops over nothing)
|
|
205
|
+
exit 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
echo "$ISSUES_JSON" | jq -r --argjson limit "$LIMIT" '
|
|
209
|
+
# Step 1: Filter out exclusions (same logic as fixture test)
|
|
210
|
+
[.[] | select(
|
|
211
|
+
.milestone == null and
|
|
212
|
+
(.labels | map(.name) | any(. == "needs-human" or
|
|
213
|
+
. == "workflow:merged" or . == "workflow:invalid" or
|
|
214
|
+
. == "workflow:decomposed" or . == "workflow:building" or
|
|
215
|
+
. == "workflow:in-review") | not) and
|
|
216
|
+
# Only include issues with an explicit priority:P* label (unlabeled = excluded)
|
|
217
|
+
(.labels | map(.name) | any(startswith("priority:P")))
|
|
218
|
+
)] |
|
|
219
|
+
# Step 2: Assign numeric priority rank
|
|
220
|
+
map(. + {
|
|
221
|
+
"_rank": (
|
|
222
|
+
.labels | map(.name) |
|
|
223
|
+
if any(. == "priority:P0") then 0
|
|
224
|
+
elif any(. == "priority:P1") then 1
|
|
225
|
+
elif any(. == "priority:P2") then 2
|
|
226
|
+
else 9
|
|
227
|
+
end
|
|
228
|
+
)
|
|
229
|
+
}) |
|
|
230
|
+
# Step 3: Sort by rank ASC, then by createdAt ASC (oldest wins within same priority band)
|
|
231
|
+
sort_by([._rank, .createdAt]) |
|
|
232
|
+
# Step 4: Apply limit and output issue numbers
|
|
233
|
+
.[:$limit] | .[].number
|
|
234
|
+
'
|
|
@@ -2,29 +2,122 @@
|
|
|
2
2
|
# transition-label.sh — Workflow label state machine for ForgeDock pipeline
|
|
3
3
|
#
|
|
4
4
|
# Usage: transition-label.sh <ISSUE_NUMBER> <GH_FLAG...> <TARGET_STATE>
|
|
5
|
+
# OR: transition-label.sh --validate <VERDICT> <ISSUE_NUMBER> [GH_FLAG...]
|
|
5
6
|
#
|
|
6
7
|
# ISSUE_NUMBER GitHub issue number (e.g. 674)
|
|
7
8
|
# GH_FLAG Repository flag passed to gh (e.g. -R RapierCraftStudios/forgedock)
|
|
8
9
|
# May be multiple tokens — pass before TARGET_STATE
|
|
9
10
|
# TARGET_STATE One of: investigating, ready-to-build, building, in-review,
|
|
10
|
-
# merged, invalid, decomposed
|
|
11
|
+
# merged, invalid, decomposed, awaiting-merge
|
|
12
|
+
#
|
|
13
|
+
# --validate Sub-command mode: translate an investigation verdict into a
|
|
14
|
+
# finding-lifecycle label (needs-validation → validated/false-positive).
|
|
15
|
+
# VERDICT One of: CONFIRMED, NOT-CONFIRMED, INVALID, PARTIAL
|
|
16
|
+
# CONFIRMED → validated; all others → false-positive
|
|
11
17
|
#
|
|
12
18
|
# Examples:
|
|
13
19
|
# transition-label.sh 674 -R RapierCraftStudios/forgedock investigating
|
|
14
20
|
# transition-label.sh 674 -R owner/repo ready-to-build
|
|
21
|
+
# transition-label.sh --validate CONFIRMED 674 -R owner/repo
|
|
22
|
+
# transition-label.sh --validate NOT-CONFIRMED 674 -R owner/repo
|
|
15
23
|
#
|
|
16
|
-
# Behavior:
|
|
24
|
+
# Behavior (workflow mode):
|
|
17
25
|
# 1. Validates ISSUE_NUMBER and TARGET_STATE
|
|
18
26
|
# 2. Verifies the issue exists (exits 1 if not)
|
|
19
27
|
# 3. Adds workflow:{TARGET_STATE} label
|
|
20
28
|
# 4. Removes all other workflow:* labels atomically
|
|
21
29
|
#
|
|
30
|
+
# Behavior (--validate mode):
|
|
31
|
+
# 1. Validates VERDICT and ISSUE_NUMBER
|
|
32
|
+
# 2. Verifies issue exists and has needs-validation label (exits 0 silently if not)
|
|
33
|
+
# 3. Adds validated (CONFIRMED) or false-positive (all other verdicts) label
|
|
34
|
+
# 4. Removes needs-validation label
|
|
35
|
+
# 5. Idempotent: if already labeled validated/false-positive, exits 0 silently
|
|
36
|
+
#
|
|
22
37
|
# Exit codes: 0 = success, 1 = error (bad args, issue not found, gh failure)
|
|
23
38
|
|
|
24
39
|
set -euo pipefail
|
|
25
40
|
|
|
26
41
|
# ---------------------------------------------------------------------------
|
|
27
|
-
#
|
|
42
|
+
# --validate sub-command: finding lifecycle label transition
|
|
43
|
+
# Signature: --validate <VERDICT> <ISSUE_NUMBER> [GH_FLAG...]
|
|
44
|
+
# Separate from the workflow state machine — operates on needs-validation,
|
|
45
|
+
# validated, false-positive labels only; never touches workflow:* labels.
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
if [ "${1:-}" = "--validate" ]; then
|
|
48
|
+
shift
|
|
49
|
+
|
|
50
|
+
if [ "$#" -lt 2 ]; then
|
|
51
|
+
echo "ERROR: Usage: transition-label.sh --validate <VERDICT> <ISSUE_NUMBER> [GH_FLAG...]" >&2
|
|
52
|
+
echo " VERDICT: CONFIRMED | NOT-CONFIRMED | INVALID | PARTIAL" >&2
|
|
53
|
+
echo " Example: transition-label.sh --validate CONFIRMED 674 -R owner/repo" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
VERDICT="$1"
|
|
58
|
+
shift
|
|
59
|
+
ISSUE_NUMBER="$1"
|
|
60
|
+
shift
|
|
61
|
+
GH_ARGS=("$@")
|
|
62
|
+
|
|
63
|
+
# Validate verdict
|
|
64
|
+
case "$VERDICT" in
|
|
65
|
+
CONFIRMED|NOT-CONFIRMED|INVALID|PARTIAL) ;;
|
|
66
|
+
*)
|
|
67
|
+
echo "ERROR: Unknown verdict: '$VERDICT'" >&2
|
|
68
|
+
echo " Valid verdicts: CONFIRMED, NOT-CONFIRMED, INVALID, PARTIAL" >&2
|
|
69
|
+
exit 1
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
|
|
73
|
+
# Validate issue number
|
|
74
|
+
if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
75
|
+
echo "ERROR: ISSUE_NUMBER must be a positive integer, got: '$ISSUE_NUMBER'" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Verify issue exists
|
|
80
|
+
if ! gh issue view "$ISSUE_NUMBER" "${GH_ARGS[@]}" --json number >/dev/null 2>&1; then
|
|
81
|
+
echo "ERROR: Issue #$ISSUE_NUMBER not found (GH_FLAG: ${GH_ARGS[*]:-<none>})" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# Check if issue carries needs-validation (idempotent gate)
|
|
86
|
+
CURRENT_LABELS=$(gh issue view "$ISSUE_NUMBER" "${GH_ARGS[@]}" --json labels \
|
|
87
|
+
--jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
|
|
88
|
+
|
|
89
|
+
if ! echo "$CURRENT_LABELS" | grep -q "needs-validation"; then
|
|
90
|
+
echo "OK: Issue #$ISSUE_NUMBER does not have needs-validation — no action taken (idempotent)"
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Check if already resolved (idempotent)
|
|
95
|
+
if echo "$CURRENT_LABELS" | grep -qE "validated|false-positive"; then
|
|
96
|
+
echo "OK: Issue #$ISSUE_NUMBER already has a resolved verdict label — no action taken (idempotent)"
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Map verdict to label
|
|
101
|
+
if [ "$VERDICT" = "CONFIRMED" ]; then
|
|
102
|
+
TARGET_VERDICT_LABEL="validated"
|
|
103
|
+
echo "Verdict CONFIRMED → adding 'validated' to issue #$ISSUE_NUMBER..."
|
|
104
|
+
else
|
|
105
|
+
TARGET_VERDICT_LABEL="false-positive"
|
|
106
|
+
echo "Verdict $VERDICT → adding 'false-positive' to issue #$ISSUE_NUMBER..."
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Add verdict label
|
|
110
|
+
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --add-label "$TARGET_VERDICT_LABEL"
|
|
111
|
+
|
|
112
|
+
# Remove needs-validation
|
|
113
|
+
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "needs-validation" 2>/dev/null || true
|
|
114
|
+
|
|
115
|
+
echo "OK: needs-validation → $TARGET_VERDICT_LABEL on issue #$ISSUE_NUMBER"
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Argument parsing (workflow state machine mode)
|
|
28
121
|
# Signature: <ISSUE_NUMBER> [GH_FLAG...] <TARGET_STATE>
|
|
29
122
|
# We consume ISSUE_NUMBER as $1, TARGET_STATE as the last arg, everything
|
|
30
123
|
# in between is GH_FLAG (e.g. -R RapierCraftStudios/forgedock).
|
|
@@ -33,6 +126,7 @@ set -euo pipefail
|
|
|
33
126
|
if [ "$#" -lt 2 ]; then
|
|
34
127
|
echo "ERROR: Usage: transition-label.sh <ISSUE_NUMBER> [GH_FLAG...] <TARGET_STATE>" >&2
|
|
35
128
|
echo " Example: transition-label.sh 674 -R RapierCraftStudios/forgedock investigating" >&2
|
|
129
|
+
echo " OR: transition-label.sh --validate CONFIRMED 674 -R owner/repo" >&2
|
|
36
130
|
exit 1
|
|
37
131
|
fi
|
|
38
132
|
|
|
@@ -65,6 +159,7 @@ VALID_STATES=(
|
|
|
65
159
|
"merged"
|
|
66
160
|
"invalid"
|
|
67
161
|
"decomposed"
|
|
162
|
+
"awaiting-merge"
|
|
68
163
|
)
|
|
69
164
|
|
|
70
165
|
# ---------------------------------------------------------------------------
|
|
@@ -161,9 +256,65 @@ echo "Adding $EFFECTIVE_LABEL to issue #$ISSUE_NUMBER..."
|
|
|
161
256
|
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --add-label "$EFFECTIVE_LABEL"
|
|
162
257
|
|
|
163
258
|
# ---------------------------------------------------------------------------
|
|
164
|
-
# Remove all other workflow:* labels (best-effort
|
|
259
|
+
# Remove all other workflow:* labels currently on the issue (best-effort)
|
|
260
|
+
#
|
|
261
|
+
# IMPORTANT: `gh issue edit --remove-label` is atomic across its whole
|
|
262
|
+
# comma-separated argument — if ANY label in the list is not a valid label
|
|
263
|
+
# on the repo (e.g. a newly added VALID_STATES entry like awaiting-merge
|
|
264
|
+
# whose repo-side `gh label create` / bootstrap hasn't run yet), the ENTIRE
|
|
265
|
+
# call fails with "not found" and — under `set -euo pipefail` without the
|
|
266
|
+
# `|| true` this used to silently swallow — no labels are removed at all,
|
|
267
|
+
# including ones that DO exist and SHOULD have been cleared (forge#1810
|
|
268
|
+
# follow-up: this exact bug was caught by dogfooding this script against
|
|
269
|
+
# the live repo before `workflow:awaiting-merge` had been bootstrapped).
|
|
270
|
+
#
|
|
271
|
+
# Fix: only ask to remove labels that are BOTH (a) in REMOVE_LABELS (valid
|
|
272
|
+
# states other than the target) AND (b) actually present on the issue right
|
|
273
|
+
# now. A label that was never applied to this issue can't be "not found" on
|
|
274
|
+
# the repo without failing the whole call, and a label the issue doesn't
|
|
275
|
+
# have doesn't need removing anyway — so intersecting against the issue's
|
|
276
|
+
# current labels sidesteps the all-or-nothing failure mode entirely instead
|
|
277
|
+
# of relying on `|| true` to mask it.
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
CURRENT_ISSUE_LABELS=$(gh issue view "$ISSUE_NUMBER" "${GH_ARGS[@]}" --json labels \
|
|
280
|
+
--jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
|
|
281
|
+
|
|
282
|
+
TO_REMOVE=""
|
|
283
|
+
IFS=',' read -ra REMOVE_CANDIDATES <<< "$REMOVE_LABELS"
|
|
284
|
+
for candidate in "${REMOVE_CANDIDATES[@]}"; do
|
|
285
|
+
case ",$CURRENT_ISSUE_LABELS," in
|
|
286
|
+
*",$candidate,"*)
|
|
287
|
+
TO_REMOVE="${TO_REMOVE:+$TO_REMOVE,}$candidate"
|
|
288
|
+
;;
|
|
289
|
+
esac
|
|
290
|
+
done
|
|
291
|
+
|
|
292
|
+
if [ -n "$TO_REMOVE" ]; then
|
|
293
|
+
echo "Removing stale workflow:* labels present on the issue ($TO_REMOVE)..."
|
|
294
|
+
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "$TO_REMOVE" 2>/dev/null || true
|
|
295
|
+
else
|
|
296
|
+
echo "No stale workflow:* labels present on the issue — nothing to remove."
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Clear needs-human (best-effort)
|
|
301
|
+
#
|
|
302
|
+
# needs-human is NOT a workflow:* label, so it is never included in
|
|
303
|
+
# VALID_STATES/REMOVE_LABELS above. Historically no code path ever cleared it
|
|
304
|
+
# (forge#1809/#1810) — it was a write-only, sticky, terminal label even after
|
|
305
|
+
# the pipeline made forward progress past the condition that set it.
|
|
306
|
+
#
|
|
307
|
+
# This script only runs at explicit forward-progress transition points (the
|
|
308
|
+
# dispatcher STOPs on needs-human before ever reaching a transition-label.sh
|
|
309
|
+
# call in normal operation — see commands/work-on.md's terminal-state checks),
|
|
310
|
+
# so clearing needs-human here is safe: it only fires when something has
|
|
311
|
+
# deliberately decided to move the issue/PR to a new state, most notably
|
|
312
|
+
# workflow:awaiting-merge (a remediated + re-reviewed PR moving off
|
|
313
|
+
# needs-human without yet meeting the auto-land bar — see #1809 Q1).
|
|
314
|
+
# Best-effort: `|| true` so a missing label never fails the script under
|
|
315
|
+
# `set -euo pipefail`.
|
|
165
316
|
# ---------------------------------------------------------------------------
|
|
166
|
-
echo "
|
|
167
|
-
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "
|
|
317
|
+
echo "Clearing needs-human (best-effort)..."
|
|
318
|
+
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "needs-human" 2>/dev/null || true
|
|
168
319
|
|
|
169
320
|
echo "OK: $EFFECTIVE_LABEL set on issue #$ISSUE_NUMBER"
|