Skip to content

Tool description schema references #55

Tool description schema references

Tool description schema references #55

Workflow file for this run

name: 🔎 Preview
on:
pull_request:
branches:
- main
types:
- opened
- reopened
- synchronize
- ready_for_review
- closed
workflow_dispatch:
inputs:
action:
description: Deploy or cleanup preview resources
required: true
type: choice
default: deploy
options:
- deploy
- cleanup
target:
description: Preview naming strategy
required: true
type: choice
default: branch
options:
- branch
- pr
pr_number:
description: PR number (required when target=pr)
required: false
type: string
preview_name:
description: Optional preview name override (target=branch)
required: false
type: string
permissions:
contents: read
pull-requests: write
concurrency:
group: >-
preview-${{ github.event.pull_request.number || format('{0}-{1}',
inputs.target, inputs.target == 'pr' && inputs.pr_number ||
inputs.preview_name || github.ref_name) }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-latest
name: 🔎 Deploy Preview Resources
environment:
name:
preview-${{ github.event.pull_request.number || inputs.pr_number ||
github.run_id }}
url: ${{ steps.deploy_preview.outputs.url }}
if: >-
(github.event_name == 'pull_request' &&
github.event.action != 'closed' &&
github.event.pull_request.head.repo.fork == false &&
github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
inputs.action == 'deploy')
steps:
- name: 📦 Checkout
uses: actions/checkout@v4
- name: 🧰 Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: 📥 Install Dependencies
run: bun install --frozen-lockfile
- name: 🧾 Resolve preview names
id: names
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
INPUT_TARGET: ${{ inputs.target }}
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
INPUT_PREVIEW_NAME: ${{ inputs.preview_name }}
run: |
set -euo pipefail
APP_NAME="epicflare"
if [ "$EVENT_NAME" = "pull_request" ]; then
PREVIEW_KIND="pr"
PREVIEW_ID="$PR_NUMBER"
else
PREVIEW_KIND="$INPUT_TARGET"
PREVIEW_ID=""
if [ "$PREVIEW_KIND" = "pr" ]; then
PREVIEW_ID="$INPUT_PR_NUMBER"
if [ -z "$PREVIEW_ID" ]; then
echo "inputs.pr_number is required when inputs.target=pr" >&2
exit 1
fi
else
PREVIEW_ID="$INPUT_PREVIEW_NAME"
if [ -z "$PREVIEW_ID" ]; then
PREVIEW_ID="$REF_NAME"
fi
fi
fi
# Cloudflare Worker names must be URL-safe; normalize to lower-kebab-case.
slug="$(echo "$PREVIEW_ID" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g' | cut -c1-32)"
if [ -z "$slug" ]; then
slug="preview"
fi
if [ "$PREVIEW_KIND" = "pr" ]; then
APP_WORKER_NAME="${APP_NAME}-pr-${slug}"
else
APP_WORKER_NAME="${APP_NAME}-branch-${slug}"
fi
echo "preview_kind=$PREVIEW_KIND" >> "$GITHUB_OUTPUT"
echo "preview_id=$PREVIEW_ID" >> "$GITHUB_OUTPUT"
echo "worker_name=$APP_WORKER_NAME" >> "$GITHUB_OUTPUT"
- name: 🧱 Ensure preview resources (D1 + KV)
id: resources
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
APP_WORKER_NAME: ${{ steps.names.outputs.worker_name }}
run: |
set -euo pipefail
bun tools/ci/preview-resources.ts ensure --worker-name "$APP_WORKER_NAME" --out-config wrangler-preview.generated.json | tee -a "$GITHUB_OUTPUT"
- name: 🗄️ Apply D1 Migrations (preview)
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ENV: preview
WRANGLER_CONFIG: ${{ steps.resources.outputs.wrangler_config }}
run:
bun ./wrangler-env.ts d1 migrations apply APP_DB --remote --config
"$WRANGLER_CONFIG"
- name: 🧪 Deploy preview mock Workers
id: deploy_mocks
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
APP_WORKER_NAME: ${{ steps.names.outputs.worker_name }}
run: |
set -euo pipefail
MOCK_API_TOKEN="$(openssl rand -hex 32)"
echo "::add-mask::$MOCK_API_TOKEN"
OVERRIDES_FILE="preview-secrets-overrides.env"
: > "$OVERRIDES_FILE"
mock_summary=""
mock_comment_summary=""
for dir in mock-servers/*; do
if [ ! -d "$dir" ]; then
continue
fi
if [ ! -f "$dir/wrangler.jsonc" ]; then
continue
fi
service="$(basename "$dir")"
service_key="$(echo "$service" | tr '[:lower:]-' '[:upper:]_')"
mock_worker_name="${APP_WORKER_NAME}-mock-${service}"
deploy_log="deploy-mock-${service}.log"
bun tools/ci/sync-worker-secrets.ts --env "" --name "$mock_worker_name" --config "$dir/wrangler.jsonc" --set "MOCK_API_TOKEN=$MOCK_API_TOKEN"
bunx wrangler deploy --env preview --name "$mock_worker_name" --config "$dir/wrangler.jsonc" 2>&1 | tee "$deploy_log"
mock_url="$(node -e "const fs = require('node:fs'); const t = fs.readFileSync(process.argv[1],'utf8'); const m = t.match(/https:\\/\\/[a-zA-Z0-9._-]+\\.workers\\.dev/g); process.stdout.write(m?.at(-1) ?? '')" "$deploy_log")"
if [ -z "$mock_url" ]; then
echo "Failed to parse mock URL for $service." >&2
exit 1
fi
echo "${service_key}_API_BASE_URL=$mock_url" >> "$OVERRIDES_FILE"
echo "${service_key}_API_KEY=$MOCK_API_TOKEN" >> "$OVERRIDES_FILE"
mock_summary="${mock_summary}\n- ${service}: ${mock_url} (\`${mock_worker_name}\`)"
mock_comment_summary="${mock_comment_summary}\n- ${service}: [${mock_url}/__mocks](${mock_url}/__mocks?token=${MOCK_API_TOKEN}) (\`${mock_worker_name}\`)"
done
if [ -n "$mock_summary" ]; then
mock_summary="$(printf "%b" "$mock_summary" | sed '1{/^$/d;}')"
fi
if [ -n "$mock_comment_summary" ]; then
mock_comment_summary="$(printf "%b" "$mock_comment_summary" | sed '1{/^$/d;}')"
fi
{
echo "mock_summary<<EOF"
printf "%b\n" "$mock_summary"
echo "EOF"
echo "mock_comment_summary<<EOF"
printf "%b\n" "$mock_comment_summary"
echo "EOF"
echo "overrides_file=$OVERRIDES_FILE"
} >> "$GITHUB_OUTPUT"
- name: 🔐 Sync preview app Worker secrets (bulk)
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
APP_WORKER_NAME: ${{ steps.names.outputs.worker_name }}
OVERRIDES_FILE: ${{ steps.deploy_mocks.outputs.overrides_file }}
run: |
set -euo pipefail
ENV_FILE=".env.example"
if [ ! -f "$ENV_FILE" ]; then
echo "Missing $ENV_FILE (required for preview deploy secrets)." >&2
exit 1
fi
if [ ! -f "$OVERRIDES_FILE" ]; then
echo "Missing overrides file ($OVERRIDES_FILE) from mock deploy step." >&2
exit 1
fi
bun tools/ci/sync-worker-secrets.ts --env "" --name "$APP_WORKER_NAME" --from-dotenv "$ENV_FILE" --from-dotenv "$OVERRIDES_FILE" --generate-cookie-secret --include-empty
- name: ☁️ Deploy preview app Worker
id: deploy_preview
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ENV: preview
APP_WORKER_NAME: ${{ steps.names.outputs.worker_name }}
WRANGLER_CONFIG: ${{ steps.resources.outputs.wrangler_config }}
run: |
set -euo pipefail
echo "worker_name=$APP_WORKER_NAME" >> "$GITHUB_OUTPUT"
bun run deploy -- --name "$APP_WORKER_NAME" --config "$WRANGLER_CONFIG" 2>&1 | tee deploy.log
PREVIEW_URL="$(node -e "const fs = require('node:fs'); const t = fs.readFileSync('deploy.log','utf8'); const m = t.match(/https:\\/\\/[a-zA-Z0-9._-]+\\.workers\\.dev/g); process.stdout.write(m?.at(-1) ?? '')")"
if [ -n "$PREVIEW_URL" ]; then
echo "url=$PREVIEW_URL" >> "$GITHUB_OUTPUT"
fi
- name: 🩺 Healthcheck (preview)
shell: bash
env:
PREVIEW_URL: ${{ steps.deploy_preview.outputs.url }}
run: |
set -euo pipefail
if [ -z "${PREVIEW_URL:-}" ]; then
echo "Missing preview URL output; cannot run healthcheck." >&2
exit 1
fi
HEALTHCHECK_URL="${PREVIEW_URL%/}/health"
echo "Healthcheck URL: $HEALTHCHECK_URL"
attempts=30
delay_seconds=3
for i in $(seq 1 "$attempts"); do
echo "Attempt $i/$attempts"
if curl --fail --silent --show-error --location --max-time 10 \
--header "Accept: application/json" \
"$HEALTHCHECK_URL" > health.json && \
node -e "const fs = require('node:fs'); const body = fs.readFileSync('health.json','utf8'); const json = JSON.parse(body); if (json?.ok !== true) { console.error('healthcheck-unexpected-response', json); process.exit(1); } console.log('healthcheck-ok', json);"; then
exit 0
fi
sleep "$delay_seconds"
done
echo "Healthcheck failed after ${attempts} attempts: $HEALTHCHECK_URL" >&2
if [ -f health.json ]; then
echo "Last response body:" >&2
cat health.json >&2
fi
exit 1
- name: 🧾 Write preview link summary
if: steps.deploy_preview.outputs.url != ''
env:
PREVIEW_URL: ${{ steps.deploy_preview.outputs.url }}
WORKER_NAME: ${{ steps.deploy_preview.outputs.worker_name }}
MOCK_SUMMARY: ${{ steps.deploy_mocks.outputs.mock_summary }}
D1_DATABASE_NAME: ${{ steps.resources.outputs.d1_database_name }}
OAUTH_KV_TITLE: ${{ steps.resources.outputs.oauth_kv_title }}
run: |
{
echo "### Preview"
echo ""
echo "$PREVIEW_URL"
echo ""
printf 'Worker: `%s`\n' "$WORKER_NAME"
if [ -n "${D1_DATABASE_NAME:-}" ]; then
printf 'D1: `%s`\n' "$D1_DATABASE_NAME"
fi
if [ -n "${OAUTH_KV_TITLE:-}" ]; then
printf 'KV: `%s`\n' "$OAUTH_KV_TITLE"
fi
if [ -n "${MOCK_SUMMARY:-}" ]; then
echo ""
echo "### Mocks"
printf '%s\n' "$MOCK_SUMMARY"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: 💬 Comment Preview URL on PR
if: >-
github.event_name == 'pull_request' &&
steps.deploy_preview.outputs.url != ''
uses: actions/github-script@v7
env:
PREVIEW_URL: ${{ steps.deploy_preview.outputs.url }}
WORKER_NAME: ${{ steps.deploy_preview.outputs.worker_name }}
MOCK_SUMMARY: ${{ steps.deploy_mocks.outputs.mock_comment_summary }}
D1_DATABASE_NAME: ${{ steps.resources.outputs.d1_database_name }}
OAUTH_KV_TITLE: ${{ steps.resources.outputs.oauth_kv_title }}
with:
script: |
const marker = "<!-- epicflare-preview-url -->";
const bodyLines = [
marker,
`🔎 Preview deployed: ${process.env.PREVIEW_URL}`,
"",
`Worker: \`${process.env.WORKER_NAME}\``,
];
if (process.env.D1_DATABASE_NAME?.trim()) {
bodyLines.push(`D1: \`${process.env.D1_DATABASE_NAME.trim()}\``);
}
if (process.env.OAUTH_KV_TITLE?.trim()) {
bodyLines.push(`KV: \`${process.env.OAUTH_KV_TITLE.trim()}\``);
}
if (process.env.MOCK_SUMMARY?.trim()) {
bodyLines.push("");
bodyLines.push("Mocks:");
bodyLines.push(process.env.MOCK_SUMMARY.trim());
}
const body = bodyLines.join("\n");
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const existing = comments.find((c) => c.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
cleanup:
runs-on: ubuntu-latest
name: 🧹 Cleanup Preview Resources
if: >-
(github.event_name == 'pull_request' &&
github.event.action == 'closed' &&
github.event.pull_request.head.repo.fork == false) ||
(github.event_name == 'workflow_dispatch' &&
inputs.action == 'cleanup')
steps:
- name: 📦 Checkout
uses: actions/checkout@v4
- name: 🧰 Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: 📥 Install Dependencies
run: bun install --frozen-lockfile
- name: 🧾 Resolve preview names
id: names
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
INPUT_TARGET: ${{ inputs.target }}
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
INPUT_PREVIEW_NAME: ${{ inputs.preview_name }}
run: |
set -euo pipefail
APP_NAME="epicflare"
if [ "$EVENT_NAME" = "pull_request" ]; then
PREVIEW_KIND="pr"
PREVIEW_ID="$PR_NUMBER"
else
PREVIEW_KIND="$INPUT_TARGET"
PREVIEW_ID=""
if [ "$PREVIEW_KIND" = "pr" ]; then
PREVIEW_ID="$INPUT_PR_NUMBER"
if [ -z "$PREVIEW_ID" ]; then
echo "inputs.pr_number is required when inputs.target=pr" >&2
exit 1
fi
else
PREVIEW_ID="$INPUT_PREVIEW_NAME"
if [ -z "$PREVIEW_ID" ]; then
PREVIEW_ID="$REF_NAME"
fi
fi
fi
slug="$(echo "$PREVIEW_ID" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g' | cut -c1-32)"
if [ -z "$slug" ]; then
slug="preview"
fi
if [ "$PREVIEW_KIND" = "pr" ]; then
APP_WORKER_NAME="${APP_NAME}-pr-${slug}"
else
APP_WORKER_NAME="${APP_NAME}-branch-${slug}"
fi
echo "worker_name=$APP_WORKER_NAME" >> "$GITHUB_OUTPUT"
- name: 🗑️ Delete preview Workers
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
APP_WORKER_NAME: ${{ steps.names.outputs.worker_name }}
run: |
set -euo pipefail
delete_worker() {
local name="$1"
local output=""
set +e
output="$(bunx wrangler delete "$name" --env preview --force 2>&1)"
local exit_code="$?"
set -e
if [ "$exit_code" -eq 0 ]; then
echo "$output"
return 0
fi
local lower
lower="$(echo "$output" | tr '[:upper:]' '[:lower:]')"
if [[ "$lower" == *"not found"* ]] || [[ "$lower" == *"no such"* ]] || [[ "$lower" == *"does not exist"* ]]; then
echo "Worker $name already deleted."
return 0
fi
echo "$output" >&2
return "$exit_code"
}
delete_worker "$APP_WORKER_NAME"
for dir in mock-servers/*; do
if [ ! -d "$dir" ]; then
continue
fi
if [ ! -f "$dir/wrangler.jsonc" ]; then
continue
fi
service="$(basename "$dir")"
mock_worker_name="${APP_WORKER_NAME}-mock-${service}"
delete_worker "$mock_worker_name"
done
- name: 🧹 Delete preview resources (D1 + KV)
if: always()
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
APP_WORKER_NAME: ${{ steps.names.outputs.worker_name }}
run: |
set -euo pipefail
bun tools/ci/preview-resources.ts cleanup --worker-name "$APP_WORKER_NAME"