bigpowers 2.86.2 → 2.86.3
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/.agents/skills/wire-ci/SKILL.md +78 -59
- package/.codebuddy/skills/wire-ci/SKILL.md +78 -59
- package/.codex/skills/wire-ci/SKILL.md +78 -59
- package/.hermes/skills/wire-ci/SKILL.md +78 -59
- package/.kilocode/rules/wire-ci.md +78 -59
- package/.mimocode/skills/wire-ci/SKILL.md +78 -59
- package/.pi/package.json +1 -1
- package/.pi/prompts/wire-ci.md +78 -59
- package/.pi/skills/wire-ci/SKILL.md +78 -59
- package/.trae/skills/wire-ci/SKILL.md +78 -59
- package/.windsurf/rules/wire-ci.md +78 -59
- package/.zcode/skills/wire-ci/SKILL.md +78 -59
- package/CHANGELOG.md +14 -0
- package/SKILL-INDEX.md +1 -1
- package/package.json +1 -1
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
- package/scripts/lib/context-wire.sh +9 -0
- package/scripts/lib/detect-forge.sh +105 -0
- package/scripts/lib/extract-story-verify.py +102 -0
- package/scripts/lib/fail-open-detect.sh +67 -0
- package/scripts/lib/golden-suite-gates.sh +4 -0
- package/scripts/lib/plan-consistency-check.sh +26 -2
- package/scripts/lib/test-assertions.sh +11 -1
- package/scripts/run-skill-verify.sh +5 -46
- package/scripts/run-story-verify.sh +208 -0
- package/scripts/test-adapters.sh +14 -8
- package/scripts/verify-install.sh +13 -1
- package/scripts/wire-ci.sh +159 -0
- package/skills/wire-ci/REFERENCE.md +15 -4
- package/skills/wire-ci/SKILL.md +63 -55
- package/skills-lock.json +2 -2
|
@@ -25,47 +25,11 @@ SKILLS_ROOT="$REPO_ROOT"
|
|
|
25
25
|
PASS=0; FAIL=0; SKIP=0
|
|
26
26
|
TARGET="${1:-}"
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
# (head/wc/cat/tee/sort/tr/sed/awk/echo/true/:). `grep x | wc -l` is the
|
|
34
|
-
# canonical offender: wc always succeeds, so the grep result is discarded.
|
|
35
|
-
# Use an assertion instead, e.g. [ "$(... | wc -l)" -gt 0 ].
|
|
36
|
-
FAIL_OPEN_TAIL_CMDS='head|wc|cat|tee|sort|tr|sed|awk|echo|true|:'
|
|
37
|
-
|
|
38
|
-
is_fail_open_directive() {
|
|
39
|
-
local cmd="$1"
|
|
40
|
-
|
|
41
|
-
# Family 1: explicit exit-status swallowing.
|
|
42
|
-
if echo "$cmd" | grep -qE '\|\|[[:space:]]*(echo|true|:)|;[[:space:]]*(true|:|exit[[:space:]]+0)[[:space:]]*$'; then
|
|
43
|
-
return 0
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# Family 2: pipeline whose last stage never fails.
|
|
47
|
-
# Strip $(...) first — a pipe inside a command substitution feeds an
|
|
48
|
-
# assertion, e.g. [ "$(ls x | wc -l)" -gt 0 ], and is NOT fail-open.
|
|
49
|
-
local stripped="$cmd" prev=""
|
|
50
|
-
while [ "$stripped" != "$prev" ]; do
|
|
51
|
-
prev="$stripped"
|
|
52
|
-
stripped=$(printf '%s' "$stripped" | sed 's/\$([^()]*)/SUBST/g')
|
|
53
|
-
done
|
|
54
|
-
|
|
55
|
-
# Only inspect the segment after the final `|` that is not part of `||`.
|
|
56
|
-
local tail_seg
|
|
57
|
-
tail_seg=$(printf '%s' "$stripped" | sed 's/||/\
|
|
58
|
-
/g' | tail -1)
|
|
59
|
-
case "$tail_seg" in
|
|
60
|
-
*\|*)
|
|
61
|
-
tail_seg=${tail_seg##*|}
|
|
62
|
-
tail_seg=$(printf '%s' "$tail_seg" | sed 's/^[[:space:]]*//')
|
|
63
|
-
echo "$tail_seg" | grep -qE "^($FAIL_OPEN_TAIL_CMDS)([[:space:]]|$)" && return 0
|
|
64
|
-
;;
|
|
65
|
-
esac
|
|
66
|
-
|
|
67
|
-
return 1
|
|
68
|
-
}
|
|
28
|
+
# is_fail_open_directive / is_executable_verify now live in the shared detector,
|
|
29
|
+
# so tier-1 (here) and tier-2 (run-story-verify.sh, #106) reject the same
|
|
30
|
+
# idioms. A new evasion must be closed in one place, not two.
|
|
31
|
+
# shellcheck source=lib/fail-open-detect.sh
|
|
32
|
+
source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
|
|
69
33
|
|
|
70
34
|
normalize_verify_cmd() {
|
|
71
35
|
local raw="$1"
|
|
@@ -74,11 +38,6 @@ normalize_verify_cmd() {
|
|
|
74
38
|
echo "$raw"
|
|
75
39
|
}
|
|
76
40
|
|
|
77
|
-
is_executable_verify() {
|
|
78
|
-
local cmd="$1"
|
|
79
|
-
echo "$cmd" | grep -qE '^[a-zA-Z][a-zA-Z0-9_.-]*\b|^\['
|
|
80
|
-
}
|
|
81
|
-
|
|
82
41
|
run_verify_cmd() {
|
|
83
42
|
local skill="$1"
|
|
84
43
|
local cmd="$2"
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: BUG-2026-07-26-story-verify-never-executed
|
|
3
|
+
# Execute story-level verify: commands declared in specs/epics/*/epic.yaml.
|
|
4
|
+
# Tier 2 of the verify arc (GH #106). Tier 1 is run-skill-verify.sh.
|
|
5
|
+
#
|
|
6
|
+
# Only stories marked `status: done` are gated: a done story asserts its own
|
|
7
|
+
# acceptance criteria hold. Backlog/in-progress stories are reported as SKIP.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash scripts/run-story-verify.sh # gate all done stories
|
|
11
|
+
# bash scripts/run-story-verify.sh e80s05 # one story
|
|
12
|
+
# bash scripts/run-story-verify.sh --self-test # prove the gate can fail
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
17
|
+
cd "$REPO_ROOT"
|
|
18
|
+
|
|
19
|
+
# shellcheck source=lib/fail-open-detect.sh
|
|
20
|
+
source "$REPO_ROOT/scripts/lib/fail-open-detect.sh"
|
|
21
|
+
# shellcheck source=lib/python-env.sh
|
|
22
|
+
source "$REPO_ROOT/scripts/lib/python-env.sh"
|
|
23
|
+
resolve_python || exit 1
|
|
24
|
+
|
|
25
|
+
VERIFY_TIMEOUT_SECONDS=120
|
|
26
|
+
|
|
27
|
+
# Portable timeout: macOS lacks GNU timeout; use perl alarm as fallback.
|
|
28
|
+
# stdin is closed for every verify command: gate_all reads its record list on
|
|
29
|
+
# fd 0, and a child that reads stdin (any test-*.sh sourcing a read loop) would
|
|
30
|
+
# otherwise swallow the remaining records and silently shrink the gate.
|
|
31
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
32
|
+
run_with_timeout() { timeout "$VERIFY_TIMEOUT_SECONDS" bash -c "$1" </dev/null 2>&1; }
|
|
33
|
+
elif command -v perl >/dev/null 2>&1; then
|
|
34
|
+
run_with_timeout() { perl -e "alarm $VERIFY_TIMEOUT_SECONDS; exec @ARGV" bash -c "$1" </dev/null 2>&1; }
|
|
35
|
+
else
|
|
36
|
+
run_with_timeout() { bash -c "$1" </dev/null 2>&1; }
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
GATED_STATUS="done"
|
|
40
|
+
PASS=0; FAIL=0; SKIP=0
|
|
41
|
+
TARGET="${1:-}"
|
|
42
|
+
|
|
43
|
+
extract_records() {
|
|
44
|
+
"$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$REPO_ROOT"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Report a story whose directive can never fail. Kept separate from execution so
|
|
48
|
+
# a fail-open directive is a FAIL even when the command happens to exit 0.
|
|
49
|
+
reject_fail_open() {
|
|
50
|
+
local sid="$1" src="$2" cmd="$3"
|
|
51
|
+
echo "FAIL: $sid [$src] — fail-open directive (cannot exit non-zero): $cmd"
|
|
52
|
+
FAIL=$((FAIL + 1))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
reject_non_executable() {
|
|
56
|
+
local sid="$1" src="$2" cmd="$3"
|
|
57
|
+
echo "FAIL: $sid [$src] — verify is prose, not a runnable command: $cmd"
|
|
58
|
+
FAIL=$((FAIL + 1))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
run_story_verify() {
|
|
62
|
+
local sid="$1" src="$2" cmd="$3"
|
|
63
|
+
|
|
64
|
+
if ! is_executable_verify "$cmd"; then
|
|
65
|
+
reject_non_executable "$sid" "$src" "$cmd"
|
|
66
|
+
return 1
|
|
67
|
+
fi
|
|
68
|
+
if is_fail_open_directive "$cmd"; then
|
|
69
|
+
reject_fail_open "$sid" "$src" "$cmd"
|
|
70
|
+
return 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
local output
|
|
74
|
+
if output=$(run_with_timeout "$cmd"); then
|
|
75
|
+
echo "PASS: $sid"
|
|
76
|
+
PASS=$((PASS + 1))
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
echo "FAIL: $sid [$src] — $cmd"
|
|
80
|
+
echo " output: $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
81
|
+
FAIL=$((FAIL + 1))
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
gate_all() {
|
|
86
|
+
local sid src status cmd
|
|
87
|
+
while IFS=$'\t' read -r sid src status cmd; do
|
|
88
|
+
[ -z "${sid:-}" ] && continue
|
|
89
|
+
if [ -n "$TARGET" ] && [ "$sid" != "$TARGET" ]; then
|
|
90
|
+
continue
|
|
91
|
+
fi
|
|
92
|
+
if [ -z "$TARGET" ] && [ "$status" != "$GATED_STATUS" ]; then
|
|
93
|
+
echo "SKIP: $sid (status: $status)"
|
|
94
|
+
SKIP=$((SKIP + 1))
|
|
95
|
+
continue
|
|
96
|
+
fi
|
|
97
|
+
run_story_verify "$sid" "$src" "$cmd" || true
|
|
98
|
+
done < <(extract_records)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Anti-vacuity proof: seed a capsule whose done story must fail, and one that
|
|
102
|
+
# must pass, then require the runner to agree. A gate that cannot fail is the
|
|
103
|
+
# defect this script exists to close (#96/#106) — so it must prove otherwise.
|
|
104
|
+
run_self_test() {
|
|
105
|
+
echo "=== run-story-verify self-test ==="
|
|
106
|
+
local tmp_root
|
|
107
|
+
tmp_root=$(mktemp -d)
|
|
108
|
+
trap 'rm -rf "$tmp_root"' EXIT
|
|
109
|
+
|
|
110
|
+
mkdir -p "$tmp_root/specs/epics/e99-selftest"
|
|
111
|
+
cat > "$tmp_root/specs/epics/e99-selftest/epic.yaml" <<'YAML'
|
|
112
|
+
id: e99
|
|
113
|
+
title: self-test capsule
|
|
114
|
+
stories:
|
|
115
|
+
- id: e99s01
|
|
116
|
+
title: passing story
|
|
117
|
+
status: done
|
|
118
|
+
verify: test -d specs
|
|
119
|
+
- id: e99s02
|
|
120
|
+
title: failing story
|
|
121
|
+
status: done
|
|
122
|
+
verify: test -f /nonexistent/path/for/story-verify-self-test
|
|
123
|
+
- id: e99s03
|
|
124
|
+
title: fail-open story
|
|
125
|
+
status: done
|
|
126
|
+
verify: test -f /nonexistent || true
|
|
127
|
+
- id: e99s04
|
|
128
|
+
title: prose story
|
|
129
|
+
status: done
|
|
130
|
+
verify: node bin/bigpowers.js setup (interactive in TTY)
|
|
131
|
+
- id: e99s05
|
|
132
|
+
title: backlog story is not gated
|
|
133
|
+
status: backlog
|
|
134
|
+
verify: test -f /nonexistent/should/not/run
|
|
135
|
+
YAML
|
|
136
|
+
|
|
137
|
+
local records
|
|
138
|
+
records=$("$PYTHON" "$REPO_ROOT/scripts/lib/extract-story-verify.py" "$tmp_root")
|
|
139
|
+
|
|
140
|
+
local checked=0 problems=0
|
|
141
|
+
local sid src status cmd
|
|
142
|
+
while IFS=$'\t' read -r sid src status cmd; do
|
|
143
|
+
[ -z "${sid:-}" ] && continue
|
|
144
|
+
checked=$((checked + 1))
|
|
145
|
+
case "$sid" in
|
|
146
|
+
e99s01)
|
|
147
|
+
if is_fail_open_directive "$cmd" || ! is_executable_verify "$cmd"; then
|
|
148
|
+
echo "FAIL: self-test — genuine assertion rejected: $cmd"; problems=$((problems + 1))
|
|
149
|
+
elif ! run_with_timeout "$cmd" >/dev/null; then
|
|
150
|
+
echo "FAIL: self-test — passing fixture did not pass"; problems=$((problems + 1))
|
|
151
|
+
fi
|
|
152
|
+
;;
|
|
153
|
+
e99s02)
|
|
154
|
+
if run_with_timeout "$cmd" >/dev/null; then
|
|
155
|
+
echo "FAIL: self-test — failing fixture exited 0 (gate cannot fail)"; problems=$((problems + 1))
|
|
156
|
+
fi
|
|
157
|
+
;;
|
|
158
|
+
e99s03)
|
|
159
|
+
if ! is_fail_open_directive "$cmd"; then
|
|
160
|
+
echo "FAIL: self-test — fail-open directive not detected: $cmd"; problems=$((problems + 1))
|
|
161
|
+
fi
|
|
162
|
+
;;
|
|
163
|
+
e99s04)
|
|
164
|
+
if is_executable_verify "$cmd"; then
|
|
165
|
+
echo "FAIL: self-test — prose accepted as a command: $cmd"; problems=$((problems + 1))
|
|
166
|
+
fi
|
|
167
|
+
;;
|
|
168
|
+
e99s05)
|
|
169
|
+
if [ "$status" = "$GATED_STATUS" ]; then
|
|
170
|
+
echo "FAIL: self-test — backlog story reported as done"; problems=$((problems + 1))
|
|
171
|
+
fi
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
done <<EOF
|
|
175
|
+
$records
|
|
176
|
+
EOF
|
|
177
|
+
|
|
178
|
+
rm -rf "$tmp_root"
|
|
179
|
+
trap - EXIT
|
|
180
|
+
|
|
181
|
+
if [ "$checked" -lt 5 ]; then
|
|
182
|
+
echo "FAIL: self-test — extractor found $checked/5 fixture stories"
|
|
183
|
+
return 1
|
|
184
|
+
fi
|
|
185
|
+
if [ "$problems" -gt 0 ]; then
|
|
186
|
+
echo "FAIL: self-test — $problems assertion(s) failed"
|
|
187
|
+
return 1
|
|
188
|
+
fi
|
|
189
|
+
echo "PASS: self-test — gate fails on broken stories, passes on sound ones"
|
|
190
|
+
return 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if [ "$TARGET" = "--self-test" ]; then
|
|
194
|
+
run_self_test
|
|
195
|
+
exit $?
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
gate_all
|
|
199
|
+
|
|
200
|
+
echo ""
|
|
201
|
+
echo "Results: $PASS PASS, $FAIL FAIL, $SKIP SKIP"
|
|
202
|
+
|
|
203
|
+
if [ -n "$TARGET" ] && [ "$((PASS + FAIL))" -eq 0 ]; then
|
|
204
|
+
echo "ERROR: no story matching '$TARGET' declares a verify: command" >&2
|
|
205
|
+
exit 2
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
[ "$FAIL" -eq 0 ]
|
package/scripts/test-adapters.sh
CHANGED
|
@@ -21,7 +21,6 @@ bash "$SCRIPT_DIR/validate-targets-yaml.sh" >/dev/null
|
|
|
21
21
|
|
|
22
22
|
PASS=0
|
|
23
23
|
FAIL=0
|
|
24
|
-
TMPDIR=""
|
|
25
24
|
TA_PASS=0
|
|
26
25
|
TA_FAIL=0
|
|
27
26
|
TA_TMPDIR=""
|
|
@@ -56,15 +55,22 @@ test_adapter() {
|
|
|
56
55
|
if [[ "$has_context" == "true" && "$mode" != "native" ]]; then
|
|
57
56
|
declare -f wire_context >/dev/null 2>&1 && ta_pass "$id: wire_context defined" || ta_fail "$id: wire_context missing"
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$
|
|
61
|
-
(
|
|
62
|
-
|
|
58
|
+
TA_TMPDIR=$(mktemp -d)
|
|
59
|
+
cp "$SCRIPT_DIR/../docs/templates/AGENTS.md" "$TA_TMPDIR/AGENTS.md" 2>/dev/null || echo "# test" > "$TA_TMPDIR/AGENTS.md"
|
|
60
|
+
# The idempotency probe runs in a subshell (it cd's away), so its ta_pass/
|
|
61
|
+
# ta_fail would increment counters in a child and be lost — the script used
|
|
62
|
+
# to print 5 PASS lines and report 4. Capture the status, tally in-process.
|
|
63
|
+
if (
|
|
64
|
+
cd "$TA_TMPDIR"
|
|
63
65
|
# shellcheck source=/dev/null
|
|
64
66
|
source "$adapter_file"
|
|
65
|
-
wire_context
|
|
66
|
-
wire_context
|
|
67
|
-
)
|
|
67
|
+
wire_context >/dev/null 2>&1 || true
|
|
68
|
+
wire_context >/dev/null 2>&1
|
|
69
|
+
); then
|
|
70
|
+
ta_pass "$id: idempotent wire_context"
|
|
71
|
+
else
|
|
72
|
+
ta_fail "$id: wire_context failed"
|
|
73
|
+
fi
|
|
68
74
|
fi
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -168,7 +168,19 @@ grep -q "'antigravity'" "$REPO_ROOT/bin/setup.js" && grep -q 'SUPPORTED_IDS' "$R
|
|
|
168
168
|
grep -q "'agy'" "$REPO_ROOT/bin/setup.js" && ta_pass "setup.js: agy supported" || ta_fail "setup.js: agy not in SUPPORTED_IDS"
|
|
169
169
|
grep -q "case 'antigravity'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: antigravity case" || ta_fail "install-helpers: missing antigravity case"
|
|
170
170
|
grep -q "case 'agy'" "$REPO_ROOT/scripts/lib/install-helpers.js" && ta_pass "install-helpers: agy case" || ta_fail "install-helpers: missing agy case"
|
|
171
|
-
|
|
171
|
+
# Scope this to install_agy's OWN body. The previous form piped a herestring into
|
|
172
|
+
# install_grep(), which is `grep "$@" $(install_grep_paths)` — it always greps the
|
|
173
|
+
# install files and ignores stdin, so the assertion actually searched every target.
|
|
174
|
+
# install_gemini/install_claude legitimately use .gemini/extensions/bigpowers
|
|
175
|
+
# (install-targets-a.sh:21,22,70), so the check could never pass. (#106)
|
|
176
|
+
AGY_BODY="$(sed -n '/^install_agy()/,/^}/p' $(install_grep_paths) 2>/dev/null)"
|
|
177
|
+
if [[ -z "$AGY_BODY" ]]; then
|
|
178
|
+
ta_fail "source: install_agy() body not found — assertion would pass vacuously"
|
|
179
|
+
elif printf '%s' "$AGY_BODY" | grep -q '\.gemini/extensions/bigpowers'; then
|
|
180
|
+
ta_fail "source: agy touches gemini extension path"
|
|
181
|
+
else
|
|
182
|
+
ta_pass "source: agy avoids gemini extension path"
|
|
183
|
+
fi
|
|
172
184
|
|
|
173
185
|
install_grep -q 'install_codex()' && ta_pass "source: install_codex()" || ta_fail "source: missing install_codex()"
|
|
174
186
|
install_grep -q 'uninstall_codex()' && ta_pass "source: uninstall_codex()" || ta_fail "source: missing uninstall_codex()"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: GH-104-forge-neutrality
|
|
3
|
+
# wire-ci.sh — resolve forge + stack and place a CI template, or skip honestly.
|
|
4
|
+
#
|
|
5
|
+
# GH #104: wire-ci declared itself a HARD GATE, then generated GitHub Actions
|
|
6
|
+
# config copied from one person's personal repository. On GitLab it could not
|
|
7
|
+
# work at all. This runner makes the template source configurable and turns an
|
|
8
|
+
# unsupported forge into an explicit skip instead of a false guarantee.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# bash scripts/wire-ci.sh --detect # report forge + stack, write nothing
|
|
12
|
+
# bash scripts/wire-ci.sh --plan # show what would be written
|
|
13
|
+
# bash scripts/wire-ci.sh --apply # write the workflow
|
|
14
|
+
# bash scripts/wire-ci.sh --self-test # prove skip/apply paths behave
|
|
15
|
+
#
|
|
16
|
+
# Exit codes: 0 applied or cleanly skipped, 1 error, 3 unsupported forge.
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
21
|
+
# shellcheck source=lib/detect-forge.sh
|
|
22
|
+
source "$REPO_ROOT/scripts/lib/detect-forge.sh"
|
|
23
|
+
|
|
24
|
+
EXIT_UNSUPPORTED_FORGE=3
|
|
25
|
+
MODE="${1:---detect}"
|
|
26
|
+
TARGET_DIR="${WIRE_CI_TARGET:-$REPO_ROOT}"
|
|
27
|
+
|
|
28
|
+
detect_stack() {
|
|
29
|
+
local root="$1"
|
|
30
|
+
if [[ -f "$root/Cargo.toml" ]]; then echo "rust"
|
|
31
|
+
elif [[ -f "$root/go.mod" ]]; then echo "go"
|
|
32
|
+
elif [[ -f "$root/pyproject.toml" || -f "$root/setup.py" ]]; then echo "python"
|
|
33
|
+
elif [[ -f "$root/package.json" ]]; then echo "node"
|
|
34
|
+
else echo "unknown"
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
template_for() {
|
|
39
|
+
local forge="$1" stack="$2"
|
|
40
|
+
printf '%s/%s/test-build-release-%s.yml' "$(forge_template_root)" "$forge" "$stack"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# The honest-degradation path. An unsupported forge gets a named reason and a
|
|
44
|
+
# non-zero-but-distinct exit code, never a GitHub workflow it cannot run.
|
|
45
|
+
report_unsupported() {
|
|
46
|
+
local forge="$1"
|
|
47
|
+
echo "wire-ci: SKIP — no CI templates for forge '$forge'"
|
|
48
|
+
echo " Supported: $FORGE_SUPPORTED_LIST"
|
|
49
|
+
echo " wire-ci is NOT a hard gate on this forge; it cannot generate config it does not ship."
|
|
50
|
+
echo " Options:"
|
|
51
|
+
echo " - point at your own templates: BIGPOWERS_CI_TEMPLATES=/path/to/templates"
|
|
52
|
+
echo " - pin the forge explicitly: echo 'forge: github' > specs/forge.yaml"
|
|
53
|
+
echo " - write CI by hand for $(forge_ci_path "$forge" || true)"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
resolve_and_report() {
|
|
57
|
+
local root="$1"
|
|
58
|
+
FORGE=$(detect_forge "$root")
|
|
59
|
+
STACK=$(detect_stack "$root")
|
|
60
|
+
echo "wire-ci: forge=$FORGE stack=$STACK"
|
|
61
|
+
|
|
62
|
+
if ! forge_is_supported "$FORGE"; then
|
|
63
|
+
report_unsupported "$FORGE"
|
|
64
|
+
return "$EXIT_UNSUPPORTED_FORGE"
|
|
65
|
+
fi
|
|
66
|
+
if [[ "$STACK" == "unknown" ]]; then
|
|
67
|
+
echo "wire-ci: SKIP — no recognized manifest (Cargo.toml, go.mod, pyproject.toml, package.json)"
|
|
68
|
+
return "$EXIT_UNSUPPORTED_FORGE"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
TEMPLATE=$(template_for "$FORGE" "$STACK")
|
|
72
|
+
if [[ ! -f "$TEMPLATE" ]]; then
|
|
73
|
+
echo "wire-ci: SKIP — no bundled template at $TEMPLATE"
|
|
74
|
+
return "$EXIT_UNSUPPORTED_FORGE"
|
|
75
|
+
fi
|
|
76
|
+
echo "wire-ci: template=$TEMPLATE"
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
apply_template() {
|
|
81
|
+
local root="$1"
|
|
82
|
+
local dest_dir="$root/$(forge_ci_path "$FORGE")"
|
|
83
|
+
mkdir -p "$dest_dir"
|
|
84
|
+
cp "$TEMPLATE" "$dest_dir/test-build-release.yml"
|
|
85
|
+
echo "wire-ci: wrote $dest_dir/test-build-release.yml"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
run_self_test() {
|
|
89
|
+
echo "=== wire-ci self-test ==="
|
|
90
|
+
local tmp problems=0
|
|
91
|
+
tmp=$(mktemp -d)
|
|
92
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
93
|
+
|
|
94
|
+
# An unsupported forge must skip, not emit GitHub Actions config.
|
|
95
|
+
mkdir -p "$tmp/gitlab-project"
|
|
96
|
+
echo '{}' > "$tmp/gitlab-project/package.json"
|
|
97
|
+
echo 'forge: gitlab' > /dev/null
|
|
98
|
+
mkdir -p "$tmp/gitlab-project/specs"
|
|
99
|
+
printf 'forge: gitlab\n' > "$tmp/gitlab-project/specs/forge.yaml"
|
|
100
|
+
local out rc
|
|
101
|
+
out=$(BIGPOWERS_FORGE="" resolve_and_report "$tmp/gitlab-project" 2>&1); rc=$?
|
|
102
|
+
if [[ "$rc" -ne "$EXIT_UNSUPPORTED_FORGE" ]]; then
|
|
103
|
+
echo "FAIL: self-test — gitlab project did not skip (rc=$rc)"; problems=$((problems + 1))
|
|
104
|
+
fi
|
|
105
|
+
if printf '%s' "$out" | grep -q 'HARD GATE'; then
|
|
106
|
+
echo "FAIL: self-test — skip path still claims HARD GATE"; problems=$((problems + 1))
|
|
107
|
+
fi
|
|
108
|
+
if [[ -d "$tmp/gitlab-project/.github" ]]; then
|
|
109
|
+
echo "FAIL: self-test — wrote .github/ for a non-GitHub forge"; problems=$((problems + 1))
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# A supported forge must resolve to a bundled template — proving the runner
|
|
113
|
+
# does not depend on any external repository being reachable.
|
|
114
|
+
mkdir -p "$tmp/gh-project/specs"
|
|
115
|
+
echo '{}' > "$tmp/gh-project/package.json"
|
|
116
|
+
printf 'forge: github\n' > "$tmp/gh-project/specs/forge.yaml"
|
|
117
|
+
if ! out=$(resolve_and_report "$tmp/gh-project" 2>&1); then
|
|
118
|
+
echo "FAIL: self-test — github/node project did not resolve: $out"; problems=$((problems + 1))
|
|
119
|
+
elif ! printf '%s' "$out" | grep -q 'test-build-release-node.yml'; then
|
|
120
|
+
echo "FAIL: self-test — resolved the wrong template: $out"; problems=$((problems + 1))
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# The default template root must live inside the package, not in a user's home
|
|
124
|
+
# and not in the project being wired. Guards the #104 regression directly.
|
|
125
|
+
local root
|
|
126
|
+
root=$(forge_template_root)
|
|
127
|
+
case "$root" in
|
|
128
|
+
"$REPO_ROOT"/*) : ;;
|
|
129
|
+
*) echo "FAIL: self-test — default template root escapes the package: $root"; problems=$((problems + 1)) ;;
|
|
130
|
+
esac
|
|
131
|
+
if printf '%s' "$root" | grep -q 'danielvm-git\|~/Developer/.github'; then
|
|
132
|
+
echo "FAIL: self-test — template root still points at a personal repository"; problems=$((problems + 1))
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
rm -rf "$tmp"
|
|
136
|
+
trap - EXIT
|
|
137
|
+
|
|
138
|
+
if [[ "$problems" -gt 0 ]]; then
|
|
139
|
+
echo "FAIL: wire-ci self-test — $problems problem(s)"
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
142
|
+
echo "PASS: wire-ci self-test — unsupported forges skip, supported forges resolve bundled templates"
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case "$MODE" in
|
|
147
|
+
--self-test) run_self_test; exit $? ;;
|
|
148
|
+
--detect|--plan)
|
|
149
|
+
resolve_and_report "$TARGET_DIR"; exit $?
|
|
150
|
+
;;
|
|
151
|
+
--apply)
|
|
152
|
+
resolve_and_report "$TARGET_DIR" || exit $?
|
|
153
|
+
apply_template "$TARGET_DIR"
|
|
154
|
+
;;
|
|
155
|
+
*)
|
|
156
|
+
echo "wire-ci: unknown mode '$MODE' (expected --detect, --plan, --apply, --self-test)" >&2
|
|
157
|
+
exit 1
|
|
158
|
+
;;
|
|
159
|
+
esac
|
|
@@ -16,19 +16,26 @@
|
|
|
16
16
|
### Create CI for a Go project (TBR + optional deploy)
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
# Resolve forge + stack, then apply the bundled template
|
|
20
|
+
bash scripts/wire-ci.sh --detect
|
|
21
|
+
bash scripts/wire-ci.sh --apply
|
|
22
22
|
|
|
23
23
|
wire-ci --validate
|
|
24
24
|
wire-ci --dry-run
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
To use your own org templates instead of the bundled ones:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
BIGPOWERS_CI_TEMPLATES=/path/to/your/templates bash scripts/wire-ci.sh --apply
|
|
31
|
+
```
|
|
32
|
+
|
|
27
33
|
### Create CI for a CLI tool (TBR only, no deploy)
|
|
28
34
|
|
|
29
35
|
```bash
|
|
30
|
-
|
|
36
|
+
bash scripts/wire-ci.sh --apply
|
|
31
37
|
# Edit release job to download build artifact — see big-release dogfood
|
|
38
|
+
# CLI/library repos: delete deploy.yml; the release job is terminal.
|
|
32
39
|
|
|
33
40
|
wire-ci --validate
|
|
34
41
|
```
|
|
@@ -172,6 +179,10 @@ jobs:
|
|
|
172
179
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
173
180
|
run-id: ${{ github.event.workflow_run.id }}
|
|
174
181
|
path: deploy-meta
|
|
182
|
+
# BigBase-specific composite action, not a bigpowers default: this deploy
|
|
183
|
+
# example targets the author's own hosting stack. Replace it with your
|
|
184
|
+
# platform's deploy step (or drop the deploy job entirely for CLI and
|
|
185
|
+
# library repos). bigpowers itself ships no deploy templates. (GH #104)
|
|
175
186
|
- uses: danielvm-git/.github/actions/bigbase-deploy@9c56ac10c629f3baa110ddb19136579e3c90d690 # v1
|
|
176
187
|
with:
|
|
177
188
|
site_id: ${{ secrets.BIGBASE_SITE_ID }}
|