@jenga-ai/agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +340 -0
- package/agents/ai_engineer.md +113 -0
- package/agents/developer.md +236 -0
- package/agents/scrum-master.md +349 -0
- package/agents/scrutiny-agent.md +137 -0
- package/agents/solution-assessor.md +185 -0
- package/agents/tester.md +339 -0
- package/bin/jenga.js +70 -0
- package/hooks/copilot_session_end.sh +29 -0
- package/hooks/on_session_end.sh +238 -0
- package/hooks/prompt_router.sh +11 -0
- package/hooks/prompt_router_helper.js +52 -0
- package/hooks/session_end_helper.js +29 -0
- package/hooks/session_end_watcher.sh +24 -0
- package/lib/commands/attach.js +47 -0
- package/lib/commands/init.js +207 -0
- package/lib/commands/start.js +16 -0
- package/lib/commands/status.js +53 -0
- package/lib/config-schema.js +72 -0
- package/lib/inject-settings.js +61 -0
- package/lib/mirror.js +244 -0
- package/lib/resolve-project-dir.sh +47 -0
- package/mcp/execute-ticket/index.js +10 -0
- package/mcp/execute-ticket/package.json +5 -0
- package/mcp/help/index.js +79 -0
- package/mcp/help/package.json +14 -0
- package/mcp/router/README.md +19 -0
- package/mcp/router/embedder.js +23 -0
- package/mcp/router/index.js +204 -0
- package/mcp/router/matcher.js +87 -0
- package/mcp/router/package-lock.json +1048 -0
- package/mcp/router/package.json +11 -0
- package/mcp/router/skill-index.js +104 -0
- package/package.json +47 -0
- package/scripts/board_resolver.sh +46 -0
- package/scripts/e25_s01_extract_board_graph.py +292 -0
- package/scripts/e25_s01_generate_synthetic_board.py +90 -0
- package/scripts/measurement-10x.json +50 -0
- package/scripts/measurement-10x.txt +4 -0
- package/scripts/measurement-real.json +50 -0
- package/scripts/measurement-real.txt +4 -0
- package/scripts/postinstall.js +165 -0
- package/scripts/todo_cleanup.sh +22 -0
- package/scripts/todo_manager.sh +86 -0
- package/scripts/validate-board.sh +190 -0
- package/scripts/validate-story-format.sh +53 -0
- package/skills/brainstorm/SKILL.md +47 -0
- package/skills/btw/SKILL.md +42 -0
- package/skills/commit/SKILL.md +29 -0
- package/skills/commit/assets/user_instructions_template.md +22 -0
- package/skills/continue/SKILL.md +29 -0
- package/skills/convert/SKILL.md +124 -0
- package/skills/convert/convert_cli.py +235 -0
- package/skills/convert/tests/sample.csv +4 -0
- package/skills/convert/tests/sample.json +5 -0
- package/skills/convert/tests/sample.jsonl +3 -0
- package/skills/convert/tests/sample.yaml +18 -0
- package/skills/convert/tests/sample_obj.csv +2 -0
- package/skills/convert/tests/sample_obj.json +9 -0
- package/skills/deep-dive/SKILL.md +167 -0
- package/skills/do/SKILL.md +88 -0
- package/skills/do/assets/sender_template.json +12 -0
- package/skills/doc/SKILL.md +314 -0
- package/skills/doc/assets/path-objectives.yaml +38 -0
- package/skills/doc-sync/SKILL.md +167 -0
- package/skills/doc-sync/assets/default_excludes.txt +21 -0
- package/skills/doc-sync/assets/doc_targets.md +14 -0
- package/skills/dooo/SKILL.md +60 -0
- package/skills/error/SKILL.md +29 -0
- package/skills/evaluate/SKILL.md +45 -0
- package/skills/evaluate/assets/evaluation_invokation_template.yml +3 -0
- package/skills/evaluate/assets/evaluation_rapport_template.md +24 -0
- package/skills/examplify/SKILL.md +42 -0
- package/skills/help/SKILL.md +36 -0
- package/skills/improve/SKILL.md +55 -0
- package/skills/index/scripts/board-index +4 -0
- package/skills/index/scripts/board_index.py +615 -0
- package/skills/index/scripts/smoke_test.sh +86 -0
- package/skills/init/SKILL.md +44 -0
- package/skills/init/assets/.gitignore_template +15 -0
- package/skills/init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/init/assets/directory_structure.txt +13 -0
- package/skills/init/assets/test-config_template.json +4 -0
- package/skills/init/assets/workflow_template.json +30 -0
- package/skills/init/scripts/init.sh +48 -0
- package/skills/jbp/SKILL.md +25 -0
- package/skills/jenga/SKILL.md +68 -0
- package/skills/lgtm/SKILL.md +21 -0
- package/skills/mirror-public/SKILL.md +237 -0
- package/skills/mirror-public/assets/config.json +5 -0
- package/skills/mirror-public/scripts/mirror.sh +374 -0
- package/skills/pi-plan/SKILL.md +62 -0
- package/skills/pi-plan/assets/epic.json +7 -0
- package/skills/pi-plan/assets/story_template.md +18 -0
- package/skills/proceed/SKILL.md +29 -0
- package/skills/publish/SKILL.md +351 -0
- package/skills/publish/adapters/droplet.md +200 -0
- package/skills/publish/adapters/mobile-ios.md +114 -0
- package/skills/publish/adapters/npm-ci.md +223 -0
- package/skills/publish/adapters/npm.md +121 -0
- package/skills/publish/assets/ExportOptions.plist.template +19 -0
- package/skills/publish/assets/ci-contract.md +111 -0
- package/skills/publish/assets/ownership-matrix.md +17 -0
- package/skills/publish/assets/publish.example.json +85 -0
- package/skills/publish/assets/publish.example.npm-ci.json +40 -0
- package/skills/publish/assets/publish.example.npm.json +41 -0
- package/skills/publish/assets/secrets-guide.md +104 -0
- package/skills/publish/schemas/fixtures/npm-ci-minimal.json +17 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-empty-secrets.json +18 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-workflow-path.json +18 -0
- package/skills/publish/schemas/publish.schema.json +428 -0
- package/skills/publish/scripts/check_target_config.sh +96 -0
- package/skills/publish/scripts/droplet_pipeline.sh +208 -0
- package/skills/publish/scripts/generate_release_notes.sh +200 -0
- package/skills/publish/scripts/ios_pipeline.sh +486 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +225 -0
- package/skills/publish/scripts/npm_pipeline.sh +249 -0
- package/skills/publish/scripts/publish_common.sh +253 -0
- package/skills/publish/scripts/publish_deploy.sh +538 -0
- package/skills/publish/scripts/reconcile_tags.sh +135 -0
- package/skills/publish/scripts/run_gates.sh +616 -0
- package/skills/publish/scripts/setup_wizard.sh +394 -0
- package/skills/publish/scripts/show_history.sh +95 -0
- package/skills/publish/scripts/suggest_semver_bump.sh +105 -0
- package/skills/publish/scripts/validate_config.sh +163 -0
- package/skills/publish/scripts/validate_droplet_env.sh +45 -0
- package/skills/publish/scripts/validate_ios_env.sh +68 -0
- package/skills/publish/scripts/validate_npm_ci_env.sh +71 -0
- package/skills/publish/scripts/validate_npm_env.sh +22 -0
- package/skills/publish/scripts/write_ledger_entry.sh +126 -0
- package/skills/publish/wizards/droplet.md +275 -0
- package/skills/publish/wizards/mobile-ios.md +157 -0
- package/skills/publish/wizards/npm-ci.md +240 -0
- package/skills/publish/wizards/npm.md +224 -0
- package/skills/reconcile/SKILL.md +93 -0
- package/skills/reconcile/assets/report_format.md +44 -0
- package/skills/reconcile-origin/SKILL.md +75 -0
- package/skills/reconcile-origin/scripts/reconcile-origin.sh +372 -0
- package/skills/redo/SKILL.md +70 -0
- package/skills/route/SKILL.md +180 -0
- package/skills/self-sync/SKILL.md +73 -0
- package/skills/self-sync/scripts/run.js +136 -0
- package/skills/skillify/SKILL.md +68 -0
- package/skills/skillify/assets/init-new/SKILL.md +35 -0
- package/skills/skillify/assets/init-new/assets/.gitignore_template +15 -0
- package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +10 -0
- package/skills/skillify/assets/init-new/assets/test-config_template.json +4 -0
- package/skills/skillify/assets/init-new/assets/workflow_template.json +17 -0
- package/skills/skillify/assets/init-new/scripts/init.sh +48 -0
- package/skills/skillify/assets/init-old/SKILL.md +124 -0
- package/skills/spinoff/SKILL.md +48 -0
- package/skills/status/SKILL.md +33 -0
- package/skills/status/assets/output_format.md +41 -0
- package/skills/todo/SKILL.md +46 -0
- package/skills/todo/assets/todo_handoff_template.md +22 -0
- package/skills/todo/assets/todo_template.md +3 -0
- package/skills/train/SKILL.md +116 -0
- package/skills/train/assets/dashboard-templates/classifiers.html +106 -0
- package/skills/train/assets/dashboard-templates/nlp.html +102 -0
- package/skills/train/assets/dashboard-templates/transformers.html +98 -0
- package/skills/train/assets/results-parsers/__init__.py +9 -0
- package/skills/train/assets/results-parsers/classifiers.py +84 -0
- package/skills/train/assets/results-parsers/nlp.py +88 -0
- package/skills/train/assets/results-parsers/reporter.py +154 -0
- package/skills/train/assets/results-parsers/transformers.py +120 -0
- package/skills/train/train_cli.py +786 -0
- package/templates/EXECUTION_PLAN_TEMPLATE.md +43 -0
- package/templates/EXECUTION_SUMMARY_TEMPLATE.md +50 -0
- package/templates/JENGA_CONFIG_TEMPLATE.json +23 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +88 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +311 -0
- package/templates/SKILL.md +16 -0
- package/templates/SKILL_TEMPLATE.md +28 -0
- package/templates/USER_INSTRUCTIONS_TEMPLATE.md +22 -0
- package/templates/copilot-instructions.md.tpl +55 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# /publish quality gate runner
|
|
5
|
+
#
|
|
6
|
+
# Invocation:
|
|
7
|
+
# run_gates.sh <phase> <target> <publish_json_path> [--non-interactive]
|
|
8
|
+
#
|
|
9
|
+
# Arguments:
|
|
10
|
+
# <phase> pre | post
|
|
11
|
+
# <target> target name from publish.json
|
|
12
|
+
# <publish_json_path> path to publish.json
|
|
13
|
+
# --non-interactive disable prompts; any gate failure aborts immediately
|
|
14
|
+
#
|
|
15
|
+
# Exit codes:
|
|
16
|
+
# 0 all required gates passed (or were intentionally skipped with warnings)
|
|
17
|
+
# 2 one or more gates failed
|
|
18
|
+
|
|
19
|
+
usage() {
|
|
20
|
+
cat <<'USAGE'
|
|
21
|
+
Usage: run_gates.sh <phase> <target> <publish_json_path> [--non-interactive]
|
|
22
|
+
USAGE
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
if REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
|
|
27
|
+
:
|
|
28
|
+
else
|
|
29
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
30
|
+
fi
|
|
31
|
+
PUBLISH_HISTORY="$REPO_ROOT/project/logs/publish-history.json"
|
|
32
|
+
CUSTOM_SCRIPT_PATTERN='^[./a-zA-Z0-9_-]+$'
|
|
33
|
+
|
|
34
|
+
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
|
35
|
+
usage >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
PHASE="$1"
|
|
40
|
+
TARGET_NAME="$2"
|
|
41
|
+
PUBLISH_JSON="$3"
|
|
42
|
+
NON_INTERACTIVE=0
|
|
43
|
+
PROMPT_INPUT_FD=0
|
|
44
|
+
|
|
45
|
+
if [[ $# -eq 4 ]]; then
|
|
46
|
+
if [[ "$4" != "--non-interactive" ]]; then
|
|
47
|
+
usage >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
NON_INTERACTIVE=1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [[ ! -t 0 ]]; then
|
|
54
|
+
NON_INTERACTIVE=1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if (( NON_INTERACTIVE == 0 )); then
|
|
58
|
+
if exec 3</dev/tty 2>/dev/null; then
|
|
59
|
+
PROMPT_INPUT_FD=3
|
|
60
|
+
else
|
|
61
|
+
NON_INTERACTIVE=1
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [[ "$PHASE" != "pre" && "$PHASE" != "post" ]]; then
|
|
66
|
+
echo "Error: <phase> must be 'pre' or 'post'." >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
if [[ ! -f "$PUBLISH_JSON" ]]; then
|
|
71
|
+
echo "Error: publish config not found at '$PUBLISH_JSON'." >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
76
|
+
echo "Error: jq is required to run publish quality gates." >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
log_info() {
|
|
81
|
+
printf '→ %s\n' "$1"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log_warn() {
|
|
85
|
+
printf '⚠ %s\n' "$1" >&2
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log_error() {
|
|
89
|
+
printf '✗ %s\n' "$1" >&2
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ensure_publish_history() {
|
|
93
|
+
mkdir -p "$(dirname "$PUBLISH_HISTORY")"
|
|
94
|
+
if [[ ! -f "$PUBLISH_HISTORY" ]]; then
|
|
95
|
+
echo '[]' > "$PUBLISH_HISTORY"
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
append_gate_failure_history() {
|
|
100
|
+
local gate_name="$1"
|
|
101
|
+
local entry
|
|
102
|
+
local history_tmp
|
|
103
|
+
|
|
104
|
+
ensure_publish_history
|
|
105
|
+
entry="$(jq -cn \
|
|
106
|
+
--arg gate "$gate_name" \
|
|
107
|
+
--arg phase "$PHASE" \
|
|
108
|
+
--arg target "$TARGET_NAME" \
|
|
109
|
+
--arg date "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
110
|
+
'{event:"gate_failure",gate:$gate,phase:$phase,target:$target,date:$date,exit_code:2}')"
|
|
111
|
+
|
|
112
|
+
history_tmp="$PUBLISH_HISTORY.tmp"
|
|
113
|
+
jq --argjson entry "$entry" '. += [$entry]' "$PUBLISH_HISTORY" > "$history_tmp"
|
|
114
|
+
mv "$history_tmp" "$PUBLISH_HISTORY"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
print_results_summary() {
|
|
118
|
+
local result
|
|
119
|
+
|
|
120
|
+
if [[ ${#RESULTS[@]} -eq 0 ]]; then
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
echo "Gate results:"
|
|
125
|
+
for result in "${RESULTS[@]}"; do
|
|
126
|
+
printf ' - %s\n' "$result"
|
|
127
|
+
done
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
jq_target_def() {
|
|
131
|
+
cat <<'JQ'
|
|
132
|
+
def target_obj:
|
|
133
|
+
if (.targets? | type) == "object" then
|
|
134
|
+
.targets[$target]
|
|
135
|
+
elif (.targets? | type) == "array" then
|
|
136
|
+
(.targets[]? | select((.name // .id // .target // "") == $target))
|
|
137
|
+
else
|
|
138
|
+
empty
|
|
139
|
+
end;
|
|
140
|
+
JQ
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
require_target() {
|
|
144
|
+
local exists
|
|
145
|
+
exists="$(jq -r --arg target "$TARGET_NAME" "$(jq_target_def) target_obj | if . == null or . == false then \"\" else \"yes\" end" "$PUBLISH_JSON")"
|
|
146
|
+
if [[ -z "$exists" ]]; then
|
|
147
|
+
log_error "Target '$TARGET_NAME' was not found in '$PUBLISH_JSON'."
|
|
148
|
+
exit 1
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
TARGET_TYPE=""
|
|
153
|
+
|
|
154
|
+
resolve_target_type() {
|
|
155
|
+
TARGET_TYPE="$(jq -r --arg target "$TARGET_NAME" "$(jq_target_def) target_obj | .type // empty" "$PUBLISH_JSON" | head -n 1)"
|
|
156
|
+
# Default to mobile-ios when unset to preserve legacy behaviour.
|
|
157
|
+
[[ -n "$TARGET_TYPE" ]] || TARGET_TYPE="mobile-ios"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get_gate_entries() {
|
|
161
|
+
jq -c --arg target "$TARGET_NAME" --arg phase "$PHASE" "$(jq_target_def)
|
|
162
|
+
target_obj as \$target_config |
|
|
163
|
+
(\$target_config.checks[\$phase] // [])[]? |
|
|
164
|
+
if type == \"string\" then
|
|
165
|
+
{name: ., config: {}}
|
|
166
|
+
elif type == \"object\" then
|
|
167
|
+
if has(\"name\") then {name: .name, config: .}
|
|
168
|
+
elif has(\"gate\") then {name: .gate, config: .}
|
|
169
|
+
elif has(\"type\") then {name: .type, config: .}
|
|
170
|
+
else empty end
|
|
171
|
+
else
|
|
172
|
+
empty
|
|
173
|
+
end
|
|
174
|
+
" "$PUBLISH_JSON"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get_value_from_config() {
|
|
178
|
+
local key="$1"
|
|
179
|
+
jq -r --arg target "$TARGET_NAME" --arg key "$key" "$(jq_target_def)
|
|
180
|
+
target_obj as \$target_config |
|
|
181
|
+
(
|
|
182
|
+
\$target_config.checks[\$key] //
|
|
183
|
+
\$target_config[\$key] //
|
|
184
|
+
.checks[\$key] //
|
|
185
|
+
.[\$key] //
|
|
186
|
+
empty
|
|
187
|
+
)
|
|
188
|
+
" "$PUBLISH_JSON"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
shell_quote() {
|
|
192
|
+
printf '%q' "$1"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
find_npm_script_command() {
|
|
196
|
+
local script_name="$1"
|
|
197
|
+
local package_file
|
|
198
|
+
while IFS= read -r package_file; do
|
|
199
|
+
if jq -e --arg script_name "$script_name" '.scripts[$script_name] // empty' "$package_file" >/dev/null 2>&1; then
|
|
200
|
+
local package_dir
|
|
201
|
+
package_dir="$(dirname "$package_file")"
|
|
202
|
+
if [[ "$package_dir" == "$REPO_ROOT" ]]; then
|
|
203
|
+
printf 'npm run %s\n' "$script_name"
|
|
204
|
+
else
|
|
205
|
+
printf 'npm --prefix %s run %s\n' "$(shell_quote "$package_dir")" "$script_name"
|
|
206
|
+
fi
|
|
207
|
+
return 0
|
|
208
|
+
fi
|
|
209
|
+
done < <(find "$REPO_ROOT" -name package.json -not -path '*/node_modules/*' -not -path '*/.git/*' | sort)
|
|
210
|
+
return 1
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Resolve an npm script from the top-level package.json only (mirrors
|
|
214
|
+
# npm_pipeline.sh, which publishes $REPO_ROOT/package.json). Prints the
|
|
215
|
+
# command form npm uses for the built-in `test` script vs. `npm run <name>`.
|
|
216
|
+
find_npm_script_in_root() {
|
|
217
|
+
local script_name="$1"
|
|
218
|
+
local package_file="$REPO_ROOT/package.json"
|
|
219
|
+
|
|
220
|
+
[[ -f "$package_file" ]] || return 1
|
|
221
|
+
jq -e --arg script_name "$script_name" '.scripts[$script_name] // empty' "$package_file" >/dev/null 2>&1 || return 1
|
|
222
|
+
|
|
223
|
+
# `npm test` is a first-class alias; everything else uses `npm run <name>`.
|
|
224
|
+
if [[ "$script_name" == "test" ]]; then
|
|
225
|
+
printf 'npm test\n'
|
|
226
|
+
else
|
|
227
|
+
printf 'npm run %s\n' "$script_name"
|
|
228
|
+
fi
|
|
229
|
+
return 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
find_xcodebuild_command() {
|
|
233
|
+
local action="$1"
|
|
234
|
+
if find "$REPO_ROOT" -maxdepth 4 \( -name '*.xcodeproj' -o -name '*.xcworkspace' \) | grep -q .; then
|
|
235
|
+
printf 'xcodebuild %s\n' "$action"
|
|
236
|
+
return 0
|
|
237
|
+
fi
|
|
238
|
+
return 1
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
resolve_default_command() {
|
|
242
|
+
local gate_name="$1"
|
|
243
|
+
|
|
244
|
+
# NPM targets: resolve strictly against $REPO_ROOT/package.json and never
|
|
245
|
+
# fall through to iOS tooling. Missing scripts are announced with a clear
|
|
246
|
+
# informational note (on stderr so it does not pollute the captured
|
|
247
|
+
# command) before the gate is reported as skipped upstream.
|
|
248
|
+
if [[ "$TARGET_TYPE" == "npm" ]]; then
|
|
249
|
+
case "$gate_name" in
|
|
250
|
+
build|test|lint)
|
|
251
|
+
if find_npm_script_in_root "$gate_name"; then
|
|
252
|
+
return 0
|
|
253
|
+
fi
|
|
254
|
+
printf '→ npm gate %q skipped: no %q script defined in package.json\n' \
|
|
255
|
+
"$gate_name" "$gate_name" >&2
|
|
256
|
+
return 0
|
|
257
|
+
;;
|
|
258
|
+
*)
|
|
259
|
+
return 0
|
|
260
|
+
;;
|
|
261
|
+
esac
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
case "$gate_name" in
|
|
265
|
+
build)
|
|
266
|
+
find_npm_script_command build || find_xcodebuild_command build || true
|
|
267
|
+
;;
|
|
268
|
+
test)
|
|
269
|
+
find_npm_script_command test || find_xcodebuild_command test || true
|
|
270
|
+
;;
|
|
271
|
+
lint)
|
|
272
|
+
find_npm_script_command lint || true
|
|
273
|
+
;;
|
|
274
|
+
type-check)
|
|
275
|
+
find_npm_script_command typecheck || find_npm_script_command type-check || true
|
|
276
|
+
;;
|
|
277
|
+
smoke-test)
|
|
278
|
+
find_npm_script_command smoke-test || true
|
|
279
|
+
;;
|
|
280
|
+
*)
|
|
281
|
+
true
|
|
282
|
+
;;
|
|
283
|
+
esac
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
RUN_OUTPUT=""
|
|
287
|
+
RUN_STATUS=0
|
|
288
|
+
LAST_GATE_COMMAND=""
|
|
289
|
+
RESOLVED_GATE_COMMAND=""
|
|
290
|
+
|
|
291
|
+
run_command_capture() {
|
|
292
|
+
local command="$1"
|
|
293
|
+
LAST_GATE_COMMAND="$command"
|
|
294
|
+
set +e
|
|
295
|
+
RUN_OUTPUT="$(bash -lc "$command" 2>&1)"
|
|
296
|
+
RUN_STATUS=$?
|
|
297
|
+
set -e
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
print_failure_block() {
|
|
301
|
+
local gate_name="$1"
|
|
302
|
+
echo ""
|
|
303
|
+
echo "========== GATE FAILURE ==========" >&2
|
|
304
|
+
echo "Gate: $gate_name" >&2
|
|
305
|
+
echo "Phase: $PHASE" >&2
|
|
306
|
+
echo "Target: $TARGET_NAME" >&2
|
|
307
|
+
echo "Command: ${LAST_GATE_COMMAND:-n/a}" >&2
|
|
308
|
+
echo "Exit code: $RUN_STATUS" >&2
|
|
309
|
+
echo "----------- output -----------" >&2
|
|
310
|
+
if [[ -n "$RUN_OUTPUT" ]]; then
|
|
311
|
+
printf '%s\n' "$RUN_OUTPUT" >&2
|
|
312
|
+
else
|
|
313
|
+
echo "(no output captured)" >&2
|
|
314
|
+
fi
|
|
315
|
+
echo "==================================" >&2
|
|
316
|
+
echo "" >&2
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
attempt_gate() {
|
|
320
|
+
local gate_json="$1"
|
|
321
|
+
local gate_name="$2"
|
|
322
|
+
local resolve_status
|
|
323
|
+
|
|
324
|
+
if resolve_gate_command "$gate_name" "$gate_json"; then
|
|
325
|
+
resolve_status=0
|
|
326
|
+
else
|
|
327
|
+
resolve_status=$?
|
|
328
|
+
fi
|
|
329
|
+
if [[ $resolve_status -ne 0 ]]; then
|
|
330
|
+
return "$resolve_status"
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [[ -z "$RESOLVED_GATE_COMMAND" ]]; then
|
|
334
|
+
log_warn "No command resolved for gate '$gate_name'; skipping."
|
|
335
|
+
return 1
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
log_info "Running gate '$gate_name'..."
|
|
339
|
+
run_command_capture "$RESOLVED_GATE_COMMAND"
|
|
340
|
+
|
|
341
|
+
if [[ $RUN_STATUS -ne 0 ]]; then
|
|
342
|
+
return 2
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
return 0
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
handle_gate_failure() {
|
|
349
|
+
local gate_name="$1"
|
|
350
|
+
local gate_json="$2"
|
|
351
|
+
local retry_count=0
|
|
352
|
+
local choice
|
|
353
|
+
local choice_lower
|
|
354
|
+
local retry_status
|
|
355
|
+
|
|
356
|
+
append_gate_failure_history "$gate_name"
|
|
357
|
+
print_failure_block "$gate_name"
|
|
358
|
+
|
|
359
|
+
if (( NON_INTERACTIVE )); then
|
|
360
|
+
return 2
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
while true; do
|
|
364
|
+
if (( retry_count >= 3 )); then
|
|
365
|
+
log_error "Retry limit reached for gate '$gate_name'. Aborting deploy."
|
|
366
|
+
return 2
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
read -r -u "$PROMPT_INPUT_FD" -p "[r] Retry after fixing / [a] Abort deploy: " choice || choice="a"
|
|
370
|
+
choice_lower="$(printf '%s' "$choice" | tr '[:upper:]' '[:lower:]')"
|
|
371
|
+
case "$choice_lower" in
|
|
372
|
+
r)
|
|
373
|
+
retry_count=$((retry_count + 1))
|
|
374
|
+
log_info "Retrying gate '$gate_name' (attempt $retry_count of 3)..."
|
|
375
|
+
if attempt_gate "$gate_json" "$gate_name"; then
|
|
376
|
+
retry_status=0
|
|
377
|
+
else
|
|
378
|
+
retry_status=$?
|
|
379
|
+
fi
|
|
380
|
+
case $retry_status in
|
|
381
|
+
0)
|
|
382
|
+
return 0
|
|
383
|
+
;;
|
|
384
|
+
1)
|
|
385
|
+
return 1
|
|
386
|
+
;;
|
|
387
|
+
2)
|
|
388
|
+
append_gate_failure_history "$gate_name"
|
|
389
|
+
print_failure_block "$gate_name"
|
|
390
|
+
;;
|
|
391
|
+
esac
|
|
392
|
+
;;
|
|
393
|
+
a)
|
|
394
|
+
log_error "Deploy aborted after gate '$gate_name' failed."
|
|
395
|
+
return 2
|
|
396
|
+
;;
|
|
397
|
+
*)
|
|
398
|
+
echo "Please enter 'r' or 'a'." >&2
|
|
399
|
+
;;
|
|
400
|
+
esac
|
|
401
|
+
done
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
resolve_custom_script_command() {
|
|
405
|
+
local gate_json="$1"
|
|
406
|
+
local script_path
|
|
407
|
+
local resolved_path
|
|
408
|
+
script_path="$(printf '%s' "$gate_json" | jq -r '.config.path // .config.script // .config.command // .config.custom_script_path // empty')"
|
|
409
|
+
if [[ -z "$script_path" ]]; then
|
|
410
|
+
script_path="$(get_value_from_config custom_script)"
|
|
411
|
+
fi
|
|
412
|
+
if [[ -z "$script_path" ]]; then
|
|
413
|
+
script_path="$(get_value_from_config custom_script_path)"
|
|
414
|
+
fi
|
|
415
|
+
if [[ -z "$script_path" ]]; then
|
|
416
|
+
script_path="$(get_value_from_config custom_script_command)"
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
if [[ -z "$script_path" ]]; then
|
|
420
|
+
log_warn "No custom script path configured for target '$TARGET_NAME'; skipping custom-script gate."
|
|
421
|
+
return 1
|
|
422
|
+
fi
|
|
423
|
+
|
|
424
|
+
if [[ ! "$script_path" =~ $CUSTOM_SCRIPT_PATTERN ]]; then
|
|
425
|
+
RUN_OUTPUT="Rejected custom-script path '$script_path': only [./a-zA-Z0-9_-] characters are allowed."
|
|
426
|
+
RUN_STATUS=2
|
|
427
|
+
LAST_GATE_COMMAND="$script_path"
|
|
428
|
+
return 2
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
if [[ "$script_path" = /* ]]; then
|
|
432
|
+
resolved_path="$script_path"
|
|
433
|
+
else
|
|
434
|
+
resolved_path="$REPO_ROOT/${script_path#./}"
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
if [[ ! -f "$resolved_path" ]]; then
|
|
438
|
+
RUN_OUTPUT="Custom script '$resolved_path' does not exist."
|
|
439
|
+
RUN_STATUS=2
|
|
440
|
+
LAST_GATE_COMMAND="$resolved_path"
|
|
441
|
+
return 2
|
|
442
|
+
fi
|
|
443
|
+
|
|
444
|
+
if [[ -x "$resolved_path" ]]; then
|
|
445
|
+
RESOLVED_GATE_COMMAND="$(shell_quote "$resolved_path")"
|
|
446
|
+
else
|
|
447
|
+
RESOLVED_GATE_COMMAND="bash $(shell_quote "$resolved_path")"
|
|
448
|
+
fi
|
|
449
|
+
return 0
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
resolve_ping_command() {
|
|
453
|
+
local gate_json="$1"
|
|
454
|
+
local ping_url
|
|
455
|
+
ping_url="$(printf '%s' "$gate_json" | jq -r '.config.url // .config.ping_url // empty')"
|
|
456
|
+
if [[ -z "$ping_url" ]]; then
|
|
457
|
+
ping_url="$(get_value_from_config ping_url)"
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
if [[ -z "$ping_url" ]]; then
|
|
461
|
+
log_warn "No ping URL configured for target '$TARGET_NAME'; skipping ping gate."
|
|
462
|
+
return 1
|
|
463
|
+
fi
|
|
464
|
+
|
|
465
|
+
RESOLVED_GATE_COMMAND="curl -fsS -o /dev/null $(shell_quote "$ping_url")"
|
|
466
|
+
return 0
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
resolve_gate_command() {
|
|
470
|
+
local gate_name="$1"
|
|
471
|
+
local gate_json="$2"
|
|
472
|
+
local command_override=""
|
|
473
|
+
RESOLVED_GATE_COMMAND=""
|
|
474
|
+
|
|
475
|
+
case "$gate_name" in
|
|
476
|
+
build)
|
|
477
|
+
command_override="$(get_value_from_config build_command)"
|
|
478
|
+
;;
|
|
479
|
+
test)
|
|
480
|
+
command_override="$(get_value_from_config test_command)"
|
|
481
|
+
;;
|
|
482
|
+
lint)
|
|
483
|
+
command_override="$(get_value_from_config lint_command)"
|
|
484
|
+
;;
|
|
485
|
+
type-check)
|
|
486
|
+
command_override="$(get_value_from_config typecheck_command)"
|
|
487
|
+
;;
|
|
488
|
+
smoke-test)
|
|
489
|
+
command_override="$(get_value_from_config smoke_test_command)"
|
|
490
|
+
;;
|
|
491
|
+
custom-script)
|
|
492
|
+
resolve_custom_script_command "$gate_json"
|
|
493
|
+
return $?
|
|
494
|
+
;;
|
|
495
|
+
ping)
|
|
496
|
+
resolve_ping_command "$gate_json"
|
|
497
|
+
return $?
|
|
498
|
+
;;
|
|
499
|
+
*)
|
|
500
|
+
log_warn "Unsupported gate '$gate_name'; skipping."
|
|
501
|
+
return 1
|
|
502
|
+
;;
|
|
503
|
+
esac
|
|
504
|
+
|
|
505
|
+
if [[ -n "$command_override" ]]; then
|
|
506
|
+
RESOLVED_GATE_COMMAND="$command_override"
|
|
507
|
+
return 0
|
|
508
|
+
fi
|
|
509
|
+
|
|
510
|
+
RESOLVED_GATE_COMMAND="$(resolve_default_command "$gate_name")"
|
|
511
|
+
return 0
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
RESULTS=()
|
|
515
|
+
|
|
516
|
+
run_gate() {
|
|
517
|
+
local gate_json="$1"
|
|
518
|
+
local gate_name
|
|
519
|
+
local gate_status
|
|
520
|
+
|
|
521
|
+
gate_name="$(printf '%s' "$gate_json" | jq -r '.name')"
|
|
522
|
+
if attempt_gate "$gate_json" "$gate_name"; then
|
|
523
|
+
gate_status=0
|
|
524
|
+
else
|
|
525
|
+
gate_status=$?
|
|
526
|
+
fi
|
|
527
|
+
case $gate_status in
|
|
528
|
+
0)
|
|
529
|
+
RESULTS+=("$gate_name:passed")
|
|
530
|
+
log_info "Gate '$gate_name' passed."
|
|
531
|
+
return 0
|
|
532
|
+
;;
|
|
533
|
+
1)
|
|
534
|
+
RESULTS+=("$gate_name:skipped")
|
|
535
|
+
return 0
|
|
536
|
+
;;
|
|
537
|
+
2)
|
|
538
|
+
if handle_gate_failure "$gate_name" "$gate_json"; then
|
|
539
|
+
gate_status=0
|
|
540
|
+
else
|
|
541
|
+
gate_status=$?
|
|
542
|
+
fi
|
|
543
|
+
case $gate_status in
|
|
544
|
+
0)
|
|
545
|
+
RESULTS+=("$gate_name:passed")
|
|
546
|
+
log_info "Gate '$gate_name' passed."
|
|
547
|
+
return 0
|
|
548
|
+
;;
|
|
549
|
+
1)
|
|
550
|
+
RESULTS+=("$gate_name:skipped")
|
|
551
|
+
return 0
|
|
552
|
+
;;
|
|
553
|
+
2)
|
|
554
|
+
return 2
|
|
555
|
+
;;
|
|
556
|
+
esac
|
|
557
|
+
;;
|
|
558
|
+
esac
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
build_gate_list() {
|
|
562
|
+
local gate_json
|
|
563
|
+
local -a mandatory_pre_gates=()
|
|
564
|
+
|
|
565
|
+
if [[ "$PHASE" == "pre" ]]; then
|
|
566
|
+
if [[ "$TARGET_TYPE" == "npm" ]]; then
|
|
567
|
+
# npm-specific mandatory gates: test, build, lint (per E26_S06_T03).
|
|
568
|
+
mandatory_pre_gates=(test build lint)
|
|
569
|
+
else
|
|
570
|
+
mandatory_pre_gates=(build test)
|
|
571
|
+
fi
|
|
572
|
+
local gate
|
|
573
|
+
for gate in "${mandatory_pre_gates[@]}"; do
|
|
574
|
+
printf '{"name":"%s","config":{},"source":"mandatory"}\n' "$gate"
|
|
575
|
+
done
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
while IFS= read -r gate_json; do
|
|
579
|
+
[[ -z "$gate_json" ]] && continue
|
|
580
|
+
local gate_name
|
|
581
|
+
gate_name="$(printf '%s' "$gate_json" | jq -r '.name')"
|
|
582
|
+
if [[ "$PHASE" == "pre" ]]; then
|
|
583
|
+
local is_mandatory=0
|
|
584
|
+
local m_gate
|
|
585
|
+
for m_gate in "${mandatory_pre_gates[@]}"; do
|
|
586
|
+
if [[ "$gate_name" == "$m_gate" ]]; then
|
|
587
|
+
is_mandatory=1
|
|
588
|
+
break
|
|
589
|
+
fi
|
|
590
|
+
done
|
|
591
|
+
if (( is_mandatory )); then
|
|
592
|
+
log_warn "Ignoring configurable '$gate_name' gate for target '$TARGET_NAME'; mandatory global gates already enforce it."
|
|
593
|
+
continue
|
|
594
|
+
fi
|
|
595
|
+
fi
|
|
596
|
+
printf '%s\n' "$gate_json"
|
|
597
|
+
done < <(get_gate_entries)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
main() {
|
|
601
|
+
local gate_json
|
|
602
|
+
|
|
603
|
+
require_target
|
|
604
|
+
resolve_target_type
|
|
605
|
+
ensure_publish_history
|
|
606
|
+
|
|
607
|
+
while IFS= read -r gate_json; do
|
|
608
|
+
[[ -z "$gate_json" ]] && continue
|
|
609
|
+
run_gate "$gate_json" || exit 2
|
|
610
|
+
done < <(build_gate_list)
|
|
611
|
+
|
|
612
|
+
print_results_summary
|
|
613
|
+
log_info "All $PHASE-deploy gates completed successfully for target '$TARGET_NAME'."
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
main "$@"
|