forgedock 1.0.16 → 1.0.18
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 +54 -3
- package/bin/cross-publish.mjs +344 -0
- package/bin/forgedock.mjs +2205 -541
- package/bin/github-app-manifest.json.license +2 -0
- package/bin/hooks/session-start.mjs +4 -5
- package/bin/registry.mjs +20 -0
- package/bin/tui.mjs +172 -96
- package/commands/adopt.md +433 -0
- package/commands/analytics.md +2 -0
- package/commands/audit-agents.md +2 -0
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +77 -0
- package/commands/changelog.md +354 -0
- package/commands/cleanup.md +2 -0
- package/commands/compat-audit.md +330 -0
- package/commands/deploy-info.md +6 -5
- package/commands/design-bench.md +176 -0
- package/commands/design-render-critique-loop.md +103 -0
- package/commands/design.md +259 -0
- package/commands/diagnose.md +483 -0
- package/commands/explain.md +349 -0
- package/commands/forge-stats.md +2 -0
- package/commands/forgedock-init.md +2 -0
- package/commands/geo-audit.md +2 -0
- package/commands/incident-response.md +2 -0
- package/commands/issue.md +9 -3
- package/commands/milestone.md +2 -0
- package/commands/optimize.md +411 -0
- package/commands/orchestrate.md +239 -0
- package/commands/pipeline-health.md +60 -1
- package/commands/qa-sweep.md +39 -2
- package/commands/quality-gate.md +67 -0
- package/commands/replay.md +263 -0
- package/commands/resume.md +264 -0
- package/commands/review-pr-agents.md +5 -1
- package/commands/review-pr-staging.md +160 -2
- package/commands/review-pr.md +85 -3
- package/commands/rollback.md +2 -0
- package/commands/scope.md +280 -0
- package/commands/security-audit.md +2 -0
- package/commands/status.md +262 -0
- package/commands/sync-ecosystem.md +2 -0
- package/commands/test-gate.md +929 -0
- package/commands/validate.md +2 -0
- package/commands/work-on/build/architect.md +128 -18
- package/commands/work-on/build/context.md +130 -17
- package/commands/work-on/build/implement.md +7 -3
- package/commands/work-on/build/validate.md +2 -0
- package/commands/work-on/build.md +32 -7
- package/commands/work-on/close.md +85 -0
- package/commands/work-on/decompose.md +2 -0
- package/commands/work-on/investigate.md +30 -3
- package/commands/work-on/review.md +13 -1
- package/commands/work-on-monolithic.md +2 -0
- package/commands/work-on.md +676 -38
- package/docs/BRAND.md +51 -0
- package/docs/CI.md +177 -0
- package/docs/CONFIG.md +94 -0
- package/docs/FORGE-PROTOCOL.md +324 -0
- package/docs/articles/b-vs-c-baseline.md +210 -0
- package/docs/articles/full-abc-benchmark.md +205 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
- package/docs/articles/selective-spec-loading-token-savings.md +140 -0
- package/docs/claude-breakpoints.json +47 -0
- package/docs/design/abc-benchmark.md +195 -0
- package/docs/design/design-architect-rationale.md +123 -0
- package/docs/design/design-memory.md +61 -0
- package/docs/design/design-spec-schema.md +248 -0
- package/docs/design/design-system-lint.md +150 -0
- package/docs/design/divergent-generation.md +106 -0
- package/docs/design/effects-appropriateness.md +133 -0
- package/docs/design/fixtures/briefs/README.md +47 -0
- package/docs/design/fixtures/briefs/cadence.md +39 -0
- package/docs/design/fixtures/briefs/plume.md +39 -0
- package/docs/design/fixtures/briefs/slipstream.md +39 -0
- package/docs/design/fixtures/briefs/tender.md +39 -0
- package/docs/design/fixtures/briefs/voltage.md +39 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
- package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
- package/docs/design/foundations/brutalist.css +294 -0
- package/docs/design/foundations/editorial.css +300 -0
- package/docs/design/foundations/minimal-luxury.css +295 -0
- package/docs/design/foundations/technical.css +293 -0
- package/docs/design/foundations/warm-photo.css +299 -0
- package/docs/design/reference-corpus.md +1726 -0
- package/docs/design/render-critique-loop.md +57 -0
- package/docs/site/getting-started.md +21 -0
- package/docs/spec-graph-schema.md +397 -0
- package/package.json +3 -3
- package/scripts/bench-scorecard.mjs +239 -0
- package/scripts/build-spec-graph.mjs +509 -0
- package/scripts/classify-lane.sh +138 -0
- package/scripts/design-system-lint.mjs +993 -0
- package/scripts/graph-query.sh +486 -0
- package/scripts/transition-label.sh +169 -0
- package/scripts/validate-pr-target.sh +30 -0
- package/scripts/validate-spec-graph.sh +423 -0
- package/templates/devdocs/README.md +72 -0
- package/templates/devdocs/agent/using-forgedock.md +3 -0
- package/templates/devdocs/agent/using-github.md +3 -0
- package/templates/devdocs/index.yaml +162 -0
- package/templates/devdocs/project/architecture.md +3 -0
- package/templates/devdocs/project/conventions.md +3 -0
- package/templates/devdocs/project/custom-instructions.md +3 -0
- package/templates/devdocs/project/glossary.md +3 -0
- package/templates/devdocs/project/stack.md +3 -0
- package/templates/devdocs/templates/commit-convention.md +3 -0
- package/templates/devdocs/templates/issue.md +3 -0
- package/templates/devdocs/templates/pull-request.md +3 -0
- package/templates/devdocs/templates/review-finding.md +3 -0
- package/templates/workflows/forgedock-review.yml +96 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# graph-query.sh — Read-only query interface over the Spec Knowledge Graph
|
|
6
|
+
#
|
|
7
|
+
# Queries the spec knowledge graph produced by `build-spec-graph.mjs`
|
|
8
|
+
# (`.forgedock/graph/spec-graph.json`). The graph is a self-map of ForgeDock's
|
|
9
|
+
# own command specs: which command WRITES/READS which FORGE annotation, which
|
|
10
|
+
# scripts a command INVOKES, which devdocs it REQUIRES, etc. This script is the
|
|
11
|
+
# consumer-facing query API so agents (and humans) don't hand-write jq.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# graph-query.sh <subcommand> <arg> [--human] [--graph <path>]
|
|
15
|
+
#
|
|
16
|
+
# Subcommands:
|
|
17
|
+
# readers <annotation> — commands/specs that READ the annotation
|
|
18
|
+
# writers <annotation> — commands/specs that WRITE (post) the annotation
|
|
19
|
+
# impact <node> — blast radius: everything transitively affected if
|
|
20
|
+
# <node> changes (annotation | label | script | command/spec)
|
|
21
|
+
# deps <command> — what a command reads/invokes/requires/contains
|
|
22
|
+
# (its full input + output set), grouped by edge type
|
|
23
|
+
# load-set <command> — minimal spec set to read for a command: the command
|
|
24
|
+
# itself plus its transitively reachable sub-phases
|
|
25
|
+
# (CONTAINS) and required devdocs (REQUIRES), as a flat
|
|
26
|
+
# list of repo-relative file paths. This is the inverse
|
|
27
|
+
# of `impact` (forward instead of reverse reachability)
|
|
28
|
+
# and powers selective spec loading in the pipeline.
|
|
29
|
+
# search <term> — substring lookup over node ids / names / paths
|
|
30
|
+
#
|
|
31
|
+
# Arguments are normalized, so all of these are accepted:
|
|
32
|
+
# readers CONTRACT == readers FORGE:CONTRACT == readers ann:FORGE:CONTRACT
|
|
33
|
+
# deps work-on == deps cmd:work-on
|
|
34
|
+
# deps work-on:build:implement == deps cmd:work-on:build:implement
|
|
35
|
+
# load-set work-on == load-set cmd:work-on
|
|
36
|
+
# impact classify-lane.sh == impact script:classify-lane.sh
|
|
37
|
+
# impact workflow:merged == impact label:workflow:merged
|
|
38
|
+
#
|
|
39
|
+
# Staleness detection (no daemon):
|
|
40
|
+
# A persisted graph carries `builtFromHash`, a sha256 fingerprint of the spec
|
|
41
|
+
# corpus it was built from. On each query against a persisted graph, this
|
|
42
|
+
# script cheaply recomputes the current fingerprint (build-spec-graph.mjs
|
|
43
|
+
# --hash) and compares. If they differ, the persisted graph is stale: a
|
|
44
|
+
# `stale-graph` banner is printed to STDERR and the graph is transparently
|
|
45
|
+
# rebuilt for this query (stdout stays pure JSON). The no-change path does one
|
|
46
|
+
# cheap hash compare and no rebuild — no background process, no watcher.
|
|
47
|
+
#
|
|
48
|
+
# Flags:
|
|
49
|
+
# --human Render an aligned table instead of compact JSON.
|
|
50
|
+
# --graph <path> Use a specific graph JSON (default:
|
|
51
|
+
# <repo>/.forgedock/graph/spec-graph.json). If the default is
|
|
52
|
+
# absent (it is gitignored), the graph is auto-built on the fly
|
|
53
|
+
# via build-spec-graph.mjs — no committed graph is required.
|
|
54
|
+
# --strict-stale Do NOT auto-rebuild a stale persisted graph — error (exit 1)
|
|
55
|
+
# instead. Useful in CI to assert the committed graph is fresh.
|
|
56
|
+
# --no-stale-check Skip the staleness check entirely (query the persisted graph
|
|
57
|
+
# as-is, even if the spec corpus has changed).
|
|
58
|
+
# -h | --help Show this help.
|
|
59
|
+
#
|
|
60
|
+
# Output:
|
|
61
|
+
# Default is compact, agent-consumable JSON (a JSON array for
|
|
62
|
+
# readers/writers/impact/search/load-set; a JSON object keyed by edge type
|
|
63
|
+
# for deps). load-set returns a sorted, de-duplicated array of repo-relative
|
|
64
|
+
# file paths. With --human, an aligned text table is printed instead.
|
|
65
|
+
#
|
|
66
|
+
# Exit codes: 0 = success, 1 = error (bad args, missing deps, unknown node).
|
|
67
|
+
#
|
|
68
|
+
# This is a UNIVERSAL-tier script (ships with the npm package). It is read-only:
|
|
69
|
+
# it never mutates the repo or the graph (it only writes a temp cache under
|
|
70
|
+
# $TMPDIR when it must auto-build, removed on exit). See
|
|
71
|
+
# devdocs/project/architecture.md for the scripts-layer precedence model and
|
|
72
|
+
# docs/spec-graph-schema.md for the graph schema.
|
|
73
|
+
|
|
74
|
+
set -euo pipefail
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Paths & dependency checks
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
81
|
+
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
82
|
+
BUILDER="$SCRIPT_DIR/build-spec-graph.mjs"
|
|
83
|
+
DEFAULT_GRAPH="$REPO_ROOT/.forgedock/graph/spec-graph.json"
|
|
84
|
+
|
|
85
|
+
# Exported so per-repo adaptive scripts can delegate (see architecture.md).
|
|
86
|
+
export FORGEDOCK_SCRIPTS="$SCRIPT_DIR"
|
|
87
|
+
export FORGEDOCK_HOME="$REPO_ROOT"
|
|
88
|
+
|
|
89
|
+
print_help() {
|
|
90
|
+
# Echo the leading comment block (between the shebang and `set -euo`).
|
|
91
|
+
sed -n '3,/^set -euo pipefail/p' "${BASH_SOURCE[0]}" \
|
|
92
|
+
| sed '$d' \
|
|
93
|
+
| sed 's/^# \{0,1\}//'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
die() {
|
|
97
|
+
echo "ERROR: $*" >&2
|
|
98
|
+
exit 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
require_dep() {
|
|
102
|
+
command -v "$1" >/dev/null 2>&1 || die "required dependency '$1' not found on PATH"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Argument parsing
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
SUBCMD=""
|
|
110
|
+
ARG=""
|
|
111
|
+
HUMAN=0
|
|
112
|
+
GRAPH_PATH=""
|
|
113
|
+
STRICT_STALE=0
|
|
114
|
+
STALE_CHECK=1
|
|
115
|
+
|
|
116
|
+
if [ "$#" -eq 0 ]; then
|
|
117
|
+
print_help
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
POSITIONAL=()
|
|
122
|
+
while [ "$#" -gt 0 ]; do
|
|
123
|
+
case "$1" in
|
|
124
|
+
-h|--help)
|
|
125
|
+
print_help
|
|
126
|
+
exit 0
|
|
127
|
+
;;
|
|
128
|
+
--human)
|
|
129
|
+
HUMAN=1
|
|
130
|
+
shift
|
|
131
|
+
;;
|
|
132
|
+
--graph)
|
|
133
|
+
[ "$#" -ge 2 ] || die "--graph requires a path"
|
|
134
|
+
GRAPH_PATH="$2"
|
|
135
|
+
shift 2
|
|
136
|
+
;;
|
|
137
|
+
--strict-stale)
|
|
138
|
+
STRICT_STALE=1
|
|
139
|
+
shift
|
|
140
|
+
;;
|
|
141
|
+
--no-stale-check)
|
|
142
|
+
STALE_CHECK=0
|
|
143
|
+
shift
|
|
144
|
+
;;
|
|
145
|
+
--)
|
|
146
|
+
shift
|
|
147
|
+
while [ "$#" -gt 0 ]; do POSITIONAL+=("$1"); shift; done
|
|
148
|
+
;;
|
|
149
|
+
-*)
|
|
150
|
+
die "unknown flag: $1"
|
|
151
|
+
;;
|
|
152
|
+
*)
|
|
153
|
+
POSITIONAL+=("$1")
|
|
154
|
+
shift
|
|
155
|
+
;;
|
|
156
|
+
esac
|
|
157
|
+
done
|
|
158
|
+
|
|
159
|
+
if [ "${#POSITIONAL[@]}" -ge 1 ]; then SUBCMD="${POSITIONAL[0]}"; fi
|
|
160
|
+
if [ "${#POSITIONAL[@]}" -ge 2 ]; then ARG="${POSITIONAL[1]}"; fi
|
|
161
|
+
if [ "${#POSITIONAL[@]}" -gt 2 ]; then
|
|
162
|
+
die "too many arguments: expected '<subcommand> <arg>', got ${#POSITIONAL[@]} positionals"
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
[ -n "$SUBCMD" ] || die "no subcommand given (one of: readers writers impact deps load-set search)"
|
|
166
|
+
|
|
167
|
+
case "$SUBCMD" in
|
|
168
|
+
readers|writers|impact|deps|load-set|search) ;;
|
|
169
|
+
*) die "unknown subcommand '$SUBCMD' (one of: readers writers impact deps load-set search)" ;;
|
|
170
|
+
esac
|
|
171
|
+
|
|
172
|
+
[ -n "$ARG" ] || die "subcommand '$SUBCMD' requires an argument"
|
|
173
|
+
|
|
174
|
+
require_dep jq
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Resolve the graph JSON (auto-build if absent — the graph is gitignored)
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
GRAPH=""
|
|
181
|
+
TMP_GRAPH=""
|
|
182
|
+
|
|
183
|
+
cleanup() {
|
|
184
|
+
[ -n "$TMP_GRAPH" ] && [ -f "$TMP_GRAPH" ] && rm -f "$TMP_GRAPH" || true
|
|
185
|
+
}
|
|
186
|
+
trap cleanup EXIT
|
|
187
|
+
|
|
188
|
+
# Rebuild the graph to a temp file and point GRAPH at it. Shared by the
|
|
189
|
+
# absent-graph auto-build path and the stale-graph rebuild path.
|
|
190
|
+
build_graph_to_temp() {
|
|
191
|
+
require_dep node
|
|
192
|
+
[ -f "$BUILDER" ] || die "graph is absent and builder not found: $BUILDER"
|
|
193
|
+
TMP_GRAPH="$(mktemp "${TMPDIR:-/tmp}/spec-graph.XXXXXX.json")"
|
|
194
|
+
if ! node "$BUILDER" --root "$REPO_ROOT" --stdout --quiet >"$TMP_GRAPH" 2>/dev/null; then
|
|
195
|
+
die "failed to build spec graph via $BUILDER"
|
|
196
|
+
fi
|
|
197
|
+
GRAPH="$TMP_GRAPH"
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Staleness check for a PERSISTED graph (default path or --graph). Cheaply
|
|
201
|
+
# recompute the current spec-corpus fingerprint and compare to the graph's
|
|
202
|
+
# stored builtFromHash. On mismatch: warn (stderr) and rebuild, unless
|
|
203
|
+
# --strict-stale (error) is set. A graph with no builtFromHash (pre-staleness
|
|
204
|
+
# schema) is treated as stale. Diagnostics go to stderr only; stdout stays pure.
|
|
205
|
+
check_staleness() {
|
|
206
|
+
[ "$STALE_CHECK" -eq 1 ] || return 0
|
|
207
|
+
# `node` is required to recompute the hash; if it is unavailable, skip the
|
|
208
|
+
# check rather than failing an otherwise-valid query.
|
|
209
|
+
command -v node >/dev/null 2>&1 || return 0
|
|
210
|
+
[ -f "$BUILDER" ] || return 0
|
|
211
|
+
|
|
212
|
+
local stored current
|
|
213
|
+
stored="$(jq -r '.graph.builtFromHash // ""' "$GRAPH" 2>/dev/null || echo "")"
|
|
214
|
+
current="$(node "$BUILDER" --root "$REPO_ROOT" --hash --quiet 2>/dev/null || echo "")"
|
|
215
|
+
|
|
216
|
+
# If we could not compute the current hash, do not block the query.
|
|
217
|
+
[ -n "$current" ] || return 0
|
|
218
|
+
# Fresh: stored hash matches current corpus → use the persisted graph as-is.
|
|
219
|
+
[ "$stored" = "$current" ] && return 0
|
|
220
|
+
|
|
221
|
+
# Stale (mismatch, or missing builtFromHash on an older graph).
|
|
222
|
+
if [ "$STRICT_STALE" -eq 1 ]; then
|
|
223
|
+
die "stale-graph: persisted graph ($GRAPH) is out of date — spec corpus changed since it was built (run: node $BUILDER). --strict-stale set, refusing to auto-rebuild."
|
|
224
|
+
fi
|
|
225
|
+
echo "stale-graph: spec corpus changed since graph was built — rebuilding for this query" >&2
|
|
226
|
+
build_graph_to_temp
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
resolve_graph() {
|
|
230
|
+
if [ -n "$GRAPH_PATH" ]; then
|
|
231
|
+
[ -f "$GRAPH_PATH" ] || die "graph file not found: $GRAPH_PATH"
|
|
232
|
+
GRAPH="$GRAPH_PATH"
|
|
233
|
+
check_staleness
|
|
234
|
+
return
|
|
235
|
+
fi
|
|
236
|
+
if [ -f "$DEFAULT_GRAPH" ]; then
|
|
237
|
+
GRAPH="$DEFAULT_GRAPH"
|
|
238
|
+
check_staleness
|
|
239
|
+
return
|
|
240
|
+
fi
|
|
241
|
+
# Auto-build to a temp file (the default path is gitignored / may not exist).
|
|
242
|
+
# A freshly built graph is current by construction — no staleness check needed.
|
|
243
|
+
build_graph_to_temp
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
resolve_graph
|
|
247
|
+
|
|
248
|
+
# Validate the graph document shape early so jq filters can assume it.
|
|
249
|
+
jq -e '.graph.nodes and .graph.edges' "$GRAPH" >/dev/null 2>&1 \
|
|
250
|
+
|| die "not a valid spec-graph document (missing .graph.nodes/.graph.edges): $GRAPH"
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# Argument normalization to canonical node ids
|
|
254
|
+
# annotation -> ann:FORGE:<NAME>
|
|
255
|
+
# label -> label:workflow:<name>
|
|
256
|
+
# script -> script:<file>.sh
|
|
257
|
+
# command -> cmd:<name[:sub[:sub]]>
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
normalize_ann() {
|
|
261
|
+
# Accept: CONTRACT | FORGE:CONTRACT | ann:FORGE:CONTRACT
|
|
262
|
+
local a="$1"
|
|
263
|
+
case "$a" in
|
|
264
|
+
ann:FORGE:*) echo "$a" ;;
|
|
265
|
+
FORGE:*) echo "ann:$a" ;;
|
|
266
|
+
*) echo "ann:FORGE:$a" ;;
|
|
267
|
+
esac
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
normalize_cmd() {
|
|
271
|
+
# Accept: work-on | work-on:build:implement | cmd:work-on
|
|
272
|
+
local c="$1"
|
|
273
|
+
case "$c" in
|
|
274
|
+
cmd:*) echo "$c" ;;
|
|
275
|
+
*) echo "cmd:$c" ;;
|
|
276
|
+
esac
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# For `impact`, the node may be any type. Resolve to a canonical id by trying,
|
|
280
|
+
# in order: exact id match in the graph, then type-specific normalizers.
|
|
281
|
+
normalize_node() {
|
|
282
|
+
local n="$1"
|
|
283
|
+
# Already a fully-qualified id present in the graph?
|
|
284
|
+
if node_exists "$n"; then echo "$n"; return; fi
|
|
285
|
+
case "$n" in
|
|
286
|
+
ann:*|label:*|script:*|cmd:*|devdoc:*)
|
|
287
|
+
echo "$n" ;; # explicit prefix, trust it
|
|
288
|
+
FORGE:*)
|
|
289
|
+
echo "ann:$n" ;;
|
|
290
|
+
workflow:*)
|
|
291
|
+
echo "label:$n" ;;
|
|
292
|
+
*.sh)
|
|
293
|
+
echo "script:$n" ;;
|
|
294
|
+
*)
|
|
295
|
+
# Bare token: prefer annotation, then command, then label — whichever
|
|
296
|
+
# exists in the graph. Fall back to annotation form.
|
|
297
|
+
if node_exists "ann:FORGE:$n"; then echo "ann:FORGE:$n"
|
|
298
|
+
elif node_exists "cmd:$n"; then echo "cmd:$n"
|
|
299
|
+
elif node_exists "label:workflow:$n"; then echo "label:workflow:$n"
|
|
300
|
+
elif node_exists "script:$n"; then echo "script:$n"
|
|
301
|
+
else echo "ann:FORGE:$n"
|
|
302
|
+
fi
|
|
303
|
+
;;
|
|
304
|
+
esac
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
node_exists() {
|
|
308
|
+
jq -e --arg id "$1" 'any(.graph.nodes[]; .id == $id)' "$GRAPH" >/dev/null 2>&1
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
# Queries
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
# List .from of edges of a given type pointing at a target node. JSON array.
|
|
316
|
+
query_edge_from() {
|
|
317
|
+
local etype="$1" target="$2"
|
|
318
|
+
jq -c --arg t "$etype" --arg to "$target" \
|
|
319
|
+
'[.graph.edges[] | select(.type == $t and .to == $to) | .from] | unique' "$GRAPH"
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# All outbound edges from a command, grouped by edge type. JSON object.
|
|
323
|
+
query_deps() {
|
|
324
|
+
local from="$1"
|
|
325
|
+
jq -c --arg from "$from" '
|
|
326
|
+
[.graph.edges[] | select(.from == $from)]
|
|
327
|
+
| reduce .[] as $e ({}; .[$e.type] += [$e.to])
|
|
328
|
+
| map_values(unique)
|
|
329
|
+
' "$GRAPH"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Transitive reverse-reachability: everything that (transitively) depends on the
|
|
333
|
+
# seed node. Walk edges where .to is in the frontier, collecting .from, to a
|
|
334
|
+
# fixpoint. Excludes the seed. JSON array of node ids.
|
|
335
|
+
query_impact() {
|
|
336
|
+
local seed="$1"
|
|
337
|
+
jq -c --arg seed "$seed" '
|
|
338
|
+
.graph.edges as $edges
|
|
339
|
+
| def step(set):
|
|
340
|
+
( [ $edges[] | select(.to as $to | set | index($to)) | .from ] ) as $froms
|
|
341
|
+
| (set + $froms | unique) as $next
|
|
342
|
+
| if ($next | length) == (set | length) then set else step($next) end;
|
|
343
|
+
(step([$seed]) - [$seed]) | unique
|
|
344
|
+
' "$GRAPH"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Transitive FORWARD-reachability: the minimal spec set to read for a command.
|
|
348
|
+
# Seeds at the command node, then walks CONTAINS (sub-phases) and REQUIRES
|
|
349
|
+
# (devdocs) edges in the forward direction (.from in frontier -> collect .to) to
|
|
350
|
+
# a fixpoint. The resolved node ids are mapped to their repo-relative `.path`
|
|
351
|
+
# (nodes without a path — e.g. annotations — are dropped). Includes the seed
|
|
352
|
+
# command's own path. Output is a sorted, de-duplicated JSON array of paths.
|
|
353
|
+
#
|
|
354
|
+
# This is the inverse of query_impact (forward instead of reverse) and shares
|
|
355
|
+
# its cycle-safe length-equality termination, so a spurious self-CONTAINS edge
|
|
356
|
+
# cannot cause a non-terminating walk. Only CONTAINS/REQUIRES are followed —
|
|
357
|
+
# READS/WRITES/TRANSITIONS/INVOKES are NOT part of the spec-read set (annotations
|
|
358
|
+
# and labels have no spec file; scripts are resolved separately at invoke time).
|
|
359
|
+
query_load_set() {
|
|
360
|
+
local seed="$1"
|
|
361
|
+
jq -c --arg seed "$seed" '
|
|
362
|
+
[.graph.edges[] | select(.type == "CONTAINS" or .type == "REQUIRES")] as $edges
|
|
363
|
+
| (reduce .graph.nodes[] as $n ({}; .[$n.id] = ($n.path // null))) as $path
|
|
364
|
+
| def step(set):
|
|
365
|
+
( [ $edges[] | select(.from as $f | set | index($f)) | .to ] ) as $tos
|
|
366
|
+
| (set + $tos | unique) as $next
|
|
367
|
+
| if ($next | length) == (set | length) then set else step($next) end;
|
|
368
|
+
step([$seed])
|
|
369
|
+
| map($path[.] // empty)
|
|
370
|
+
| unique
|
|
371
|
+
' "$GRAPH"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Substring search over node id / name / path. JSON array of {id,type,name}.
|
|
375
|
+
query_search() {
|
|
376
|
+
local term="$1"
|
|
377
|
+
jq -c --arg term "$term" '
|
|
378
|
+
($term | ascii_downcase) as $t
|
|
379
|
+
| [ .graph.nodes[]
|
|
380
|
+
| select(
|
|
381
|
+
(.id | ascii_downcase | contains($t)) or
|
|
382
|
+
(.name | ascii_downcase | contains($t)) or
|
|
383
|
+
((.path // "") | ascii_downcase | contains($t))
|
|
384
|
+
)
|
|
385
|
+
| {id, type, name} ]
|
|
386
|
+
| sort_by(.id)
|
|
387
|
+
' "$GRAPH"
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Human-readable rendering
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
render_list_human() {
|
|
395
|
+
# stdin: JSON array of strings. Print one per line, or "(none)".
|
|
396
|
+
local title="$1"
|
|
397
|
+
local rows
|
|
398
|
+
rows="$(jq -r '.[]' 2>/dev/null || true)"
|
|
399
|
+
echo "$title"
|
|
400
|
+
if [ -z "$rows" ]; then
|
|
401
|
+
echo " (none)"
|
|
402
|
+
else
|
|
403
|
+
echo "$rows" | sed 's/^/ /'
|
|
404
|
+
fi
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
render_deps_human() {
|
|
408
|
+
# stdin: JSON object {EDGE_TYPE: [to, ...]}. Group by edge type.
|
|
409
|
+
local obj
|
|
410
|
+
obj="$(cat)"
|
|
411
|
+
if [ "$(echo "$obj" | jq 'length')" -eq 0 ]; then
|
|
412
|
+
echo "(no outbound edges)"
|
|
413
|
+
return
|
|
414
|
+
fi
|
|
415
|
+
echo "$obj" | jq -r 'to_entries[] | "\(.key):\n" + ([.value[] | " " + .] | join("\n"))'
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
render_search_human() {
|
|
419
|
+
# stdin: JSON array of {id,type,name}. Aligned 2-col table.
|
|
420
|
+
jq -r '.[] | "\(.type)\t\(.id)"' | column -t -s $'\t' 2>/dev/null \
|
|
421
|
+
|| jq -r '.[] | "\(.type) \(.id)"'
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# ---------------------------------------------------------------------------
|
|
425
|
+
# Dispatch
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
case "$SUBCMD" in
|
|
429
|
+
readers)
|
|
430
|
+
target="$(normalize_ann "$ARG")"
|
|
431
|
+
result="$(query_edge_from READS "$target")"
|
|
432
|
+
if [ "$HUMAN" -eq 1 ]; then
|
|
433
|
+
echo "$result" | render_list_human "Specs that READ $target:"
|
|
434
|
+
else
|
|
435
|
+
echo "$result"
|
|
436
|
+
fi
|
|
437
|
+
;;
|
|
438
|
+
writers)
|
|
439
|
+
target="$(normalize_ann "$ARG")"
|
|
440
|
+
result="$(query_edge_from WRITES "$target")"
|
|
441
|
+
if [ "$HUMAN" -eq 1 ]; then
|
|
442
|
+
echo "$result" | render_list_human "Specs that WRITE $target:"
|
|
443
|
+
else
|
|
444
|
+
echo "$result"
|
|
445
|
+
fi
|
|
446
|
+
;;
|
|
447
|
+
deps)
|
|
448
|
+
from="$(normalize_cmd "$ARG")"
|
|
449
|
+
node_exists "$from" || die "no command/spec node '$from' in the graph (try: graph-query.sh search ${ARG})"
|
|
450
|
+
result="$(query_deps "$from")"
|
|
451
|
+
if [ "$HUMAN" -eq 1 ]; then
|
|
452
|
+
echo "Dependencies of $from:"
|
|
453
|
+
echo "$result" | render_deps_human
|
|
454
|
+
else
|
|
455
|
+
echo "$result"
|
|
456
|
+
fi
|
|
457
|
+
;;
|
|
458
|
+
load-set)
|
|
459
|
+
from="$(normalize_cmd "$ARG")"
|
|
460
|
+
node_exists "$from" || die "no command/spec node '$from' in the graph (try: graph-query.sh search ${ARG})"
|
|
461
|
+
result="$(query_load_set "$from")"
|
|
462
|
+
if [ "$HUMAN" -eq 1 ]; then
|
|
463
|
+
echo "$result" | render_list_human "Minimal spec set for $from:"
|
|
464
|
+
else
|
|
465
|
+
echo "$result"
|
|
466
|
+
fi
|
|
467
|
+
;;
|
|
468
|
+
impact)
|
|
469
|
+
seed="$(normalize_node "$ARG")"
|
|
470
|
+
node_exists "$seed" || die "no node '$seed' in the graph (try: graph-query.sh search ${ARG})"
|
|
471
|
+
result="$(query_impact "$seed")"
|
|
472
|
+
if [ "$HUMAN" -eq 1 ]; then
|
|
473
|
+
echo "$result" | render_list_human "Impact (blast radius) of changing $seed:"
|
|
474
|
+
else
|
|
475
|
+
echo "$result"
|
|
476
|
+
fi
|
|
477
|
+
;;
|
|
478
|
+
search)
|
|
479
|
+
result="$(query_search "$ARG")"
|
|
480
|
+
if [ "$HUMAN" -eq 1 ]; then
|
|
481
|
+
echo "$result" | render_search_human
|
|
482
|
+
else
|
|
483
|
+
echo "$result"
|
|
484
|
+
fi
|
|
485
|
+
;;
|
|
486
|
+
esac
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# transition-label.sh — Workflow label state machine for ForgeDock pipeline
|
|
3
|
+
#
|
|
4
|
+
# Usage: transition-label.sh <ISSUE_NUMBER> <GH_FLAG...> <TARGET_STATE>
|
|
5
|
+
#
|
|
6
|
+
# ISSUE_NUMBER GitHub issue number (e.g. 674)
|
|
7
|
+
# GH_FLAG Repository flag passed to gh (e.g. -R RapierCraftStudios/forgedock)
|
|
8
|
+
# May be multiple tokens — pass before TARGET_STATE
|
|
9
|
+
# TARGET_STATE One of: investigating, ready-to-build, building, in-review,
|
|
10
|
+
# merged, invalid, decomposed
|
|
11
|
+
#
|
|
12
|
+
# Examples:
|
|
13
|
+
# transition-label.sh 674 -R RapierCraftStudios/forgedock investigating
|
|
14
|
+
# transition-label.sh 674 -R owner/repo ready-to-build
|
|
15
|
+
#
|
|
16
|
+
# Behavior:
|
|
17
|
+
# 1. Validates ISSUE_NUMBER and TARGET_STATE
|
|
18
|
+
# 2. Verifies the issue exists (exits 1 if not)
|
|
19
|
+
# 3. Adds workflow:{TARGET_STATE} label
|
|
20
|
+
# 4. Removes all other workflow:* labels atomically
|
|
21
|
+
#
|
|
22
|
+
# Exit codes: 0 = success, 1 = error (bad args, issue not found, gh failure)
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Argument parsing
|
|
28
|
+
# Signature: <ISSUE_NUMBER> [GH_FLAG...] <TARGET_STATE>
|
|
29
|
+
# We consume ISSUE_NUMBER as $1, TARGET_STATE as the last arg, everything
|
|
30
|
+
# in between is GH_FLAG (e.g. -R RapierCraftStudios/forgedock).
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
if [ "$#" -lt 2 ]; then
|
|
34
|
+
echo "ERROR: Usage: transition-label.sh <ISSUE_NUMBER> [GH_FLAG...] <TARGET_STATE>" >&2
|
|
35
|
+
echo " Example: transition-label.sh 674 -R RapierCraftStudios/forgedock investigating" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
ISSUE_NUMBER="$1"
|
|
40
|
+
shift
|
|
41
|
+
|
|
42
|
+
# Last arg is TARGET_STATE; everything remaining before it is GH_FLAG
|
|
43
|
+
# Build an array of all remaining args, then split off the last one.
|
|
44
|
+
ALL_REMAINING=("$@")
|
|
45
|
+
LAST_INDEX=$(( ${#ALL_REMAINING[@]} - 1 ))
|
|
46
|
+
TARGET_STATE="${ALL_REMAINING[$LAST_INDEX]}"
|
|
47
|
+
GH_ARGS=("${ALL_REMAINING[@]:0:$LAST_INDEX}")
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Validate ISSUE_NUMBER
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
53
|
+
echo "ERROR: ISSUE_NUMBER must be a positive integer, got: '$ISSUE_NUMBER'" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Valid workflow states (complete list — all workflow:* labels)
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
VALID_STATES=(
|
|
61
|
+
"investigating"
|
|
62
|
+
"ready-to-build"
|
|
63
|
+
"building"
|
|
64
|
+
"in-review"
|
|
65
|
+
"merged"
|
|
66
|
+
"invalid"
|
|
67
|
+
"decomposed"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Validate TARGET_STATE
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
VALID=0
|
|
74
|
+
for state in "${VALID_STATES[@]}"; do
|
|
75
|
+
if [ "$state" = "$TARGET_STATE" ]; then
|
|
76
|
+
VALID=1
|
|
77
|
+
break
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
if [ "$VALID" -eq 0 ]; then
|
|
82
|
+
echo "ERROR: Unknown target state: '$TARGET_STATE'" >&2
|
|
83
|
+
echo "Valid states:" >&2
|
|
84
|
+
for state in "${VALID_STATES[@]}"; do
|
|
85
|
+
echo " $state" >&2
|
|
86
|
+
done
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Verify issue exists before mutating labels
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
if ! gh issue view "$ISSUE_NUMBER" "${GH_ARGS[@]}" --json number >/dev/null 2>&1; then
|
|
94
|
+
echo "ERROR: Issue #$ISSUE_NUMBER not found (GH_FLAG: ${GH_ARGS[*]:-<none>})" >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Build remove list: all valid states except the target
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
REMOVE_LABELS=""
|
|
102
|
+
for state in "${VALID_STATES[@]}"; do
|
|
103
|
+
if [ "$state" != "$TARGET_STATE" ]; then
|
|
104
|
+
if [ -n "$REMOVE_LABELS" ]; then
|
|
105
|
+
REMOVE_LABELS="$REMOVE_LABELS,workflow:$state"
|
|
106
|
+
else
|
|
107
|
+
REMOVE_LABELS="workflow:$state"
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
# Export universal script environment so per-repo scripts can call back into universal scripts.
|
|
113
|
+
# Per-repo scripts (.forgedock/scripts/{operation}.sh) source these to delegate to universal ones.
|
|
114
|
+
export FORGEDOCK_SCRIPTS
|
|
115
|
+
FORGEDOCK_SCRIPTS="$(cd "$(dirname "$0")" && pwd)"
|
|
116
|
+
export FORGEDOCK_HOME
|
|
117
|
+
FORGEDOCK_HOME="$(cd "$(dirname "$0")/.." && pwd)"
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# FORGE_LABEL_MAP — optional env var (JSON object) from forge.yaml → learned.label_map
|
|
121
|
+
#
|
|
122
|
+
# If set and non-empty (not '{}'), look up "workflow:$TARGET_STATE" in the map.
|
|
123
|
+
# If a mapping exists, use the mapped label name instead of the canonical one.
|
|
124
|
+
# This allows repos with non-standard label naming to use ForgeDock without
|
|
125
|
+
# renaming their labels to match the canonical workflow:* format.
|
|
126
|
+
#
|
|
127
|
+
# Set by work-on.md Phase 0B.1: export FORGE_LABEL_MAP="$LEARNED_LABEL_MAP"
|
|
128
|
+
# Falls back to canonical label if: FORGE_LABEL_MAP is unset/empty/{}, jq
|
|
129
|
+
# is not available, or no mapping exists for the target state.
|
|
130
|
+
#
|
|
131
|
+
# Note: --remove-label always uses canonical workflow:* format — it clears all
|
|
132
|
+
# canonical labels regardless of mapping, which is correct cleanup behavior.
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
CANONICAL_LABEL="workflow:$TARGET_STATE"
|
|
135
|
+
EFFECTIVE_LABEL="$CANONICAL_LABEL"
|
|
136
|
+
|
|
137
|
+
if [ -n "${FORGE_LABEL_MAP:-}" ] && [ "${FORGE_LABEL_MAP:-}" != "{}" ]; then
|
|
138
|
+
if command -v jq >/dev/null 2>&1; then
|
|
139
|
+
MAPPED=$(echo "$FORGE_LABEL_MAP" | jq -r --arg key "$CANONICAL_LABEL" '.[$key] // empty' 2>/dev/null || true)
|
|
140
|
+
if [ -n "$MAPPED" ]; then
|
|
141
|
+
# Validate: reject any mapped value starting with '-' to prevent CLI flag injection.
|
|
142
|
+
# A forge.yaml learned.label_map entry like "workflow:investigating": "--json" would
|
|
143
|
+
# otherwise be passed directly to gh issue edit --add-label, interpreted as a flag.
|
|
144
|
+
if [[ "$MAPPED" == -* ]]; then
|
|
145
|
+
echo "ERROR: FORGE_LABEL_MAP value for '$CANONICAL_LABEL' is not a valid label name: '$MAPPED'" >&2
|
|
146
|
+
echo " Label names must not start with '-'. Using canonical label fallback: $CANONICAL_LABEL" >&2
|
|
147
|
+
else
|
|
148
|
+
EFFECTIVE_LABEL="$MAPPED"
|
|
149
|
+
echo "Label map override: $CANONICAL_LABEL → $EFFECTIVE_LABEL"
|
|
150
|
+
fi
|
|
151
|
+
fi
|
|
152
|
+
else
|
|
153
|
+
echo "WARNING: FORGE_LABEL_MAP is set but jq is not available — using canonical label ($CANONICAL_LABEL)" >&2
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Add target label
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
echo "Adding $EFFECTIVE_LABEL to issue #$ISSUE_NUMBER..."
|
|
161
|
+
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --add-label "$EFFECTIVE_LABEL"
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Remove all other workflow:* labels (best-effort — labels may not all exist)
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
echo "Removing stale workflow:* labels ($REMOVE_LABELS)..."
|
|
167
|
+
gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --remove-label "$REMOVE_LABELS" 2>/dev/null || true
|
|
168
|
+
|
|
169
|
+
echo "OK: $EFFECTIVE_LABEL set on issue #$ISSUE_NUMBER"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# validate-pr-target.sh — Hard-fail if PR base branch doesn't match classified lane
|
|
3
|
+
#
|
|
4
|
+
# Usage: validate-pr-target.sh <actual_base> <classified_lane>
|
|
5
|
+
# actual_base: the branch the PR will target (e.g. staging, milestone/deterministic-pipeline-v2)
|
|
6
|
+
# classified_lane: the lane produced by classify-lane.sh (e.g. staging, milestone/deterministic-pipeline-v2)
|
|
7
|
+
#
|
|
8
|
+
# Output: Structured finding (prefixed with severity)
|
|
9
|
+
# BLOCKING: <message> — mismatch detected; pipeline must not continue
|
|
10
|
+
# OK: <message> — base matches classified lane
|
|
11
|
+
#
|
|
12
|
+
# Exit codes: 0 = match (PR target is correct), 1 = mismatch (hard-fail)
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
if [ $# -ne 2 ]; then
|
|
17
|
+
echo "BLOCKING: validate-pr-target.sh requires exactly 2 arguments: <actual_base> <classified_lane>" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
ACTUAL_BASE="$1"
|
|
22
|
+
CLASSIFIED_LANE="$2"
|
|
23
|
+
|
|
24
|
+
if [ "$ACTUAL_BASE" = "$CLASSIFIED_LANE" ]; then
|
|
25
|
+
echo "OK: PR target '${ACTUAL_BASE}' matches classified lane"
|
|
26
|
+
exit 0
|
|
27
|
+
else
|
|
28
|
+
echo "BLOCKING: PR target '${ACTUAL_BASE}' does not match classified lane '${CLASSIFIED_LANE}'"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|