shipwright-cli 1.10.0 → 2.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/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright github-app — GitHub App Management & Webhook Receiver ║
|
|
4
|
+
# ║ JWT generation · Installation tokens · Webhook validation · Events ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="2.0.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
35
|
+
now_epoch() { date +%s; }
|
|
36
|
+
|
|
37
|
+
# ─── Structured Event Log ────────────────────────────────────────────────────
|
|
38
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
39
|
+
|
|
40
|
+
emit_event() {
|
|
41
|
+
local event_type="$1"
|
|
42
|
+
shift
|
|
43
|
+
local json_fields=""
|
|
44
|
+
for kv in "$@"; do
|
|
45
|
+
local key="${kv%%=*}"
|
|
46
|
+
local val="${kv#*=}"
|
|
47
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
48
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
49
|
+
else
|
|
50
|
+
val="${val//\"/\\\"}"
|
|
51
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
mkdir -p "${HOME}/.shipwright"
|
|
55
|
+
echo "{\"ts\":\"$(now_iso)\",\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ─── Config File Locations ────────────────────────────────────────────────
|
|
59
|
+
CONFIG_DIR="${HOME}/.shipwright"
|
|
60
|
+
CONFIG_FILE="${CONFIG_DIR}/github-app.json"
|
|
61
|
+
TOKENS_FILE="${CONFIG_DIR}/github-app-tokens.json"
|
|
62
|
+
WEBHOOK_LOG="${CONFIG_DIR}/webhook-events.jsonl"
|
|
63
|
+
|
|
64
|
+
# ─── Ensure config directory exists ───────────────────────────────────────
|
|
65
|
+
_ensure_config_dir() {
|
|
66
|
+
mkdir -p "$CONFIG_DIR"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
# APP CONFIG FUNCTIONS
|
|
71
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
# ─── Setup: Interactive configuration ──────────────────────────────────────
|
|
74
|
+
cmd_setup() {
|
|
75
|
+
_ensure_config_dir
|
|
76
|
+
|
|
77
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
78
|
+
warn "GitHub App config already exists at ${CONFIG_FILE}"
|
|
79
|
+
read -p "Overwrite? (y/n) " -r
|
|
80
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
81
|
+
info "Skipped setup"
|
|
82
|
+
return 0
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
echo ""
|
|
87
|
+
info "GitHub App Configuration"
|
|
88
|
+
echo ""
|
|
89
|
+
|
|
90
|
+
read -p "App ID: " app_id
|
|
91
|
+
read -p "Private key file path: " key_path
|
|
92
|
+
|
|
93
|
+
if [[ ! -f "$key_path" ]]; then
|
|
94
|
+
error "Private key file not found: $key_path"
|
|
95
|
+
return 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
read -p "Installation ID: " installation_id
|
|
99
|
+
read -p "Webhook secret (optional, press Enter to skip): " webhook_secret
|
|
100
|
+
|
|
101
|
+
# Create config atomically
|
|
102
|
+
local tmp_config
|
|
103
|
+
tmp_config=$(mktemp)
|
|
104
|
+
jq -n \
|
|
105
|
+
--arg app_id "$app_id" \
|
|
106
|
+
--arg key_path "$key_path" \
|
|
107
|
+
--arg install_id "$installation_id" \
|
|
108
|
+
--arg webhook_secret "$webhook_secret" \
|
|
109
|
+
'{
|
|
110
|
+
app_id: ($app_id | tonumber),
|
|
111
|
+
private_key_path: $key_path,
|
|
112
|
+
installation_id: ($install_id | tonumber),
|
|
113
|
+
webhook_secret: $webhook_secret,
|
|
114
|
+
created_at: "'$(now_iso)'"
|
|
115
|
+
}' > "$tmp_config"
|
|
116
|
+
|
|
117
|
+
mv "$tmp_config" "$CONFIG_FILE"
|
|
118
|
+
chmod 600 "$CONFIG_FILE"
|
|
119
|
+
|
|
120
|
+
success "GitHub App config saved to ${CONFIG_FILE}"
|
|
121
|
+
emit_event "github_app.setup" "app_id=$app_id" "install_id=$installation_id"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ─── Load config from file ────────────────────────────────────────────────
|
|
125
|
+
_load_config() {
|
|
126
|
+
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
127
|
+
error "GitHub App config not found. Run 'shipwright github-app setup' first."
|
|
128
|
+
return 1
|
|
129
|
+
fi
|
|
130
|
+
cat "$CONFIG_FILE"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# ─── Get config value ────────────────────────────────────────────────────
|
|
134
|
+
_get_config_value() {
|
|
135
|
+
local key="$1"
|
|
136
|
+
_load_config | jq -r ".$key // empty" 2>/dev/null || true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
140
|
+
# JWT & TOKEN FUNCTIONS
|
|
141
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
142
|
+
|
|
143
|
+
# ─── Generate JWT from private key ────────────────────────────────────────
|
|
144
|
+
_generate_jwt() {
|
|
145
|
+
local app_id="$1"
|
|
146
|
+
local key_path="$2"
|
|
147
|
+
|
|
148
|
+
if [[ ! -f "$key_path" ]]; then
|
|
149
|
+
error "Private key not found: $key_path"
|
|
150
|
+
return 1
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# JWT header (alg: RS256, typ: JWT)
|
|
154
|
+
local header
|
|
155
|
+
header=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '=' | tr '+/' '-_')
|
|
156
|
+
|
|
157
|
+
# JWT payload (iss: app_id, iat: now, exp: now + 10 min)
|
|
158
|
+
local now
|
|
159
|
+
now=$(date +%s)
|
|
160
|
+
local exp=$((now + 600))
|
|
161
|
+
|
|
162
|
+
local payload
|
|
163
|
+
payload=$(echo -n '{"iss":'$app_id',"iat":'$now',"exp":'$exp'}' | base64 | tr -d '=' | tr '+/' '-_')
|
|
164
|
+
|
|
165
|
+
# Sign with private key
|
|
166
|
+
local signature_input="${header}.${payload}"
|
|
167
|
+
local signature
|
|
168
|
+
signature=$(echo -n "$signature_input" | openssl dgst -sha256 -sign "$key_path" | base64 | tr -d '=' | tr '+/' '-_')
|
|
169
|
+
|
|
170
|
+
echo "${signature_input}.${signature}"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# ─── Exchange JWT for installation token ─────────────────────────────────
|
|
174
|
+
_get_installation_token() {
|
|
175
|
+
local jwt="$1"
|
|
176
|
+
local installation_id="$2"
|
|
177
|
+
|
|
178
|
+
if [[ "${NO_GITHUB:-}" == "true" || "${NO_GITHUB:-}" == "1" ]]; then
|
|
179
|
+
echo ""
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
local response
|
|
184
|
+
response=$(curl -s -H "Authorization: Bearer $jwt" \
|
|
185
|
+
-H "Accept: application/vnd.github+json" \
|
|
186
|
+
"https://api.github.com/app/installations/${installation_id}/access_tokens" \
|
|
187
|
+
-d '{}' -X POST 2>/dev/null) || true
|
|
188
|
+
|
|
189
|
+
if echo "$response" | jq -e '.token' >/dev/null 2>&1; then
|
|
190
|
+
echo "$response" | jq -r '.token'
|
|
191
|
+
else
|
|
192
|
+
error "Failed to get installation token"
|
|
193
|
+
echo "$response" | jq -r '.message // "Unknown error"' >&2
|
|
194
|
+
return 1
|
|
195
|
+
fi
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# ─── Cache token with expiry ──────────────────────────────────────────────
|
|
199
|
+
_cache_token() {
|
|
200
|
+
local installation_id="$1"
|
|
201
|
+
local token="$2"
|
|
202
|
+
local expires_at="$3"
|
|
203
|
+
|
|
204
|
+
_ensure_config_dir
|
|
205
|
+
|
|
206
|
+
local tmp_tokens
|
|
207
|
+
tmp_tokens=$(mktemp)
|
|
208
|
+
|
|
209
|
+
if [[ -f "$TOKENS_FILE" ]]; then
|
|
210
|
+
jq ".tokens += [{\"installation_id\":$installation_id,\"token\":\"$token\",\"expires_at\":\"$expires_at\"}]" \
|
|
211
|
+
"$TOKENS_FILE" > "$tmp_tokens"
|
|
212
|
+
else
|
|
213
|
+
jq -n ".tokens = [{\"installation_id\":$installation_id,\"token\":\"$token\",\"expires_at\":\"$expires_at\"}]" \
|
|
214
|
+
> "$tmp_tokens"
|
|
215
|
+
fi
|
|
216
|
+
|
|
217
|
+
mv "$tmp_tokens" "$TOKENS_FILE"
|
|
218
|
+
chmod 600 "$TOKENS_FILE"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# ─── Get cached token if still valid ──────────────────────────────────────
|
|
222
|
+
_get_cached_token() {
|
|
223
|
+
local installation_id="$1"
|
|
224
|
+
|
|
225
|
+
if [[ ! -f "$TOKENS_FILE" ]]; then
|
|
226
|
+
echo ""
|
|
227
|
+
return 1
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
local now
|
|
231
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
232
|
+
|
|
233
|
+
local token
|
|
234
|
+
token=$(jq -r ".tokens[] | select(.installation_id==$installation_id and .expires_at > \"$now\") | .token" \
|
|
235
|
+
"$TOKENS_FILE" 2>/dev/null | head -1 || true)
|
|
236
|
+
|
|
237
|
+
if [[ -n "$token" ]]; then
|
|
238
|
+
echo "$token"
|
|
239
|
+
return 0
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
echo ""
|
|
243
|
+
return 1
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# ─── Get installation token (cached or fresh) ─────────────────────────────
|
|
247
|
+
cmd_token() {
|
|
248
|
+
local app_id
|
|
249
|
+
app_id=$(_get_config_value "app_id") || {
|
|
250
|
+
error "GitHub App not configured. Run 'shipwright github-app setup' first."
|
|
251
|
+
return 1
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
local key_path
|
|
255
|
+
key_path=$(_get_config_value "private_key_path") || {
|
|
256
|
+
error "Missing private_key_path in config"
|
|
257
|
+
return 1
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
local installation_id
|
|
261
|
+
installation_id=$(_get_config_value "installation_id") || {
|
|
262
|
+
error "Missing installation_id in config"
|
|
263
|
+
return 1
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Try cached token first
|
|
267
|
+
local cached_token
|
|
268
|
+
cached_token=$(_get_cached_token "$installation_id" 2>/dev/null) || true
|
|
269
|
+
if [[ -n "$cached_token" ]]; then
|
|
270
|
+
echo "$cached_token"
|
|
271
|
+
return 0
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# Generate JWT and exchange for token
|
|
275
|
+
info "Generating JWT and requesting installation token..."
|
|
276
|
+
local jwt
|
|
277
|
+
jwt=$(_generate_jwt "$app_id" "$key_path") || return 1
|
|
278
|
+
|
|
279
|
+
local token
|
|
280
|
+
token=$(_get_installation_token "$jwt" "$installation_id") || return 1
|
|
281
|
+
|
|
282
|
+
# Cache token (valid for 1 hour)
|
|
283
|
+
local expires_at
|
|
284
|
+
expires_at=$(date -u -d "+1 hour" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+1H +"%Y-%m-%dT%H:%M:%SZ")
|
|
285
|
+
_cache_token "$installation_id" "$token" "$expires_at"
|
|
286
|
+
|
|
287
|
+
success "Got installation token (cached for 1 hour)"
|
|
288
|
+
emit_event "github_app.token_acquired" "installation_id=$installation_id"
|
|
289
|
+
echo "$token"
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
293
|
+
# WEBHOOK FUNCTIONS
|
|
294
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
295
|
+
|
|
296
|
+
# ─── Verify webhook signature (HMAC-SHA256) ───────────────────────────────
|
|
297
|
+
cmd_verify() {
|
|
298
|
+
local webhook_secret
|
|
299
|
+
webhook_secret=$(_get_config_value "webhook_secret") || true
|
|
300
|
+
|
|
301
|
+
if [[ -z "$webhook_secret" ]]; then
|
|
302
|
+
error "Webhook secret not configured"
|
|
303
|
+
return 1
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
# Read payload from stdin
|
|
307
|
+
local payload
|
|
308
|
+
payload=$(cat)
|
|
309
|
+
|
|
310
|
+
# Get signature from header (passed as argument or env var)
|
|
311
|
+
local signature="${1:-${X_HUB_SIGNATURE_256:-}}"
|
|
312
|
+
|
|
313
|
+
if [[ -z "$signature" ]]; then
|
|
314
|
+
error "No signature provided. Pass as argument or X_HUB_SIGNATURE_256 env var."
|
|
315
|
+
return 1
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Compute expected signature
|
|
319
|
+
local expected_sig
|
|
320
|
+
expected_sig=$(echo -n "$payload" | openssl dgst -sha256 -mac HMAC -macopt "key:${webhook_secret}" | sed 's/^.* /sha256=/')
|
|
321
|
+
|
|
322
|
+
if [[ "$expected_sig" == "$signature" ]]; then
|
|
323
|
+
success "Webhook signature verified"
|
|
324
|
+
echo "$payload"
|
|
325
|
+
return 0
|
|
326
|
+
else
|
|
327
|
+
error "Webhook signature verification failed"
|
|
328
|
+
error "Expected: $expected_sig"
|
|
329
|
+
error "Got: $signature"
|
|
330
|
+
return 1
|
|
331
|
+
fi
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# ─── Log webhook event ────────────────────────────────────────────────────
|
|
335
|
+
_log_webhook_event() {
|
|
336
|
+
local event_type="$1"
|
|
337
|
+
local payload="$2"
|
|
338
|
+
|
|
339
|
+
_ensure_config_dir
|
|
340
|
+
|
|
341
|
+
local event
|
|
342
|
+
event=$(jq -n \
|
|
343
|
+
--arg ts "$(now_iso)" \
|
|
344
|
+
--arg type "$event_type" \
|
|
345
|
+
--argjson payload "$payload" \
|
|
346
|
+
'{timestamp: $ts, event_type: $type, payload: $payload}')
|
|
347
|
+
|
|
348
|
+
echo "$event" >> "$WEBHOOK_LOG"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# ─── Handle GitHub webhook events ─────────────────────────────────────────
|
|
352
|
+
_handle_webhook_event() {
|
|
353
|
+
local event_type="$1"
|
|
354
|
+
local payload="$2"
|
|
355
|
+
|
|
356
|
+
case "$event_type" in
|
|
357
|
+
issues)
|
|
358
|
+
local action
|
|
359
|
+
action=$(echo "$payload" | jq -r '.action // empty')
|
|
360
|
+
if [[ "$action" == "labeled" ]]; then
|
|
361
|
+
local label
|
|
362
|
+
label=$(echo "$payload" | jq -r '.label.name // empty')
|
|
363
|
+
info "Issue labeled with: $label"
|
|
364
|
+
emit_event "webhook.issue_labeled" "label=$label"
|
|
365
|
+
fi
|
|
366
|
+
;;
|
|
367
|
+
pull_request)
|
|
368
|
+
local action
|
|
369
|
+
action=$(echo "$payload" | jq -r '.action // empty')
|
|
370
|
+
if [[ "$action" == "opened" ]]; then
|
|
371
|
+
info "Pull request opened"
|
|
372
|
+
emit_event "webhook.pr_opened"
|
|
373
|
+
elif [[ "$action" == "review_requested" ]]; then
|
|
374
|
+
info "Review requested on PR"
|
|
375
|
+
emit_event "webhook.pr_review_requested"
|
|
376
|
+
fi
|
|
377
|
+
;;
|
|
378
|
+
check_suite)
|
|
379
|
+
local action
|
|
380
|
+
action=$(echo "$payload" | jq -r '.action // empty')
|
|
381
|
+
if [[ "$action" == "requested" ]]; then
|
|
382
|
+
info "Check suite requested"
|
|
383
|
+
emit_event "webhook.check_suite_requested"
|
|
384
|
+
fi
|
|
385
|
+
;;
|
|
386
|
+
push)
|
|
387
|
+
local ref
|
|
388
|
+
ref=$(echo "$payload" | jq -r '.ref // empty')
|
|
389
|
+
info "Push to: $ref"
|
|
390
|
+
emit_event "webhook.push" "ref=$ref"
|
|
391
|
+
;;
|
|
392
|
+
*)
|
|
393
|
+
info "Webhook event: $event_type"
|
|
394
|
+
;;
|
|
395
|
+
esac
|
|
396
|
+
|
|
397
|
+
_log_webhook_event "$event_type" "$payload"
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
401
|
+
# APP MANIFEST & STATUS FUNCTIONS
|
|
402
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
403
|
+
|
|
404
|
+
# ─── Generate GitHub App manifest for easy setup ────────────────────────
|
|
405
|
+
cmd_manifest() {
|
|
406
|
+
local app_name="${1:-shipwright-app}"
|
|
407
|
+
local webhook_url="${2:-https://webhook.example.com}"
|
|
408
|
+
|
|
409
|
+
local manifest
|
|
410
|
+
manifest=$(jq -n \
|
|
411
|
+
--arg name "$app_name" \
|
|
412
|
+
--arg webhook_url "$webhook_url" \
|
|
413
|
+
'{
|
|
414
|
+
name: $name,
|
|
415
|
+
url: "https://github.com/sethdford/shipwright",
|
|
416
|
+
hook_attributes: {
|
|
417
|
+
url: $webhook_url
|
|
418
|
+
},
|
|
419
|
+
redirect_url: "https://github.com/apps/'$app_name'/installations/new",
|
|
420
|
+
description: "Autonomous pipeline delivery with Shipwright",
|
|
421
|
+
public: true,
|
|
422
|
+
default_events: [
|
|
423
|
+
"issues",
|
|
424
|
+
"pull_request",
|
|
425
|
+
"pull_request_review",
|
|
426
|
+
"pull_request_review_comment",
|
|
427
|
+
"check_suite",
|
|
428
|
+
"check_run",
|
|
429
|
+
"push"
|
|
430
|
+
],
|
|
431
|
+
default_permissions: {
|
|
432
|
+
contents: "write",
|
|
433
|
+
checks: "write",
|
|
434
|
+
pull_requests: "write",
|
|
435
|
+
issues: "write",
|
|
436
|
+
deployments: "write"
|
|
437
|
+
}
|
|
438
|
+
}')
|
|
439
|
+
|
|
440
|
+
echo "$manifest" | jq .
|
|
441
|
+
success "Manifest generated. Visit: https://github.com/settings/apps/new to create your app."
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# ─── Show app status and config ─────────────────────────────────────────
|
|
445
|
+
cmd_status() {
|
|
446
|
+
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
447
|
+
warn "GitHub App not configured"
|
|
448
|
+
echo ""
|
|
449
|
+
echo "Run '${CYAN}shipwright github-app setup${RESET}' to configure"
|
|
450
|
+
return 0
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
info "GitHub App Status"
|
|
454
|
+
echo ""
|
|
455
|
+
|
|
456
|
+
local config
|
|
457
|
+
config=$(cat "$CONFIG_FILE")
|
|
458
|
+
|
|
459
|
+
local app_id
|
|
460
|
+
app_id=$(echo "$config" | jq -r '.app_id')
|
|
461
|
+
local install_id
|
|
462
|
+
install_id=$(echo "$config" | jq -r '.installation_id')
|
|
463
|
+
local webhook_secret
|
|
464
|
+
webhook_secret=$(echo "$config" | jq -r '.webhook_secret // empty')
|
|
465
|
+
|
|
466
|
+
echo -e "${BOLD}Configuration:${RESET}"
|
|
467
|
+
echo " App ID: $app_id"
|
|
468
|
+
echo " Installation ID: $install_id"
|
|
469
|
+
echo " Webhook Secret: ${webhook_secret:-${DIM}(none)${RESET}}"
|
|
470
|
+
echo ""
|
|
471
|
+
|
|
472
|
+
# Show recent webhook events
|
|
473
|
+
if [[ -f "$WEBHOOK_LOG" ]]; then
|
|
474
|
+
local count
|
|
475
|
+
count=$(wc -l < "$WEBHOOK_LOG" 2>/dev/null || echo 0)
|
|
476
|
+
if [[ "$count" -gt 0 ]]; then
|
|
477
|
+
echo -e "${BOLD}Recent Webhook Events (last 10):${RESET}"
|
|
478
|
+
tail -10 "$WEBHOOK_LOG" | jq '{timestamp, event_type}' -c
|
|
479
|
+
echo ""
|
|
480
|
+
fi
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
# Show cached tokens
|
|
484
|
+
if [[ -f "$TOKENS_FILE" ]]; then
|
|
485
|
+
echo -e "${BOLD}Cached Tokens:${RESET}"
|
|
486
|
+
jq '.tokens[] | {installation_id, expires_at}' "$TOKENS_FILE" 2>/dev/null || echo " (none)"
|
|
487
|
+
echo ""
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
success "Status retrieved"
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# ─── List recent webhook events ────────────────────────────────────────
|
|
494
|
+
cmd_events() {
|
|
495
|
+
local limit="${1:-20}"
|
|
496
|
+
|
|
497
|
+
if [[ ! -f "$WEBHOOK_LOG" ]]; then
|
|
498
|
+
warn "No webhook events logged yet"
|
|
499
|
+
return 0
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
info "Recent Webhook Events"
|
|
503
|
+
echo ""
|
|
504
|
+
|
|
505
|
+
tail -"$limit" "$WEBHOOK_LOG" | jq '{timestamp, event_type, payload: (.payload | keys)}' -c
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
509
|
+
# HELP & MAIN
|
|
510
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
511
|
+
|
|
512
|
+
show_help() {
|
|
513
|
+
cat <<EOF
|
|
514
|
+
${CYAN}${BOLD}shipwright github-app${RESET} — GitHub App Management & Webhook Receiver
|
|
515
|
+
|
|
516
|
+
${BOLD}USAGE${RESET}
|
|
517
|
+
shipwright github-app <command> [options]
|
|
518
|
+
|
|
519
|
+
${BOLD}COMMANDS${RESET}
|
|
520
|
+
${CYAN}setup${RESET} Interactive configuration (app ID, private key, installation ID)
|
|
521
|
+
${CYAN}token${RESET} Get/refresh installation access token (with caching)
|
|
522
|
+
${CYAN}manifest${RESET} Generate GitHub App manifest JSON for setup at github.com/settings/apps/new
|
|
523
|
+
${CYAN}verify${RESET} Verify webhook signature (read payload from stdin)
|
|
524
|
+
${CYAN}events${RESET} [limit] List recent webhook events (default: 20)
|
|
525
|
+
${CYAN}status${RESET} Show current app config, installation status, cached tokens
|
|
526
|
+
${CYAN}help${RESET} Show this help message
|
|
527
|
+
|
|
528
|
+
${BOLD}EXAMPLES${RESET}
|
|
529
|
+
${DIM}# Initial setup${RESET}
|
|
530
|
+
shipwright github-app setup
|
|
531
|
+
|
|
532
|
+
${DIM}# Get installation token${RESET}
|
|
533
|
+
shipwright github-app token
|
|
534
|
+
|
|
535
|
+
${DIM}# Generate manifest for app creation${RESET}
|
|
536
|
+
shipwright github-app manifest "my-app" "https://my-webhook.com"
|
|
537
|
+
|
|
538
|
+
${DIM}# Verify webhook signature${RESET}
|
|
539
|
+
cat webhook-payload.json | shipwright github-app verify "sha256=..."
|
|
540
|
+
|
|
541
|
+
${DIM}# Check app status${RESET}
|
|
542
|
+
shipwright github-app status
|
|
543
|
+
|
|
544
|
+
${DIM}# View recent webhook events${RESET}
|
|
545
|
+
shipwright github-app events 50
|
|
546
|
+
|
|
547
|
+
${BOLD}CONFIG LOCATION${RESET}
|
|
548
|
+
${DIM}${CONFIG_FILE}${RESET}
|
|
549
|
+
|
|
550
|
+
${BOLD}WEBHOOK LOG${RESET}
|
|
551
|
+
${DIM}${WEBHOOK_LOG}${RESET}
|
|
552
|
+
|
|
553
|
+
${BOLD}TOKEN CACHE${RESET}
|
|
554
|
+
${DIM}${TOKENS_FILE}${RESET}
|
|
555
|
+
|
|
556
|
+
EOF
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
main() {
|
|
560
|
+
local cmd="${1:-help}"
|
|
561
|
+
shift 2>/dev/null || true
|
|
562
|
+
|
|
563
|
+
case "$cmd" in
|
|
564
|
+
setup)
|
|
565
|
+
cmd_setup "$@"
|
|
566
|
+
;;
|
|
567
|
+
token)
|
|
568
|
+
cmd_token "$@"
|
|
569
|
+
;;
|
|
570
|
+
manifest)
|
|
571
|
+
cmd_manifest "$@"
|
|
572
|
+
;;
|
|
573
|
+
verify)
|
|
574
|
+
cmd_verify "$@"
|
|
575
|
+
;;
|
|
576
|
+
events)
|
|
577
|
+
cmd_events "$@"
|
|
578
|
+
;;
|
|
579
|
+
status)
|
|
580
|
+
cmd_status "$@"
|
|
581
|
+
;;
|
|
582
|
+
help|--help|-h)
|
|
583
|
+
show_help
|
|
584
|
+
;;
|
|
585
|
+
*)
|
|
586
|
+
error "Unknown command: ${cmd}"
|
|
587
|
+
echo ""
|
|
588
|
+
show_help
|
|
589
|
+
exit 1
|
|
590
|
+
;;
|
|
591
|
+
esac
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
595
|
+
main "$@"
|
|
596
|
+
fi
|