Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Playbook — Automating Your Workflow (for DevOps)

This playbook provides seven ready-to-use automation recipes for DevOps and platform engineers. Each recipe is a complete, copy-pasteable shell script that solves a real operational problem using the Alloy API.

For scripting fundamentals (capturing IDs, chaining requests, pagination), see the API Automation Tutorial. For deployment and API key setup, see For DevOps & Platform Teams.

Prerequisites

All recipes assume these environment variables are set:

BASE_URL="http://localhost:3000"

Register a user and capture credentials:

curl -s -X POST "$BASE_URL/api/v1/auth/register" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "devops-auto@alloy.dev",
    "password": "devops-auto-pass1",
    "display_name": "DevOps Automation"
  }' | jq .
{
  "user_id": "...",
  "email": "devops-auto@alloy.dev",
  "display_name": "DevOps Automation",
  "access_token": "..."
}

Save the token:

TOKEN="<access_token from above>"
USER_ID="<user_id from above>"

Create an organization for automation:

curl -s -X POST "$BASE_URL/api/v1/orgs" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "DevOps Automation Org",
    "slug": "devops-auto-org"
  }' | jq .
{
  "id": "...",
  "name": "DevOps Automation Org",
  "slug": "devops-auto-org"
}

Create a project:

curl -s -X POST "$BASE_URL/api/v1/projects" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "{
    \"org_id\": \"$ORG_ID\",
    \"key\": \"INFRA\",
    \"name\": \"Infra Platform\"
  }" | jq .
{
  "id": "...",
  "org_id": "...",
  "key": "INFRA",
  "name": "Infra Platform",
  "ticket_counter": 0
}

Recipe 1: Health Check Monitor

Monitor Alloy availability from a cron job or CI step. The /health endpoint is unauthenticated and returns the service status and database backend.

curl -s "$BASE_URL/health" | jq .
{
  "status": "ok",
  "service": "alloy",
  "version": "0.1.0",
  "database": "...",
  "db_healthy": true,
  "migration_version": "...",
  "uptime_seconds": "..."
}

Full monitoring script (use plain code blocks for scripts with logic):

#!/bin/bash
# health-monitor.sh — Exit 1 if Alloy is unhealthy
set -euo pipefail

ALLOY_URL="${ALLOY_URL:-http://localhost:3000}"
STATUS=$(curl -sf "$ALLOY_URL/health" | jq -r '.status')

if [ "$STATUS" != "ok" ]; then
  echo "ALERT: Alloy health check failed — status=$STATUS"
  exit 1
fi

echo "OK: Alloy healthy"

Wire this into cron (*/5 * * * *), Kubernetes liveness probes, or your uptime monitor.


Recipe 2: Automated Ticket Creation from CI

Create a ticket automatically when a CI pipeline detects an issue — for example, a failed deployment or a flaky test suite.

Create a ticket in the project:

curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "title": "CI: Deploy failed on staging",
    "description": "Automated ticket from CI pipeline. Build #1234 failed during database migration step.",
    "priority": "High",
    "reporter_id": "'$USER_ID'"
  }' | jq .
{
  "id": "...",
  "project_id": "...",
  "title": "CI: Deploy failed on staging",
  "description": "Automated ticket from CI pipeline. Build #1234 failed during database migration step.",
  "priority": "High",
  "status": "..."
}

Add a comment with build details:

curl -s -X POST "$BASE_URL/api/v1/tickets/$TICKET_ID/comments" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "body": "Build log: deployment failed at migration step V42. Rollback initiated.",
    "author_id": "'$USER_ID'"
  }' | jq .
{
  "id": "...",
  "ticket_id": "...",
  "body": "Build log: deployment failed at migration step V42. Rollback initiated.",
  "author_id": "..."
}

Full CI script:

#!/bin/bash
# create-incident-ticket.sh — Create a ticket when deploy fails
set -euo pipefail

ALLOY_URL="${ALLOY_URL:-http://localhost:3000}"
ALLOY_TOKEN="${ALLOY_TOKEN}"
ALLOY_PROJECT_ID="${ALLOY_PROJECT_ID}"
ALLOY_USER_ID="${ALLOY_USER_ID}"
BUILD_NUMBER="${BUILD_NUMBER:-unknown}"
FAILURE_REASON="${FAILURE_REASON:-unspecified}"

TICKET_ID=$(curl -sf -X POST "$ALLOY_URL/api/v1/projects/$ALLOY_PROJECT_ID/tickets" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALLOY_TOKEN" \
  -d "{
    \"title\": \"CI: Build #$BUILD_NUMBER failed\",
    \"description\": \"Automated incident ticket from CI.\",
    \"priority\": \"High\",
    \"reporter_id\": \"$ALLOY_USER_ID\"
  }" | jq -r '.id')

curl -sf -X POST "$ALLOY_URL/api/v1/tickets/$TICKET_ID/comments" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALLOY_TOKEN" \
  -d "{
    \"body\": \"Failure reason: $FAILURE_REASON\",
    \"author_id\": \"$ALLOY_USER_ID\"
  }" > /dev/null

echo "Created incident ticket: $TICKET_ID"

Recipe 3: API Key Rotation

Rotate API keys periodically. Create a new key, update your secrets manager, then revoke the old one.

Create a new API key:

curl -s -X POST "$BASE_URL/api/v1/api-keys" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "Rotated CI Key",
    "scopes": ["read", "write"]
  }' | jq .
{
  "id": "...",
  "name": "Rotated CI Key",
  "key": "...",
  "key_prefix": "...",
  "scopes": ["read", "write"],
  "project_ids": [],
  "created_at": "...",
  "expires_at": null
}

List existing keys to find old ones:

curl -s "$BASE_URL/api/v1/api-keys" \
  -H "Authorization: Bearer $TOKEN" | jq .
[
  {
    "id": "...",
    "name": "Rotated CI Key",
    "key_prefix": "...",
    "scopes": ["read", "write"],
    "project_ids": [],
    "created_at": "...",
    "last_used_at": null,
    "expires_at": null
  }
]

Revoke the old key by deleting it (returns 204 No Content):

curl -s -X DELETE "$BASE_URL/api/v1/api-keys/$API_KEY_ID" \
  -H "Authorization: Bearer $TOKEN" -w "%{http_code}" -o /dev/null

Full rotation script:

#!/bin/bash
# rotate-api-key.sh — Create new key, output it, revoke old key
set -euo pipefail

ALLOY_URL="${ALLOY_URL:-http://localhost:3000}"
ALLOY_TOKEN="${ALLOY_TOKEN}"
OLD_KEY_ID="${1:?Usage: rotate-api-key.sh <old-key-id>}"

# Create new key
NEW_KEY_JSON=$(curl -sf -X POST "$ALLOY_URL/api/v1/api-keys" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALLOY_TOKEN" \
  -d '{"name": "Rotated CI Key", "scopes": ["read", "write"]}')

NEW_KEY=$(echo "$NEW_KEY_JSON" | jq -r '.key')
echo "New key created: $(echo "$NEW_KEY_JSON" | jq -r '.key_prefix')..."

# Store the new key in your secrets manager here
# e.g., vault kv put secret/alloy-api-key value="$NEW_KEY"

# Revoke old key
curl -sf -X DELETE "$ALLOY_URL/api/v1/api-keys/$OLD_KEY_ID" \
  -H "Authorization: Bearer $ALLOY_TOKEN"

echo "Old key $OLD_KEY_ID revoked"

Recipe 4: Webhook Setup for Event-Driven Pipelines

Set up webhooks to trigger external systems when tickets change. Use this to drive CI/CD pipelines, Slack notifications, or audit logs.

Create a webhook for ticket events:

curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/webhooks" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://hooks.example.com/alloy-auto",
    "event_types": ["ticket.created", "ticket.updated", "ticket.status_changed"]
  }' | jq .
{
  "id": "...",
  "org_id": "...",
  "url": "https://hooks.example.com/alloy-auto",
  "secret": "...",
  "event_types": ["ticket.created", "ticket.updated", "ticket.status_changed"],
  "active": true,
  "created_at": "...",
  "updated_at": "..."
}

Save the secret — it is only shown at creation time. Use it to verify webhook signatures via the X-Alloy-Signature header (HMAC-SHA256).

List webhooks to confirm:

curl -s "$BASE_URL/api/v1/orgs/$ORG_ID/webhooks" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "org_id": "...",
      "url": "https://hooks.example.com/alloy-auto",
      "event_types": ["ticket.created", "ticket.updated", "ticket.status_changed"],
      "active": true,
      "created_at": "...",
      "updated_at": "..."
    }
  ],
  "next_cursor": null
}

Check delivery status:

curl -s "$BASE_URL/api/v1/webhooks/$WEBHOOK_ID/deliveries" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [],
  "next_cursor": null
}

Recipe 5: Sprint Automation

Automate sprint lifecycle — create a sprint, add tickets, start it, and close it when the iteration is done.

Create a sprint:

curl -s -X POST "$BASE_URL/api/v1/projects/$PROJECT_ID/sprints" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "name": "Auto Sprint 1",
    "start_date": "2026-04-01",
    "end_date": "2026-04-15"
  }' | jq .
{
  "id": "...",
  "project_id": "...",
  "name": "Auto Sprint 1",
  "start_date": "2026-04-01",
  "end_date": "2026-04-15",
  "status": "Planned"
}

List tickets in the project to find one for the sprint:

curl -s "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets?limit=5" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "project_id": "...",
      "title": "...",
      "status": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Start the sprint:

curl -s -X POST "$BASE_URL/api/v1/sprints/$SPRINT_ID/start" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "id": "...",
  "name": "Auto Sprint 1",
  "status": "Active"
}

Full sprint lifecycle script:

#!/bin/bash
# sprint-lifecycle.sh — Create, populate, and start a sprint
set -euo pipefail

ALLOY_URL="${ALLOY_URL:-http://localhost:3000}"
ALLOY_TOKEN="${ALLOY_TOKEN}"
PROJECT_ID="${ALLOY_PROJECT_ID}"
START_DATE=$(date -u +%Y-%m-%d)
END_DATE=$(date -u -v+14d +%Y-%m-%d 2>/dev/null || date -u -d "+14 days" +%Y-%m-%d)

SPRINT_ID=$(curl -sf -X POST "$ALLOY_URL/api/v1/projects/$PROJECT_ID/sprints" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ALLOY_TOKEN" \
  -d "{\"name\": \"Sprint $(date +%V)\", \"start_date\": \"$START_DATE\", \"end_date\": \"$END_DATE\"}" \
  | jq -r '.id')

echo "Created sprint: $SPRINT_ID"

# Assign unassigned tickets to the sprint
TICKET_IDS=$(curl -sf "$ALLOY_URL/api/v1/projects/$PROJECT_ID/tickets?limit=50" \
  -H "Authorization: Bearer $ALLOY_TOKEN" \
  | jq -r '.items[] | select(.sprint_id == null) | .id')

for TID in $TICKET_IDS; do
  curl -sf -X PATCH "$ALLOY_URL/api/v1/tickets/$TID" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $ALLOY_TOKEN" \
    -d "{\"sprint_id\": \"$SPRINT_ID\"}" > /dev/null
  echo "  Assigned ticket $TID"
done

# Start the sprint
curl -sf -X POST "$ALLOY_URL/api/v1/sprints/$SPRINT_ID/start" \
  -H "Authorization: Bearer $ALLOY_TOKEN" > /dev/null

echo "Sprint started"

Recipe 6: Bulk Ticket Updates

Update multiple tickets at once — for example, re-prioritize all open tickets or move them to a new status when plans change.

List tickets in the project:

curl -s "$BASE_URL/api/v1/projects/$PROJECT_ID/tickets?limit=20" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "project_id": "...",
      "ticket_number": "...",
      "title": "...",
      "status": "...",
      "priority": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Update a ticket’s priority:

curl -s -X PATCH "$BASE_URL/api/v1/tickets/$TICKET_ID" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "priority": "Urgent"
  }' | jq .
{
  "id": "...",
  "title": "...",
  "status": "...",
  "priority": "Urgent"
}

Full bulk update script:

#!/bin/bash
# bulk-reprioritize.sh — Set all open tickets to a given priority
set -euo pipefail

ALLOY_URL="${ALLOY_URL:-http://localhost:3000}"
ALLOY_TOKEN="${ALLOY_TOKEN}"
PROJECT_ID="${ALLOY_PROJECT_ID}"
NEW_PRIORITY="${1:-Medium}"

TICKET_IDS=$(curl -sf "$ALLOY_URL/api/v1/projects/$PROJECT_ID/tickets?limit=100" \
  -H "Authorization: Bearer $ALLOY_TOKEN" \
  | jq -r '.items[] | select(.status != "Done") | .id')

COUNT=0
for TID in $TICKET_IDS; do
  curl -sf -X PATCH "$ALLOY_URL/api/v1/tickets/$TID" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $ALLOY_TOKEN" \
    -d "{\"priority\": \"$NEW_PRIORITY\"}" > /dev/null
  COUNT=$((COUNT + 1))
done

echo "Updated $COUNT tickets to priority=$NEW_PRIORITY"

Recipe 7: Scheduled Reporting

Generate reports on a schedule for stakeholders — capitalization data, sprint burndowns, or time tracking summaries.

Create a label for tracking automation:

curl -s -X POST "$BASE_URL/api/v1/orgs/$ORG_ID/labels" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "automated-report", "color": "#6366f1"}' | jq .
{
  "id": "...",
  "name": "automated-report",
  "color": "#6366f1"
}

List projects to get current status:

curl -s "$BASE_URL/api/v1/projects?org_id=$ORG_ID" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "name": "...",
      "org_id": "..."
    }
  ],
  "next_cursor": null,
  "has_more": false
}

Full reporting script:

#!/bin/bash
# weekly-report.sh — Generate a weekly project status report
set -euo pipefail

ALLOY_URL="${ALLOY_URL:-http://localhost:3000}"
ALLOY_TOKEN="${ALLOY_TOKEN}"
PROJECT_ID="${ALLOY_PROJECT_ID}"
REPORT_FILE="alloy-report-$(date +%Y%m%d).json"

echo '{"generated_at": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",' > "$REPORT_FILE"

# Project info
echo '"project":' >> "$REPORT_FILE"
curl -sf "$ALLOY_URL/api/v1/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $ALLOY_TOKEN" >> "$REPORT_FILE"
echo ',' >> "$REPORT_FILE"

# Open tickets
echo '"tickets":' >> "$REPORT_FILE"
curl -sf "$ALLOY_URL/api/v1/projects/$PROJECT_ID/tickets?limit=100" \
  -H "Authorization: Bearer $ALLOY_TOKEN" >> "$REPORT_FILE"
echo ',' >> "$REPORT_FILE"

# Active sprints
echo '"sprints":' >> "$REPORT_FILE"
curl -sf "$ALLOY_URL/api/v1/projects/$PROJECT_ID/sprints" \
  -H "Authorization: Bearer $ALLOY_TOKEN" >> "$REPORT_FILE"

echo '}' >> "$REPORT_FILE"

echo "Report saved to $REPORT_FILE"
echo "Tickets: $(jq '.tickets.items | length' "$REPORT_FILE")"

Next Steps