From 2686633a218d3fb33f1cc4aff4f63717cf451f2a Mon Sep 17 00:00:00 2001 From: Ezekiel Date: Sun, 30 Aug 2026 16:46:17 -0700 Subject: [PATCH 1/2] Add CapRover PR preview deployment Self-contained multi-stage Dockerfile (builds the static export inside the image itself, rather than expecting a host-built out/ dir) so the same Dockerfile works for both caprover deploy/tarball testing and CI. Includes scripts/deploy-tar.sh and scripts/scaffold.sh for fast local iteration against a CapRover server with no CI/registry round-trip, plus GitHub Actions workflows for per-PR preview deploys (PR comment with the preview URL) and cron-based cleanup of expired previews. Verified end-to-end against a live CapRover instance: caprover deploy built and served the site correctly (home, static routes, dynamic event pages, 404s, static assets). Supersedes #104 and #106. --- .dockerignore | 5 + .env.example | 7 ++ .github/workflows/cleanup-previews.yml | 66 +++++++++++ .github/workflows/preview.yml | 142 ++++++++++++++++++++++++ Dockerfile | 25 +++++ captain-definition | 4 + nginx.conf | 18 +++ scripts/deploy-tar.sh | 106 ++++++++++++++++++ scripts/scaffold.sh | 147 +++++++++++++++++++++++++ 9 files changed, 520 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/cleanup-previews.yml create mode 100644 .github/workflows/preview.yml create mode 100644 Dockerfile create mode 100644 captain-definition create mode 100644 nginx.conf create mode 100755 scripts/deploy-tar.sh create mode 100755 scripts/scaffold.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e5020e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.next +out +.git +.env* diff --git a/.env.example b/.env.example index 4b4cd1e..2f5fed7 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,9 @@ LUMA_API_KEY="secret-..." +# Used by scripts/scaffold.sh and scripts/deploy-tar.sh (local CapRover testing). +# Requires the caprover CLI to be installed and logged in: caprover login +CAPROVER_URL="https://captain.your-caprover-domain.com" +CAPROVER_APP="devx-preview-test" +CAPROVER_APP_DOMAIN="your-caprover-root-domain.com" +CAPROVER_PASSWORD="secret-..." + diff --git a/.github/workflows/cleanup-previews.yml b/.github/workflows/cleanup-previews.yml new file mode 100644 index 0000000..62c315d --- /dev/null +++ b/.github/workflows/cleanup-previews.yml @@ -0,0 +1,66 @@ +name: Cleanup Expired Previews + +on: + schedule: + - cron: '0 */6 * * *' # every 6 hours + workflow_dispatch: + +permissions: + pull-requests: write + +jobs: + cleanup: + runs-on: ubuntu-latest + env: + CAPROVER_URL: ${{ secrets.CAPROVER_URL }} + CAPROVER_PASSWORD: ${{ secrets.CAPROVER_PASSWORD }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + EXPIRY_HOURS: 6 + steps: + - name: Expire old PR previews + run: | + ADMIN_TOKEN=$(curl -sf -X POST "$CAPROVER_URL/api/v2/login" \ + -H "Content-Type: application/json" \ + -d "$(jq -nc --arg pass "$CAPROVER_PASSWORD" '{password:$pass}')" \ + | jq -r '.data.token') + + NOW_TS=$(date +%s) + REDEPLOY_URL="https://github.com/$REPO/actions/workflows/preview.yml" + + gh pr list --repo "$REPO" --state open --json number --jq '.[].number' | while read PR_NUM; do + APP_NAME="pr-$PR_NUM" + + # Find the active (non-expired) preview comment + COMMENT=$(gh api "repos/$REPO/issues/$PR_NUM/comments" \ + --jq '[.[] | select(.body | contains("")) | select(.body | contains("expired") | not)] | first') + + [ "$COMMENT" = "null" ] || [ -z "$COMMENT" ] && continue + + COMMENT_ID=$(echo "$COMMENT" | jq -r '.id') + UPDATED_AT=$(echo "$COMMENT" | jq -r '.updated_at') + UPDATED_TS=$(date -d "$UPDATED_AT" +%s) + AGE_HOURS=$(( (NOW_TS - UPDATED_TS) / 3600 )) + + [ "$AGE_HOURS" -lt "$EXPIRY_HOURS" ] && continue + + echo "Expiring preview for PR #$PR_NUM (age: ${AGE_HOURS}h)" + + # Delete CapRover app + curl -s -X POST "$CAPROVER_URL/api/v2/user/apps/appDefinitions/delete" \ + -H "Content-Type: application/json" \ + -H "x-captain-auth: $ADMIN_TOKEN" \ + -d "{\"appName\": \"$APP_NAME\"}" || true + + # Update comment to show expired state with re-deploy link + gh api "repos/$REPO/issues/comments/$COMMENT_ID" \ + -X PATCH \ + --field body=" +## Preview deployment _(expired)_ + +Removed after ${EXPIRY_HOURS}h of inactivity. + +[Re-deploy preview]($REDEPLOY_URL) — click **Run workflow** and enter PR number \`$PR_NUM\`. + +_Expired: $(date -u '+%a, %d %b %Y %H:%M:%S UTC')_" + done diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..389a7f5 --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,142 @@ +name: Preview + +on: + pull_request: + branches: ["main"] + types: [opened, reopened, synchronize, closed] + workflow_dispatch: + inputs: + pr_number: + description: PR number to (re-)deploy + required: true + +concurrency: + group: "preview-${{ github.event.number || inputs.pr_number }}" + cancel-in-progress: true + +permissions: + pull-requests: write + packages: write + +jobs: + deploy-preview: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + env: + PR_NUMBER: ${{ github.event.number || inputs.pr_number }} + APP_NAME: pr-${{ github.event.number || inputs.pr_number }} + CAPROVER_URL: ${{ secrets.CAPROVER_URL }} + CAPROVER_PASSWORD: ${{ secrets.CAPROVER_PASSWORD }} + CAPROVER_APP_DOMAIN: ${{ secrets.CAPROVER_APP_DOMAIN }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set image URL + run: | + REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + SHA=$(echo "${{ github.sha }}" | cut -c1-7) + echo "IMAGE=ghcr.io/${REPO}-preview:pr-${PR_NUMBER}-${SHA}" >> $GITHUB_ENV + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Log in to GHCR + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ env.IMAGE }} + build-args: | + NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Create app and deploy image via CapRover API + run: | + ADMIN_TOKEN=$(curl -sf -X POST "$CAPROVER_URL/api/v2/login" \ + -H "Content-Type: application/json" \ + -d "$(jq -nc --arg pass "$CAPROVER_PASSWORD" '{password:$pass}')" \ + | jq -r '.data.token') + + # Create app if it doesn't exist + curl -s -X POST "$CAPROVER_URL/api/v2/user/apps/appDefinitions/register" \ + -H "Content-Type: application/json" \ + -H "x-captain-auth: $ADMIN_TOKEN" \ + -d "{\"appName\": \"$APP_NAME\", \"hasPersistentData\": false}" || true + + curl -sf -X POST "$CAPROVER_URL/api/v2/user/apps/appDefinitions/enablebasedomainssl" \ + -H "Content-Type: application/json" \ + -H "x-captain-auth: $ADMIN_TOKEN" \ + -d "{\"appName\": \"$APP_NAME\"}" || true + + # Point app at the pre-built image and trigger deploy + curl -sf -X POST "$CAPROVER_URL/api/v2/user/apps/appDefinitions/update" \ + -H "Content-Type: application/json" \ + -H "x-captain-auth: $ADMIN_TOKEN" \ + -d "{\"appName\": \"$APP_NAME\", \"imageName\": \"$IMAGE\", \"instanceCount\": 1}" + + - name: Comment preview URL on PR + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + CAPROVER_APP_DOMAIN: ${{ secrets.CAPROVER_APP_DOMAIN }} + with: + script: | + const prNumber = process.env.PR_NUMBER; + const appName = `pr-${prNumber}`; + const url = `https://${appName}.${process.env.CAPROVER_APP_DOMAIN}`; + const marker = ''; + const body = `${marker}\n## Preview deployment\n\n${url}\n\n_Updated: ${new Date().toUTCString()} — expires after 6h of inactivity._`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: Number(prNumber), + }); + + const existing = comments.find(c => c.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: Number(prNumber), + body, + }); + } + + cleanup-preview: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + env: + APP_NAME: pr-${{ github.event.number }} + CAPROVER_URL: ${{ secrets.CAPROVER_URL }} + CAPROVER_PASSWORD: ${{ secrets.CAPROVER_PASSWORD }} + steps: + - name: Delete CapRover app + run: | + TOKEN=$(curl -sf -X POST "$CAPROVER_URL/api/v2/login" \ + -H "Content-Type: application/json" \ + -d "$(jq -nc --arg pass "$CAPROVER_PASSWORD" '{password:$pass}')" \ + | jq -r '.data.token') + + curl -s -X POST "$CAPROVER_URL/api/v2/user/apps/appDefinitions/delete" \ + -H "Content-Type: application/json" \ + -H "x-captain-auth: $TOKEN" \ + -d "{\"appName\": \"$APP_NAME\"}" || true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1c9d114 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Stage 1: build the static export from source (no host-built out/ required — +# this makes the image buildable directly by CapRover, via `caprover deploy` +# or a tarball upload, with no CI/registry round-trip needed). +FROM oven/bun:1 AS build +WORKDIR /app +COPY package.json bun.lock ./ + +# --ignore-scripts: sqlite3/better-sqlite3 are unused dead deps that otherwise +# try to compile a native module at install time and need a full toolchain. +RUN bun install --frozen-lockfile --ignore-scripts +COPY . . + +# Public Supabase anon key/URL — safe to bake in, matches .github/workflows/check.yml. +ARG NEXT_PUBLIC_SUPABASE_URL="https://psbmuerdpmkajkkldqtz.supabase.co" +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBzYm11ZXJkcG1rYWpra2xkcXR6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjMzNDE1NjcsImV4cCI6MjA3ODkxNzU2N30.JKaPS9tajIe6YJklEAdlih8a5xA-XgD3hStwKOEiihI" +ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL +ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY + +RUN bun run build + +# Stage 2: serve the static export +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/out /usr/share/nginx/html +EXPOSE 80 diff --git a/captain-definition b/captain-definition new file mode 100644 index 0000000..0e14f82 --- /dev/null +++ b/captain-definition @@ -0,0 +1,4 @@ +{ + "schemaVersion": 2, + "dockerfilePath": "./Dockerfile" +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..58a9c10 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,18 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Next.js static export writes routes as flat `.html` files + # (e.g. /events -> events.html), not `/index.html`, so try the + # .html sibling before falling back to a directory index. + location / { + try_files $uri $uri.html $uri/index.html $uri/ =404; + } + + error_page 404 /404.html; + location = /404.html { + internal; + } +} diff --git a/scripts/deploy-tar.sh b/scripts/deploy-tar.sh new file mode 100755 index 0000000..a875877 --- /dev/null +++ b/scripts/deploy-tar.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Deploy to CapRover via tarball upload. +# +# Packages the project source + captain-definition into a tar and deploys +# using `caprover deploy -t`. CapRover builds the Docker image on the server — +# no container registry or GitHub Actions required. Useful for fast local +# iteration: no CI round-trip, just build-on-server and check the URL. +# +# Usage: +# ./scripts/deploy-tar.sh # deploy using .env.local +# ./scripts/deploy-tar.sh --dry-run # print what would happen without doing it +# ./scripts/deploy-tar.sh --env=.env.staging +# +# Requires: +# - caprover CLI installed and logged in (run `caprover login` first) +# - CAPROVER_URL and CAPROVER_APP set in .env.local + +set -euo pipefail + +ENV_FILE=".env.local" +DRY_RUN=false +TAR_FILE="./deploy.tar" + +for arg in "$@"; do + case $arg in + --dry-run) DRY_RUN=true ;; + --env=*) ENV_FILE="${arg#--env=}" ;; + esac +done + +# --- Parse env file --- +declare -A ENV +while IFS= read -r line; do + [[ "$line" =~ ^[[:space:]]*# ]] && continue + [[ -z "${line// }" ]] && continue + if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + val="${BASH_REMATCH[2]}" + val="${val#\"}" ; val="${val%\"}" + val="${val#\'}" ; val="${val%\'}" + ENV["$key"]="$val" + fi +done < "$ENV_FILE" + +get() { echo "${ENV[$1]:-}"; } + +CAPROVER_URL="$(get CAPROVER_URL)" +APP_NAME="$(get CAPROVER_APP)" + +for var in CAPROVER_URL CAPROVER_APP; do + if [[ -z "${ENV[$var]:-}" ]]; then + echo "Error: $var not set in $ENV_FILE" >&2; exit 1 + fi +done + +# --- Find the caprover machine name matching CAPROVER_URL --- +echo "==> Finding caprover CLI session for $CAPROVER_URL..." +CAPROVER_NAME=$(caprover ls 2>/dev/null \ + | awk -v url="$CAPROVER_URL" '$0 ~ url { print $2 }') + +if [[ -z "$CAPROVER_NAME" ]]; then + echo "Error: no caprover CLI session found for $CAPROVER_URL" >&2 + echo " Run: caprover login" >&2 + exit 1 +fi +echo " using machine '$CAPROVER_NAME'" + +if $DRY_RUN; then + echo "" + echo "[dry-run] Would create $TAR_FILE from project source" + echo "[dry-run] caprover deploy -t $TAR_FILE -n $CAPROVER_NAME -a $APP_NAME" + exit 0 +fi + +# --- Build tar --- +# Excludes match .dockerignore, plus the tar itself. +echo "" +echo "==> Creating $TAR_FILE..." + +tar -cf "$TAR_FILE" \ + --exclude='./node_modules' \ + --exclude='./.next' \ + --exclude='./out' \ + --exclude='./.env*' \ + --exclude='./.git' \ + --exclude="$TAR_FILE" \ + . + +echo " $(du -sh "$TAR_FILE" | cut -f1) — $(tar -tf "$TAR_FILE" | wc -l | tr -d ' ') files" + +# --- Deploy --- +echo "" +echo "==> Deploying '$APP_NAME' to '$CAPROVER_NAME'..." +echo " CapRover will build the Docker image on the server." +echo " Build logs will stream below — this takes a few minutes." +echo "" + +caprover deploy \ + --tarFile "$TAR_FILE" \ + --caproverName "$CAPROVER_NAME" \ + --caproverApp "$APP_NAME" + +# --- Cleanup --- +rm -f "$TAR_FILE" +echo "" +echo "Deploy complete." diff --git a/scripts/scaffold.sh b/scripts/scaffold.sh new file mode 100755 index 0000000..82a158e --- /dev/null +++ b/scripts/scaffold.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# One-time setup of the app on CapRover. +# Safe to re-run — create calls no-op if the resource already exists. +# +# Usage: +# ./scripts/scaffold.sh # full setup +# ./scripts/scaffold.sh --dry-run # print API calls without executing +# +# Requires: caprover CLI (already logged in), jq, values set in .env.local +# Required in .env.local: CAPROVER_URL, CAPROVER_APP +# Optional in .env.local: CAPROVER_APP_DOMAIN + +set -euo pipefail + +ENV_FILE=".env.local" +DRY_RUN=false + +for arg in "$@"; do + case $arg in + --dry-run) DRY_RUN=true ;; + --env=*) ENV_FILE="${arg#--env=}" ;; + esac +done + +# --- Parse env file --- +declare -A ENV +while IFS= read -r line; do + [[ "$line" =~ ^[[:space:]]*# ]] && continue + [[ -z "${line// }" ]] && continue + if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + val="${BASH_REMATCH[2]}" + val="${val#\"}" ; val="${val%\"}" + val="${val#\'}" ; val="${val%\'}" + ENV["$key"]="$val" + fi +done < "$ENV_FILE" + +get() { echo "${ENV[$1]:-}"; } + +CAPROVER_URL="$(get CAPROVER_URL)" +APP_NAME="$(get CAPROVER_APP)" +APP_DOMAIN="$(get CAPROVER_APP_DOMAIN)" + +for var in CAPROVER_URL CAPROVER_APP; do + if [[ -z "${ENV[$var]:-}" ]]; then + echo "Error: $var not set in $ENV_FILE" >&2; exit 1 + fi +done + +CUSTOM_DOMAIN="${APP_NAME}.${APP_DOMAIN}" + +# ============================================================ +# Authenticate directly against the CapRover REST API. +# +# NOTE: `caprover api` (the CLI's generic-API subcommand) throws +# `ERR_USE_AFTER_CLOSE` from inquirer/readline whenever stdin isn't a real +# TTY (i.e. always, from a script). Calling the REST API with curl directly +# is what CapRover's own CI examples do and is what preview.yml/deploy.yml +# use too — sidesteps the bug entirely. +# ============================================================ +CAPROVER_PASSWORD="$(get CAPROVER_PASSWORD)" +if [[ -z "$CAPROVER_PASSWORD" ]]; then + echo "Error: CAPROVER_PASSWORD not set in $ENV_FILE" >&2; exit 1 +fi + +echo "==> Authenticating with $CAPROVER_URL..." +if ! $DRY_RUN; then + LOGIN_BODY=$(jq -n --arg pass "$CAPROVER_PASSWORD" '{password:$pass}') + ADMIN_TOKEN=$(curl -sf -X POST "$CAPROVER_URL/api/v2/login" \ + -H "Content-Type: application/json" \ + -d "$LOGIN_BODY" | jq -r '.data.token') + if [[ -z "$ADMIN_TOKEN" || "$ADMIN_TOKEN" == "null" ]]; then + echo "Error: login failed — check CAPROVER_URL/CAPROVER_PASSWORD" >&2; exit 1 + fi +fi + +cap_api() { + local method="$1" path="$2" body="${3:-}" + if $DRY_RUN; then + echo " [dry-run] $method $CAPROVER_URL$path" + [[ -n "$body" ]] && echo " $body" + return + fi + curl -sf -X "$method" "$CAPROVER_URL$path" \ + -H "Content-Type: application/json" \ + -H "x-captain-auth: $ADMIN_TOKEN" \ + ${body:+-d "$body"} +} + +# ============================================================ +# 1. Create the app (no-ops if already exists) +# ============================================================ +echo "" +echo "==> Creating app '$APP_NAME'..." +if $DRY_RUN; then + cap_api POST /api/v2/user/apps/appDefinitions/register \ + "{\"appName\": \"$APP_NAME\", \"hasPersistentData\": false}" +elif cap_api POST /api/v2/user/apps/appDefinitions/register \ + "{\"appName\": \"$APP_NAME\", \"hasPersistentData\": false}" &>/dev/null; then + echo " created" +else + echo " already exists, continuing" +fi + +# ============================================================ +# 2. Configure app: instance count, container port +# ============================================================ +echo "==> Configuring app..." + +APP_CONFIG=$(jq -n \ + --arg app "$APP_NAME" \ + '{ + "appName": $app, + "instanceCount": 1, + "containerHttpPort": 80, + "ports": [], + "notExposeAsWebApp": false + }') + +cap_api POST /api/v2/user/apps/appDefinitions/update "$APP_CONFIG" + +# ============================================================ +# 3. Enable HTTPS on default CapRover subdomain +# ============================================================ +echo "" +echo "==> Enabling HTTPS on default subdomain for '$APP_NAME'..." +cap_api POST /api/v2/user/apps/appDefinitions/enablebasedomainssl \ + "{\"appName\": \"$APP_NAME\"}" || \ + echo " Warning: SSL on default subdomain failed — DNS may not be propagated yet" + +# ============================================================ +echo "" +echo "Scaffolding complete." +echo "" +if [[ -n "$APP_DOMAIN" ]]; then + echo "App URL: https://$CUSTOM_DOMAIN" + echo "(CAPROVER_APP_DOMAIN is only used here to print this URL — it's assumed" + echo " to be the CapRover root domain itself, so \$appName.\$rootDomain is the" + echo " free auto-SSL subdomain enabled above, not a separate custom domain." + echo " To point a genuinely different domain at this app, do that by hand in" + echo " the CapRover dashboard: Apps > $APP_NAME > HTTP Settings.)" +fi +echo "" +echo "Next steps:" +echo " 1. Verify the app in CapRover dashboard: $CAPROVER_URL" +echo " 2. Deploy: ./scripts/deploy-tar.sh" From 31ff743694aa8806be4781e368750a08769eef9d Mon Sep 17 00:00:00 2001 From: Ezekiel Date: Mon, 31 Aug 2026 13:33:21 -0700 Subject: [PATCH 2/2] Gate preview deploys behind a "preview" label Previews now only build/deploy when the PR carries the "preview" label, instead of on every push. Removing the label tears the preview down. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01W2kALmabEHRhY4yGKRKykv --- .github/workflows/preview.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 389a7f5..61a4064 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -3,7 +3,7 @@ name: Preview on: pull_request: branches: ["main"] - types: [opened, reopened, synchronize, closed] + types: [opened, reopened, synchronize, closed, labeled, unlabeled] workflow_dispatch: inputs: pr_number: @@ -19,8 +19,15 @@ permissions: packages: write jobs: + # Only deploys when the PR carries the "preview" label — either the label + # was just added, or it was already present when the PR opened/reopened/ + # got new commits. Keeps previews opt-in instead of building on every PR. deploy-preview: - if: github.event.action != 'closed' + if: | + github.event_name == 'workflow_dispatch' || + (github.event.action == 'labeled' && github.event.label.name == 'preview') || + (contains(fromJSON('["opened","reopened","synchronize"]'), github.event.action) && + contains(github.event.pull_request.labels.*.name, 'preview')) runs-on: ubuntu-latest env: PR_NUMBER: ${{ github.event.number || inputs.pr_number }} @@ -122,7 +129,9 @@ jobs: } cleanup-preview: - if: github.event.action == 'closed' + if: | + github.event.action == 'closed' || + (github.event.action == 'unlabeled' && github.event.label.name == 'preview') runs-on: ubuntu-latest env: APP_NAME: pr-${{ github.event.number }}