251 lines
9.5 KiB
YAML
251 lines
9.5 KiB
YAML
name: Deploy
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
inputs:
|
||
target:
|
||
description: "What to deploy"
|
||
required: true
|
||
default: full
|
||
type: choice
|
||
options:
|
||
- full
|
||
- backend
|
||
- frontend
|
||
allow_deleting_large_indexes:
|
||
description: "Allow Convex to delete large indexes"
|
||
required: true
|
||
default: false
|
||
type: boolean
|
||
|
||
concurrency:
|
||
group: deploy-production
|
||
cancel-in-progress: true
|
||
|
||
permissions:
|
||
contents: write
|
||
statuses: read
|
||
|
||
jobs:
|
||
validate-deploy-request:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
outputs:
|
||
deploy_backend: ${{ steps.mode.outputs.deploy_backend }}
|
||
deploy_frontend: ${{ steps.mode.outputs.deploy_frontend }}
|
||
run_smoke: ${{ steps.mode.outputs.run_smoke }}
|
||
target: ${{ steps.mode.outputs.target }}
|
||
steps:
|
||
- name: Require main ref for production deploy
|
||
run: |
|
||
set -euo pipefail
|
||
if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then
|
||
echo "Production deploys must run from main."
|
||
exit 1
|
||
fi
|
||
|
||
- name: Resolve deploy mode
|
||
id: mode
|
||
run: |
|
||
set -euo pipefail
|
||
target="${{ inputs.target }}"
|
||
case "$target" in
|
||
full)
|
||
echo "deploy_backend=true" >> "$GITHUB_OUTPUT"
|
||
echo "deploy_frontend=true" >> "$GITHUB_OUTPUT"
|
||
echo "run_smoke=true" >> "$GITHUB_OUTPUT"
|
||
;;
|
||
backend)
|
||
echo "deploy_backend=true" >> "$GITHUB_OUTPUT"
|
||
echo "deploy_frontend=false" >> "$GITHUB_OUTPUT"
|
||
echo "run_smoke=true" >> "$GITHUB_OUTPUT"
|
||
;;
|
||
frontend)
|
||
echo "deploy_backend=false" >> "$GITHUB_OUTPUT"
|
||
echo "deploy_frontend=true" >> "$GITHUB_OUTPUT"
|
||
echo "run_smoke=true" >> "$GITHUB_OUTPUT"
|
||
;;
|
||
*)
|
||
echo "Unsupported deploy target: $target" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||
|
||
deploy-production:
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 45
|
||
needs: validate-deploy-request
|
||
environment:
|
||
name: Production
|
||
url: https://clawhub.ai
|
||
env:
|
||
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
|
||
PLAYWRIGHT_AUTH_STORAGE_STATE_JSON: ${{ secrets.PLAYWRIGHT_AUTH_STORAGE_STATE_JSON }}
|
||
PLAYWRIGHT_BASE_URL: https://clawhub.ai
|
||
steps:
|
||
- name: Check deploy configuration
|
||
run: |
|
||
set -euo pipefail
|
||
missing=()
|
||
|
||
if [[ "${{ needs.validate-deploy-request.outputs.deploy_backend }}" == "true" && -z "$CONVEX_DEPLOY_KEY" ]]; then
|
||
missing+=("CONVEX_DEPLOY_KEY")
|
||
fi
|
||
|
||
if (( ${#missing[@]} > 0 )); then
|
||
echo "::error::Missing required production environment secrets: ${missing[*]}"
|
||
exit 1
|
||
fi
|
||
|
||
echo "Deploy target: ${{ needs.validate-deploy-request.outputs.target }}"
|
||
echo "Allow deleting large Convex indexes: ${{ inputs.allow_deleting_large_indexes }}"
|
||
|
||
if [[ -z "$PLAYWRIGHT_AUTH_STORAGE_STATE_JSON" ]]; then
|
||
echo "PLAYWRIGHT_AUTH_STORAGE_STATE_JSON not set; authenticated smoke will be skipped."
|
||
fi
|
||
|
||
- uses: actions/checkout@v6
|
||
|
||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
|
||
with:
|
||
bun-version: 1.3.10
|
||
|
||
- name: Install
|
||
run: bun install --frozen-lockfile
|
||
|
||
- name: Stamp Convex build SHA
|
||
if: needs.validate-deploy-request.outputs.deploy_backend == 'true'
|
||
run: bunx convex env set APP_BUILD_SHA "${GITHUB_SHA}" --prod
|
||
|
||
- name: Stamp Convex deploy time
|
||
if: needs.validate-deploy-request.outputs.deploy_backend == 'true'
|
||
run: bunx convex env set APP_DEPLOYED_AT "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" --prod
|
||
|
||
- name: Deploy Convex
|
||
if: needs.validate-deploy-request.outputs.deploy_backend == 'true'
|
||
run: |
|
||
set -euo pipefail
|
||
if [[ "${{ inputs.allow_deleting_large_indexes }}" == "true" ]]; then
|
||
bunx convex deploy --typecheck=disable --yes --allow-deleting-large-indexes
|
||
else
|
||
bun run convex:deploy
|
||
fi
|
||
|
||
- name: Verify Convex contract
|
||
if: needs.validate-deploy-request.outputs.deploy_backend == 'true'
|
||
run: bun run verify:convex-contract -- --prod
|
||
|
||
- name: Wait for Vercel production deployment
|
||
id: vercel
|
||
if: needs.validate-deploy-request.outputs.deploy_frontend == 'true'
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||
GITHUB_SHA: ${{ github.sha }}
|
||
VERCEL_STATUS_CONTEXT: Vercel – clawhub
|
||
run: |
|
||
set -euo pipefail
|
||
for attempt in {1..90}; do
|
||
if ! status_json="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/status" \
|
||
--jq '.statuses[] | select(.context == env.VERCEL_STATUS_CONTEXT) | {state, target_url, description} | @base64' \
|
||
2>/dev/null | head -n1)"; then
|
||
echo "GitHub status check failed for $GITHUB_SHA on attempt $attempt; retrying..."
|
||
sleep 10
|
||
continue
|
||
fi
|
||
|
||
if [[ -z "$status_json" ]]; then
|
||
state=""
|
||
target_url=""
|
||
else
|
||
state="$(printf '%s' "$status_json" | base64 -d | jq -r '.state // ""')"
|
||
target_url="$(printf '%s' "$status_json" | base64 -d | jq -r '.target_url // ""')"
|
||
fi
|
||
|
||
case "$state" in
|
||
success)
|
||
echo "Vercel production deployment ready for $GITHUB_SHA"
|
||
echo "deployment_url=$target_url" >> "$GITHUB_OUTPUT"
|
||
exit 0
|
||
;;
|
||
failure|error)
|
||
echo "::error::Vercel production deployment failed for $GITHUB_SHA"
|
||
exit 1
|
||
;;
|
||
pending)
|
||
echo "Vercel deployment pending for $GITHUB_SHA on attempt $attempt; waiting..."
|
||
;;
|
||
*)
|
||
echo "Vercel status for $GITHUB_SHA not published yet on attempt $attempt; waiting..."
|
||
;;
|
||
esac
|
||
|
||
sleep 10
|
||
done
|
||
|
||
echo "::error::Timed out waiting for Vercel production deployment for $GITHUB_SHA"
|
||
exit 1
|
||
|
||
- name: Install Playwright browser
|
||
if: needs.validate-deploy-request.outputs.run_smoke == 'true' && needs.validate-deploy-request.outputs.deploy_frontend == 'true'
|
||
run: bunx playwright install --with-deps chromium webkit
|
||
|
||
- name: Smoke test production HTTP
|
||
if: needs.validate-deploy-request.outputs.run_smoke == 'true'
|
||
run: bun run test:e2e:prod-http
|
||
|
||
- name: Write authenticated storage state
|
||
if: needs.validate-deploy-request.outputs.run_smoke == 'true' && needs.validate-deploy-request.outputs.deploy_frontend == 'true' && env.PLAYWRIGHT_AUTH_STORAGE_STATE_JSON != ''
|
||
run: |
|
||
echo "$PLAYWRIGHT_AUTH_STORAGE_STATE_JSON" > "$RUNNER_TEMP/playwright-auth.json"
|
||
echo "PLAYWRIGHT_AUTH_STORAGE_STATE=$RUNNER_TEMP/playwright-auth.json" >> "$GITHUB_ENV"
|
||
|
||
- name: Smoke test production UI
|
||
if: needs.validate-deploy-request.outputs.run_smoke == 'true' && needs.validate-deploy-request.outputs.deploy_frontend == 'true'
|
||
run: bunx playwright test --workers=1 e2e/menu-smoke.pw.test.ts e2e/publish-entry-workflows.pw.test.ts e2e/upload-auth-smoke.pw.test.ts
|
||
|
||
- name: Tag production frontend deployment
|
||
if: needs.validate-deploy-request.outputs.deploy_frontend == 'true'
|
||
env:
|
||
DEPLOY_TARGET: ${{ needs.validate-deploy-request.outputs.target }}
|
||
DEPLOYMENT_URL: ${{ steps.vercel.outputs.deployment_url }}
|
||
run: |
|
||
set -euo pipefail
|
||
deployed_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||
tag_name="deploy/prod/$(date -u +"%Y%m%d-%H%M%SZ")-${GITHUB_SHA::7}"
|
||
version_prefix="prod/v$(date -u +"%Y.%m.%d")."
|
||
run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||
|
||
next_version=1
|
||
while IFS= read -r existing_tag; do
|
||
existing_tag="${existing_tag#refs/tags/}"
|
||
existing_tag="${existing_tag%\^\{\}}"
|
||
suffix="${existing_tag##*.}"
|
||
if [[ "$existing_tag" == "$version_prefix"* && "$suffix" =~ ^[0-9]+$ && "$suffix" -ge "$next_version" ]]; then
|
||
next_version=$((suffix + 1))
|
||
fi
|
||
done < <(git ls-remote --tags origin "refs/tags/${version_prefix}*" | awk '{print $2}' | sort -u)
|
||
version_tag="${version_prefix}${next_version}"
|
||
|
||
git tag -a "$tag_name" "$GITHUB_SHA" \
|
||
-m "Production frontend deploy $tag_name" \
|
||
-m "SHA: $GITHUB_SHA" \
|
||
-m "Version: $version_tag" \
|
||
-m "Deployed at: $deployed_at" \
|
||
-m "Target: $DEPLOY_TARGET" \
|
||
-m "Vercel: ${DEPLOYMENT_URL:-unknown}" \
|
||
-m "Run: $run_url"
|
||
git tag -a "$version_tag" "$GITHUB_SHA" \
|
||
-m "Production frontend deploy $version_tag" \
|
||
-m "SHA: $GITHUB_SHA" \
|
||
-m "Timestamp tag: $tag_name" \
|
||
-m "Deployed at: $deployed_at" \
|
||
-m "Target: $DEPLOY_TARGET" \
|
||
-m "Vercel: ${DEPLOYMENT_URL:-unknown}" \
|
||
-m "Run: $run_url"
|
||
git push origin "refs/tags/$tag_name" "refs/tags/$version_tag"
|