-
Notifications
You must be signed in to change notification settings - Fork 1
Pipeline Design 57
Shipwright currently authenticates to GitHub exclusively via Personal Access Tokens (PATs) through the gh CLI. This works but has limitations:
-
Polling-only issue detection —
sw-daemon.shpolls GitHub Issues API on a configurable interval (~30s default), wasting API rate limit and introducing latency between issue creation and pipeline start. -
No branded check runs — Check runs created via PAT appear as the user, not as "Shipwright". The Checks API's full feature set (action buttons, structured output annotations,
external_idcorrelation) requires a GitHub App identity. -
No bot identity for PR reviews — PR comments and reviews appear as the PAT owner, not as a
[bot]account, making it unclear which feedback is automated. -
Rate limit pressure — PAT rate limits (5,000/hr) are shared across all
ghCLI usage; GitHub App installation tokens get a separate 5,000/hr budget per installation.
Codebase constraints:
- All scripts are Bash 3.2 compatible (no associative arrays, no
readarray, no${var,,}) -
set -euo pipefaileverywhere - Existing auth flows in
sw-github-checks.sh(521 lines),sw-github-graphql.sh(972 lines), andsw-github-deploy.sh(533 lines) all usegh apiwith implicit PAT auth - The daemon (
sw-daemon.sh, 5,670 lines) has a polling loop atdaemon_poll_loop()that would need a parallel webhook-driven code path -
$NO_GITHUBenv var must continue to disable all GitHub API calls including app auth - Test harness uses mock binaries in
$PATHwith canned responses — no real API calls in CI
Introduce a new module scripts/sw-github-app.sh that handles GitHub App authentication (JWT generation via openssl, installation token exchange and caching). When app auth is configured, this module exports GH_TOKEN with the installation token, which the gh CLI transparently uses for all subsequent API calls. This means zero changes to existing gh api call sites — the auth switch happens at the environment level.
Auth resolution order:
- If
SHIPWRIGHT_APP_ID+SHIPWRIGHT_APP_PRIVATE_KEY_FILEare set (or configured in.claude/daemon-config.jsonundergithub_app), use GitHub App auth - Otherwise, fall back to existing
ghCLI PAT auth (no behavioral change)
JWT generation uses openssl dgst -sha256 -sign with a manually-constructed RS256 JWT (header.payload base64url-encoded, then signed). This avoids any dependency beyond openssl, which ships with macOS and every Linux distro. The JWT is cached for 9 minutes (GitHub allows 10-minute expiry). Installation tokens are cached for 55 minutes (GitHub allows 60-minute expiry). Cache files go in ~/.shipwright/github-app/ with atomic writes (tmp + mv).
Webhook receiver is a new scripts/sw-github-webhook.sh that uses socat (preferred) or nc (fallback) to listen on a configurable port. It reads raw HTTP, verifies HMAC-SHA256 signatures against a shared secret, parses the JSON payload with jq, and dispatches to handler functions by event type. This is intentionally minimal — production deployments will front this with ngrok, Cloudflare Tunnel, or a reverse proxy. The webhook receiver is optional — the daemon can still poll.
Data flow for webhook mode:
GitHub → webhook HTTP POST → sw-github-webhook.sh (verify signature)
→ parse event type + action
→ issues.labeled → daemon_process_issue()
→ pull_request.opened → daemon_process_pr_event()
→ check_suite.requested → daemon_process_check_request()
→ push → daemon_process_push()
Data flow for check runs with app identity:
Pipeline stage starts → sw-github-checks.sh:checks_create_run()
→ sw-github-app.sh:gh_app_ensure_token() (if app configured)
→ GH_TOKEN set to installation token
→ gh api /repos/{owner}/{repo}/check-runs (POST)
→ Check run appears with Shipwright app identity + logo
Error handling:
- JWT generation failure → log error, fall back to PAT if available, skip if not
- Installation token exchange failure (403/404) → log with diagnostic info (app not installed on repo?), fall back to PAT
- Token expiry mid-pipeline →
gh_app_ensure_token()called before each API batch detects expiry via cached timestamp, refreshes transparently - Webhook signature mismatch → 401 response, event dropped, counter incremented for monitoring
- Webhook port already in use → error on start with diagnostic (
lsofhint) -
socat/ncnot available → error with install instructions, webhook mode unavailable
When app auth is active, the pipeline's stage_pr() function creates PRs and submits reviews using the installation token. GitHub automatically shows the [bot] badge. No code change needed beyond ensuring gh_app_ensure_token() runs before PR-related API calls in sw-pipeline.sh.
Added to .claude/daemon-config.json under a new github_app key:
{
"github_app": {
"app_id": "",
"installation_id": "",
"private_key_file": "~/.shipwright/shipwright-app.pem",
"webhook_mode": false,
"webhook_port": 3456,
"webhook_secret": ""
}
}All fields also available as env vars: SHIPWRIGHT_APP_ID, SHIPWRIGHT_APP_INSTALLATION_ID, SHIPWRIGHT_APP_PRIVATE_KEY_FILE, SHIPWRIGHT_WEBHOOK_PORT, SHIPWRIGHT_WEBHOOK_SECRET. Env vars take precedence over config file.
-
Replace PAT entirely with GitHub App (no dual mode) — Pros: Simpler code, single auth path, no conditional branching. Cons: Breaking change for all existing users; GitHub App setup is non-trivial (create app, generate key, install on repos); users with working PAT setups would be forced to migrate. Rejected because backward compatibility is a core Shipwright principle — the daemon config and all existing scripts must continue working without app configuration.
-
Use a Node.js/Bun webhook server instead of bash socat/nc — Pros: More robust HTTP handling, built-in JSON parsing, proper HTTP/1.1 compliance, could share code with
dashboard/server.ts. Cons: Introduces a runtime dependency (Node.js or Bun) for a core daemon feature; all other core scripts are pure bash; the webhook receiver is intentionally thin (signature verify + dispatch) and doesn't need full HTTP framework capabilities. Rejected because adding a runtime dependency for webhook receipt breaks the "bash-only core" architecture. The dashboard already uses Bun but it's optional — webhook processing should work on any server with just bash + openssl + jq. -
Use GitHub Actions for webhook processing instead of a local receiver — Pros: No local server needed, works behind NATs/firewalls, leverages GitHub's infrastructure. Cons: Adds latency (Actions cold start), requires a separate workflow file in each repo, harder to debug, can't process events when Actions is down, diverges from the local-first daemon architecture. Rejected because Shipwright's daemon is designed to run locally or on dedicated infrastructure, and the webhook receiver should be as close to the daemon process as possible for low-latency event processing.
-
Use
smee.ioor similar webhook proxy as the primary receiver — Pros: Works behind NAT immediately, no port exposure needed. Cons: Adds external service dependency, requiressmee-clientnpm package, introduces a relay point that could fail or have latency. Not rejected entirely — documenting smee.io as a development/testing option, but the primary receiver should be self-contained.
| File | Purpose |
|---|---|
scripts/sw-github-app.sh |
GitHub App auth: JWT generation, installation token management, setup/status CLI, token caching with atomic writes |
scripts/sw-github-webhook.sh |
Webhook HTTP receiver: socat/nc server, HMAC-SHA256 verification, event dispatch, rate limiting, lifecycle management |
scripts/sw-github-app-test.sh |
Unit tests: JWT format validation, token caching, config loading, setup flow, auth mode detection, NO_GITHUB bypass |
scripts/sw-github-webhook-test.sh |
Unit tests: signature verification, event routing, rate limiting, lifecycle, port conflicts, malformed payloads |
templates/github-app-manifest.json |
GitHub App manifest for one-click creation: permissions (checks:write, issues:read, pull_requests:write, contents:read, metadata:read), webhook events (issues, pull_request, check_suite, push) |
| File | Change | Risk |
|---|---|---|
scripts/sw-github-checks.sh |
Source app module; call gh_app_ensure_token() before API calls in checks_create_run(), checks_update_run(), checks_complete_run(); add external_id and structured output object when in app mode |
Low — guarded by gh_app_configured(), no change when unconfigured |
scripts/sw-github-graphql.sh |
Source app module; call gh_app_ensure_token() in _gql_query() (single injection point for all GraphQL calls) |
Low — single touch point, transparent fallback |
scripts/sw-github-deploy.sh |
Source app module; call gh_app_ensure_token() in deploy_create() and deploy_update_status()
|
Low — same pattern as checks |
scripts/sw-daemon.sh |
Source app/webhook modules (~line 48); add github_app.* config loading in daemon_load_config(); add daemon_webhook_loop() as alternative to daemon_poll_loop(); token refresh in daemon_health_check(); new daemon webhook start/stop/status subcommand |
Medium — daemon is 5,670 lines, the webhook loop is a parallel code path. Mitigated by keeping webhook loop self-contained and only activated via config flag |
scripts/sw-pipeline.sh |
Call gh_app_ensure_token() before PR creation in stage_pr() and review submission |
Low — additive, no behavioral change for PAT users |
scripts/sw-doctor.sh |
Add GitHub App validation block in section 13: check private key file exists, JWT generation works, token retrieval succeeds, report auth mode | Low — additive diagnostic checks |
scripts/sw |
Add app|github-app → sw-github-app.sh and webhook → sw-github-webhook.sh routing in the case statement |
Low — two new lines in router |
package.json |
Add sw-github-app-test.sh and sw-github-webhook-test.sh to test script chain |
Low — append to existing chain |
-
No new runtime dependencies.
openssl(for JWT),jq(already required),socatornc(for webhook server — commonly available, with fallback).ghCLI (already required). - No new build/dev dependencies.
-
JWT generation with openssl — Base64url encoding in bash requires translating
+/to-_and stripping=padding. The RS256 signing withopenssl dgst -sha256 -signis well-documented but the full JWT assembly in bash is error-prone. Mitigation: Dedicated test cases that validate JWT structure against known-good tokens; theapp statuscommand decodes and displays the JWT for debugging. -
Token caching race conditions — Multiple concurrent daemon workers could attempt token refresh simultaneously. Mitigation: Atomic writes (tmp + mv) and read-after-write verification. Installation tokens are safe to cache — GitHub returns the same token for concurrent requests within the validity window.
-
Webhook HTTP parsing in bash — Raw HTTP parsing with
readis fragile. Mitigation: Only parse what's needed (Content-Length for body read, signature header for verification); reject malformed requests early; the server is expected to sit behind a reverse proxy in production. -
Daemon webhook mode testing — The webhook loop is harder to unit test than the poll loop because it involves a running server process. Mitigation: Test the webhook handler functions in isolation; integration test sends requests via
curlto a test server instance. -
sw-daemon.shcomplexity — At 5,670 lines, the daemon is the largest script. Adding webhook mode increases surface area. Mitigation: Webhook functions are self-contained (daemon_webhook_loop,daemon_webhook_handler,daemon_webhook_dispatch); the existing poll loop is untouched; selection between modes is a single branch indaemon_start().
-
shipwright app statuscorrectly reports "app" or "pat" auth mode based on configuration -
shipwright app setupinteractively collects app_id, installation_id, private key path, and webhook secret; writes to.claude/daemon-config.json - JWT generated by
gh_app_generate_jwt()is valid RS256 (verifiable withopenssl dgst -sha256 -verify) - Installation token is cached in
~/.shipwright/github-app/token.jsonwith atomic writes; re-used within 55-minute window -
gh_app_ensure_token()is a no-op when$NO_GITHUBis set - All existing
gh apicalls continue to work unchanged when no GitHub App is configured (PAT fallback) - Check runs created with app auth show the app's name and logo in GitHub UI (manual verification on test repo)
-
shipwright webhook startbinds to configured port and responds to health check (curl -s http://localhost:3456/health) - Webhook receiver rejects requests with invalid HMAC-SHA256 signatures (returns 401)
- Webhook receiver correctly routes
issues.labeledevents todaemon_process_issue() -
shipwright daemon startwithwebhook_mode: truestarts the webhook listener instead of polling -
shipwright doctorsection 13 validates GitHub App config and reports issues clearly - PR reviews submitted via app auth display the
[bot]badge (manual verification) - All 24 test suites pass: 22 existing (no regressions) + 2 new (
sw-github-app-test.sh,sw-github-webhook-test.sh) - No Bash 3.2 compatibility violations (no associative arrays, no
readarray, no${var,,}) - Token refresh works mid-pipeline: simulate a 60-minute pipeline run and verify token is refreshed at least once
- Webhook rate limiting caps at 10 events/sec, excess events are queued or dropped with warning