mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-08 01:51:14 +00:00
docs: Rewrite trigger service skill for direct URL + bearer auth pattern
- Updated overview to reflect current architecture (public sprite URL with TRIGGER_SECRET bearer auth, not start service API) - Added Step 5 for setting sprite URL auth to public - Updated workflow template to use SPRITE_URL + TRIGGER_SECRET secrets - Updated secret naming convention to match deployed services - Added Step 10 for CLI-based end-to-end testing - Added "Current Deployed Services" reference table - Fixed all stale references and code samples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4c456df091
commit
fe9b6ad449
1 changed files with 119 additions and 147 deletions
|
|
@ -16,103 +16,71 @@ The user wants to set up a trigger service for: **$ARGUMENTS**
|
|||
|
||||
## Overview
|
||||
|
||||
Sprites pause when idle to save resources. This skill uses the [Sprite start service API](https://docs.sprites.dev/api/v001-rc30/services/#start-service) to wake the Sprite and run a service on demand:
|
||||
Sprites pause when idle to save resources. This skill sets up a trigger server that GitHub Actions can call to wake the Sprite and run a script:
|
||||
|
||||
```
|
||||
GitHub Actions (cron / events / manual)
|
||||
-> POST https://api.sprites.dev/v1/sprites/{name}/services/{service}/start
|
||||
-> Sprite API wakes the Sprite from pause
|
||||
-> Sprite starts the specified service (trigger-server.ts)
|
||||
-> trigger-server.ts runs the target script (single cycle, then exits)
|
||||
-> curl POST $SPRITE_URL/trigger (with Bearer token)
|
||||
-> Sprite wakes from pause (http-port auto-start)
|
||||
-> trigger-server.ts validates Bearer token
|
||||
-> target script runs (single cycle, then exits)
|
||||
-> Sprite goes idle again until next trigger
|
||||
```
|
||||
|
||||
The trigger server is configured with `--http-port 8080`, which makes the service auto-start when the Sprite receives incoming HTTP traffic.
|
||||
**How it works:**
|
||||
- The Sprite's public URL is set to `auth: "public"` so GitHub Actions can reach it
|
||||
- The trigger server listens on port 8080 with `--http-port 8080` (auto-starts on HTTP traffic)
|
||||
- A `TRIGGER_SECRET` bearer token protects the `/trigger` endpoint from unauthorized access
|
||||
- The Sprite URL + trigger secret are stored as GitHub Actions secrets
|
||||
|
||||
## Step 1: Ensure trigger-server.ts exists
|
||||
## Prerequisites
|
||||
|
||||
The trigger server is included in this skill directory at:
|
||||
- You are running Claude Code **inside a Sprite VM**
|
||||
- `sprite-env` commands are available
|
||||
- `bun` is installed (comes with Sprite by default)
|
||||
- `gh` CLI is installed and authenticated
|
||||
- Repository has write access for setting secrets
|
||||
- A `SPRITE_TOKEN` (from https://sprites.dev/account) for setting the Sprite's URL auth to public
|
||||
|
||||
## Step 1: Verify trigger-server.ts
|
||||
|
||||
The trigger server lives at:
|
||||
`/home/sprite/spawn/.claude/skills/setup-trigger-service/trigger-server.ts`
|
||||
|
||||
You can use this file directly, or copy it to your repo if needed. The file contents are:
|
||||
It reads two required env vars:
|
||||
- `TRIGGER_SECRET` — Bearer token for authenticating requests
|
||||
- `TARGET_SCRIPT` — Absolute path to the script to run on trigger
|
||||
|
||||
```typescript
|
||||
const PORT = 8080;
|
||||
const TARGET_SCRIPT = process.env.TARGET_SCRIPT ?? "";
|
||||
const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT ?? "3", 10);
|
||||
**Endpoints:**
|
||||
- `GET /health` → `{"status":"ok"}` (no auth, for health checks)
|
||||
- `POST /trigger` → validates `Authorization: Bearer <secret>`, runs target script in background
|
||||
|
||||
if (!TARGET_SCRIPT) {
|
||||
console.error("ERROR: TARGET_SCRIPT env var is required");
|
||||
process.exit(1);
|
||||
}
|
||||
**Responses:**
|
||||
- `200` — `{"triggered":true,"reason":"...","running":N,"max":N}` on success
|
||||
- `401` — `{"error":"unauthorized"}` if bearer token is wrong
|
||||
- `429` — `{"error":"max concurrent runs reached"}` if at limit (default 3, configurable via `MAX_CONCURRENT` env var)
|
||||
|
||||
let runningCount = 0;
|
||||
## Step 2: Generate a trigger secret
|
||||
|
||||
async function runScript(reason: string) {
|
||||
runningCount++;
|
||||
try {
|
||||
console.log(`[trigger] Running ${TARGET_SCRIPT} (reason=${reason}, concurrent=${runningCount}/${MAX_CONCURRENT})`);
|
||||
const proc = Bun.spawn(["bash", TARGET_SCRIPT], {
|
||||
cwd: TARGET_SCRIPT.substring(0, TARGET_SCRIPT.lastIndexOf("/")) || ".",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
});
|
||||
await proc.exited;
|
||||
console.log(`[trigger] ${TARGET_SCRIPT} finished (exit=${proc.exitCode})`);
|
||||
} catch (e) {
|
||||
console.error(`[trigger] ${TARGET_SCRIPT} failed:`, e);
|
||||
} finally {
|
||||
runningCount--;
|
||||
}
|
||||
}
|
||||
|
||||
const server = Bun.serve({
|
||||
port: PORT,
|
||||
async fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/health") {
|
||||
return Response.json({ status: "ok" });
|
||||
}
|
||||
|
||||
if (req.method === "POST" && url.pathname === "/trigger") {
|
||||
if (runningCount >= MAX_CONCURRENT) {
|
||||
return Response.json({ error: "max concurrent runs reached", running: runningCount, max: MAX_CONCURRENT }, { status: 429 });
|
||||
}
|
||||
const reason = url.searchParams.get("reason") ?? "manual";
|
||||
runScript(reason);
|
||||
return Response.json({ triggered: true, reason, running: runningCount, max: MAX_CONCURRENT });
|
||||
}
|
||||
|
||||
return Response.json({ error: "not found" }, { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`[trigger] Listening on port ${server.port}`);
|
||||
console.log(`[trigger] TARGET_SCRIPT=${TARGET_SCRIPT}`);
|
||||
console.log(`[trigger] MAX_CONCURRENT=${MAX_CONCURRENT}`);
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
**Key behaviors:**
|
||||
- `GET /health` -> `{"status":"ok"}` (for health checks)
|
||||
- `POST /trigger` -> runs target script in background (auth handled at Sprite API level)
|
||||
- Returns `429` if max concurrent runs reached (default 3, configurable via `MAX_CONCURRENT`)
|
||||
- Zero npm dependencies — uses only Bun built-ins
|
||||
Save this — you'll use it in Steps 3 and 5.
|
||||
|
||||
## Step 2: Create the wrapper script
|
||||
## Step 3: Create the wrapper script
|
||||
|
||||
Create a **gitignored** wrapper script that sets env vars and launches the server.
|
||||
|
||||
Create `start-<service-name>.sh` in the skill directory (e.g., `start-improve.sh`, `start-refactor.sh`):
|
||||
Create `start-<service-name>.sh` in the skill directory:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export TRIGGER_SECRET="<secret-from-step-2>"
|
||||
export TARGET_SCRIPT="/home/sprite/spawn/.claude/skills/setup-trigger-service/<target-script>.sh"
|
||||
exec bun run /home/sprite/spawn/.claude/skills/setup-trigger-service/trigger-server.ts
|
||||
```
|
||||
|
||||
**Note:** Authentication is handled at the Sprite API level (via `SPRITE_TOKEN`), so no secret is needed in the wrapper script.
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
|
|
@ -125,9 +93,9 @@ chmod +x /home/sprite/spawn/.claude/skills/setup-trigger-service/start-<service-
|
|||
.claude/skills/setup-trigger-service/start-*.sh
|
||||
```
|
||||
|
||||
All `start-*.sh` files in the skill directory should be gitignored.
|
||||
Wrapper scripts contain secrets and MUST NOT be committed.
|
||||
|
||||
## Step 3: Create the Sprite service
|
||||
## Step 4: Create the Sprite service
|
||||
|
||||
Register the trigger server as a Sprite service with HTTP port forwarding:
|
||||
|
||||
|
|
@ -152,8 +120,13 @@ sprite-env services list
|
|||
curl -sf http://localhost:8080/health
|
||||
# Expected: {"status":"ok"}
|
||||
|
||||
# Test trigger (no auth needed — auth is at the Sprite API level)
|
||||
curl -sf -X POST "http://localhost:8080/trigger?reason=test"
|
||||
# Test auth rejection
|
||||
curl -sf -o /dev/null -w "%{http_code}" -X POST http://localhost:8080/trigger
|
||||
# Expected: 401
|
||||
|
||||
# Test valid trigger
|
||||
curl -sf -X POST "http://localhost:8080/trigger?reason=test" \
|
||||
-H "Authorization: Bearer <secret-from-step-2>"
|
||||
# Expected: {"triggered":true,"reason":"test","running":1,"max":3}
|
||||
```
|
||||
|
||||
|
|
@ -163,11 +136,35 @@ curl -sf -X POST "http://localhost:8080/trigger?reason=test"
|
|||
sprite-env services list # List all services
|
||||
sprite-env services stop <service-name> # Stop
|
||||
sprite-env services start <service-name> # Start
|
||||
sprite-env services restart <service-name> # Restart
|
||||
sprite-env services delete <service-name> # Delete entirely
|
||||
```
|
||||
|
||||
## Step 4: Create the GitHub Actions workflow
|
||||
## Step 5: Set the Sprite URL to public
|
||||
|
||||
The Sprite URL must be publicly accessible so GitHub Actions can reach it. Use the Sprite API to set `url_settings.auth` to `"public"`:
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer $SPRITE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url_settings": {"auth": "public"}}' \
|
||||
"https://api.sprites.dev/v1/sprites/$SPRITE_NAME"
|
||||
```
|
||||
|
||||
**How to get the Sprite name and URL:**
|
||||
|
||||
```bash
|
||||
# Get sprite info (requires SPRITE_TOKEN env var)
|
||||
# The sprite name and URL are shown when you create the Sprite, or via:
|
||||
curl -s -H "Authorization: Bearer $SPRITE_TOKEN" \
|
||||
"https://api.sprites.dev/v1/sprites" | jq '.[] | {name, url}'
|
||||
```
|
||||
|
||||
If `SPRITE_TOKEN` is not set locally, ask the user to provide it or set it as an env var.
|
||||
|
||||
**IMPORTANT:** Setting auth to "public" means anyone with the URL can reach the server. This is safe because the trigger server requires a `TRIGGER_SECRET` bearer token — unauthorized requests get a 401.
|
||||
|
||||
## Step 6: Create the GitHub Actions workflow
|
||||
|
||||
Create `.github/workflows/<service-name>.yml`:
|
||||
|
||||
|
|
@ -176,12 +173,9 @@ name: Trigger <Service Name>
|
|||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */2 * * *' # Every 2 hours (adjust as needed)
|
||||
# Add event triggers as needed:
|
||||
# issues:
|
||||
# types: [opened, reopened]
|
||||
# push:
|
||||
# branches: [main]
|
||||
- cron: '*/30 * * * *' # Every 30 minutes (adjust as needed)
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
workflow_dispatch: # Always include for manual testing
|
||||
|
||||
concurrency:
|
||||
|
|
@ -194,65 +188,41 @@ jobs:
|
|||
steps:
|
||||
- name: Trigger <service-name> sprite
|
||||
env:
|
||||
SPRITE_TOKEN: ${{ secrets.SPRITE_TOKEN }}
|
||||
SPRITE_NAME: ${{ secrets.<SERVICE_NAME>_SPRITE_NAME }}
|
||||
SERVICE_NAME: ${{ secrets.<SERVICE_NAME>_SERVICE_NAME }}
|
||||
SPRITE_URL: ${{ secrets.<SERVICE_NAME>_SPRITE_URL }}
|
||||
TRIGGER_SECRET: ${{ secrets.<SERVICE_NAME>_TRIGGER_SECRET }}
|
||||
run: |
|
||||
curl -s -X POST "https://api.sprites.dev/v1/sprites/${SPRITE_NAME}/services/${SERVICE_NAME}/start?duration=0s" \
|
||||
-H "Authorization: Bearer ${SPRITE_TOKEN}"
|
||||
curl -sf -X POST "${SPRITE_URL}/trigger?reason=${{ github.event_name }}" \
|
||||
-H "Authorization: Bearer ${TRIGGER_SECRET}"
|
||||
```
|
||||
|
||||
**Note:** This uses the [Sprite start service API](https://docs.sprites.dev/api/v001-rc30/services/#start-service) which wakes the Sprite and starts the specified service automatically.
|
||||
|
||||
**Cron examples:**
|
||||
- `'*/30 * * * *'` — every 30 minutes
|
||||
- `'0 */2 * * *'` — every 2 hours
|
||||
- `'0 */6 * * *'` — every 6 hours
|
||||
- `'0 0 * * *'` — daily at midnight
|
||||
- `'*/30 * * * *'` — every 30 minutes
|
||||
|
||||
## Step 5: Set GitHub Actions secrets
|
||||
## Step 7: Set GitHub Actions secrets
|
||||
|
||||
Set three secrets per Sprite service. Use **namespaced** secret names to avoid collisions:
|
||||
|
||||
### 5a. Get the Sprite name and service name
|
||||
|
||||
The Sprite name and service name are needed to use the [Sprite start service API](https://docs.sprites.dev/api/v001-rc30/services/#start-service).
|
||||
Set two secrets per service. Use **namespaced** secret names to avoid collisions:
|
||||
|
||||
```bash
|
||||
# Get the sprite name (ask user if not available programmatically)
|
||||
# The sprite name is what you used when creating the Sprite
|
||||
# Set the Sprite's public URL
|
||||
printf '<sprite-url>' | gh secret set <SERVICE_NAME>_SPRITE_URL --repo <owner>/<repo>
|
||||
# Example: printf 'https://my-sprite-abc1.sprites.app' | gh secret set DISCOVERY_SPRITE_URL --repo OpenRouterTeam/spawn
|
||||
|
||||
# Get the service name (from Step 4)
|
||||
sprite-env services list
|
||||
# Use the name you registered in Step 3 (e.g., "improve-trigger")
|
||||
```
|
||||
|
||||
### 5b. Set the secrets
|
||||
|
||||
```bash
|
||||
# Set the sprite name (one per Sprite)
|
||||
gh secret set <SERVICE_NAME>_SPRITE_NAME --repo <owner>/<repo>
|
||||
# When prompted, enter the sprite name (e.g., "improve-sprite")
|
||||
|
||||
# Set the service name (matches the name from Step 4)
|
||||
gh secret set <SERVICE_NAME>_SERVICE_NAME --repo <owner>/<repo>
|
||||
# When prompted, enter the service name (e.g., "improve-trigger")
|
||||
|
||||
# Set the Sprite API token (shared across all services)
|
||||
# This should already be set as SPRITE_TOKEN
|
||||
gh secret set SPRITE_TOKEN --repo <owner>/<repo>
|
||||
# When prompted, paste your Sprite API token from https://sprites.dev
|
||||
# Set the trigger secret (from Step 2)
|
||||
printf '<secret-from-step-2>' | gh secret set <SERVICE_NAME>_TRIGGER_SECRET --repo <owner>/<repo>
|
||||
# Example: printf '61e6...' | gh secret set DISCOVERY_TRIGGER_SECRET --repo OpenRouterTeam/spawn
|
||||
```
|
||||
|
||||
**Secret naming convention:**
|
||||
|
||||
| Secret | Example | Purpose |
|
||||
|--------|---------|---------|
|
||||
| `SPRITE_TOKEN` | `SPRITE_TOKEN` | Sprite API bearer token (shared across all services) |
|
||||
| `<SERVICE>_SPRITE_NAME` | `IMPROVE_SPRITE_NAME` | Name of the Sprite running this service |
|
||||
| `<SERVICE>_SERVICE_NAME` | `IMPROVE_SERVICE_NAME` | Name of the service registered in Step 3 |
|
||||
| `<SERVICE>_SPRITE_URL` | `DISCOVERY_SPRITE_URL` | Public URL of the Sprite |
|
||||
| `<SERVICE>_TRIGGER_SECRET` | `DISCOVERY_TRIGGER_SECRET` | Bearer token for the trigger server |
|
||||
|
||||
## Step 6: Ensure the target script is single-cycle
|
||||
## Step 8: Ensure the target script is single-cycle
|
||||
|
||||
The target script (e.g., `refactor.sh`, `improve.sh`) MUST:
|
||||
|
||||
|
|
@ -266,9 +236,7 @@ If converting from a looping script, remove the `while true` / `sleep` and keep
|
|||
- `improve.sh` — Continuous improvement loop for spawn (already single-cycle ready)
|
||||
- `refactor.sh` — Refactoring team service (already single-cycle ready)
|
||||
|
||||
To create a new service script, add it to `/home/sprite/spawn/.claude/skills/setup-trigger-service/` and follow the same pattern.
|
||||
|
||||
## Step 7: Commit and push
|
||||
## Step 9: Commit and push
|
||||
|
||||
Commit the workflow file and .gitignore changes (but NOT the wrapper script):
|
||||
|
||||
|
|
@ -278,22 +246,27 @@ git commit -m "feat: Add GitHub Actions trigger for <service-name>"
|
|||
git push origin main
|
||||
```
|
||||
|
||||
Note: The trigger-server.ts and service scripts are already in the skill directory and don't need to be committed again.
|
||||
## Step 10: Test end-to-end
|
||||
|
||||
## Step 8: Test end-to-end
|
||||
```bash
|
||||
# Trigger manually via GitHub Actions
|
||||
gh workflow run <service-name>.yml --repo <owner>/<repo>
|
||||
|
||||
1. Go to the GitHub Actions tab
|
||||
2. Select the workflow
|
||||
3. Click "Run workflow" (workflow_dispatch)
|
||||
4. Verify the Sprite wakes and runs the target script
|
||||
# Watch the run
|
||||
gh run list --repo <owner>/<repo> --workflow <service-name>.yml --limit 1
|
||||
|
||||
# Check run logs
|
||||
gh run view <run-id> --repo <owner>/<repo> --log
|
||||
```
|
||||
|
||||
Verify the Sprite wakes, the trigger server accepts the request, and the target script runs.
|
||||
|
||||
## Multiple Services on Different Sprites
|
||||
|
||||
Each Sprite gets its own:
|
||||
- `start-<service-name>.sh` in the skill directory with its own `TARGET_SCRIPT`
|
||||
- `start-<service-name>.sh` wrapper with its own `TRIGGER_SECRET` and `TARGET_SCRIPT`
|
||||
- GitHub Actions workflow file
|
||||
- Pair of GitHub secrets (`<SERVICE>_SPRITE_NAME` + `<SERVICE>_SERVICE_NAME`)
|
||||
- All services share the `SPRITE_TOKEN` secret
|
||||
- Pair of GitHub secrets (`<SERVICE>_SPRITE_URL` + `<SERVICE>_TRIGGER_SECRET`)
|
||||
|
||||
The `trigger-server.ts` file is **shared** — same code runs on every Sprite, configured only by env vars.
|
||||
|
||||
|
|
@ -307,23 +280,22 @@ To add a new automation script (beyond improve.sh and refactor.sh):
|
|||
4. Create a corresponding `start-<script-name>.sh` wrapper with the appropriate env vars
|
||||
5. Follow the setup steps above to register the service and create the GitHub Actions workflow
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This skill assumes:
|
||||
- You are running Claude Code **inside a Sprite VM**
|
||||
- `sprite-env` commands are available
|
||||
- `bun` is installed (comes with Sprite by default)
|
||||
- `gh` CLI is installed and authenticated
|
||||
- Repository has write access for setting secrets
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| Service won't start | Check `sprite-env services list` — is another service using `--http-port 8080`? |
|
||||
| 401 on API call | Verify `SPRITE_TOKEN` is set correctly in GitHub secrets |
|
||||
| 401 on trigger | Verify `TRIGGER_SECRET` matches between wrapper script and GitHub secret |
|
||||
| curl exits with code 22 | The sprite URL may require auth — run Step 5 to set `auth: "public"` |
|
||||
| Script runs but nothing happens | Check the target script works standalone: `bash /path/to/script.sh` |
|
||||
| Sprite doesn't wake | Verify `<SERVICE>_SPRITE_NAME` and `<SERVICE>_SERVICE_NAME` secrets are correct |
|
||||
| `{"error":"max concurrent runs reached"}` | Max concurrent limit reached (default 3) — wait for runs to finish or increase MAX_CONCURRENT |
|
||||
| Sprite doesn't wake | Verify `<SERVICE>_SPRITE_URL` secret matches the Sprite's public URL |
|
||||
| `{"error":"max concurrent runs reached"}` | Max concurrent limit reached (default 3) — wait for runs to finish or increase `MAX_CONCURRENT` env var in wrapper script |
|
||||
| env vars not passed | Use the wrapper script pattern (not `--env` flag with commas in values) |
|
||||
| Service not found | Verify the service name matches what you registered in Step 3 with `sprite-env services create` |
|
||||
| GitHub Actions secret is empty | Check `gh secret list --repo <owner>/<repo>` and re-set with `printf` (not `echo`, to avoid trailing newline) |
|
||||
|
||||
## Current Deployed Services
|
||||
|
||||
| Workflow | Sprite | Service Name | Secrets |
|
||||
|----------|--------|-------------|---------|
|
||||
| `improve.yml` (Trigger Discovery) | `lab-spawn-discovery` | `improve-trigger` | `DISCOVERY_SPRITE_URL`, `DISCOVERY_TRIGGER_SECRET` |
|
||||
| `refactor.yml` (Trigger Refactor) | _(not yet configured)_ | _(not yet configured)_ | `REFACTOR_SPRITE_URL`, `REFACTOR_TRIGGER_SECRET` |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue