305 lines
12 KiB
YAML
305 lines
12 KiB
YAML
name: Preview Environment
|
|
|
|
# Dual triggers — mirrors the claude-code-review.yml pattern exactly:
|
|
# pull_request: same-repo PRs (secrets available, safe to run on PR head)
|
|
# pull_request_target: fork PRs (secrets available via base repo, gated by allowlist)
|
|
# Exactly one job runs per PR event based on event_name AND head repo identity.
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened, closed]
|
|
pull_request_target:
|
|
# `labeled` is needed so a maintainer applying `safe-to-review` to a fork PR
|
|
# triggers a deploy against the current head SHA. Subsequent fork pushes
|
|
# (synchronize) re-use the maintainer's approval for the new head.
|
|
types: [opened, synchronize, reopened, closed, labeled]
|
|
|
|
concurrency:
|
|
group: preview-env-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
|
|
# Don't cancel cleanup on close — let it finish destroying the sprite.
|
|
cancel-in-progress: ${{ github.event.action != 'closed' }}
|
|
|
|
jobs:
|
|
# -----------------------------------------------------------------------
|
|
# Deploy / update preview (same-repo PRs)
|
|
# Always runs for same-repo pushes that are not a close event.
|
|
# -----------------------------------------------------------------------
|
|
deploy-same-repo:
|
|
if: >
|
|
github.event.action != 'closed' &&
|
|
github.event_name == 'pull_request' &&
|
|
github.event.pull_request.head.repo.full_name == github.repository
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
preview_url: ${{ steps.deploy.outputs.preview_url }}
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
|
with:
|
|
go-version-file: apps/backend/go.mod
|
|
cache-dependency-path: apps/backend/go.sum
|
|
|
|
- uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
|
with:
|
|
version: "9.15.9"
|
|
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Install frontend dependencies
|
|
run: pnpm -C apps install --frozen-lockfile
|
|
|
|
- name: Deploy preview environment
|
|
id: deploy
|
|
working-directory: apps/backend
|
|
run: |
|
|
set -euo pipefail
|
|
deploy_output="$(go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install --skip-description)"
|
|
printf '%s\n' "$deploy_output"
|
|
preview_url="$(printf '%s\n' "$deploy_output" | sed -n 's/^preview URL: //p' | tail -n 1)"
|
|
test -n "$preview_url"
|
|
printf 'preview_url=%s\n' "$preview_url" >> "$GITHUB_OUTPUT"
|
|
env:
|
|
PR: ${{ github.event.pull_request.number }}
|
|
SHA: ${{ github.event.pull_request.head.sha }}
|
|
REPO: ${{ github.repository }}
|
|
SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Deploy / update preview (fork PRs, gated by allowlist)
|
|
# Uses pull_request_target so secrets are available.
|
|
# Three approval paths:
|
|
# - PREVIEW_ENV_ALLOWLIST repo variable (trusted login list, all events;
|
|
# JSON array of logins)
|
|
# - CLAUDE_REVIEW_ALLOWLIST repo variable (trusted login list, all events;
|
|
# the Claude workflow also adds the approval labels on open)
|
|
# - `safe-to-review` label (maintainer opt-in, durable until removed). It
|
|
# authorizes the current and subsequent fork heads. This matters more
|
|
# here than for claude-code-review.yml because deploy actually runs the
|
|
# fork's `cmd/preview` code with SPRITES_API_TOKEN.
|
|
# -----------------------------------------------------------------------
|
|
# Branch order matters: keep cheap/safe checks before `fromJSON(vars.*)`,
|
|
# which throws (and skips the whole job) if the repo variable is
|
|
# non-empty but unparseable JSON.
|
|
deploy-fork:
|
|
if: >
|
|
github.event.action != 'closed' &&
|
|
github.event_name == 'pull_request_target' &&
|
|
github.event.pull_request.head.repo.full_name != github.repository &&
|
|
((contains(github.event.pull_request.labels.*.name, 'safe-to-review')) ||
|
|
(vars.PREVIEW_ENV_ALLOWLIST != '' && contains(fromJSON(vars.PREVIEW_ENV_ALLOWLIST), github.event.pull_request.user.login)) ||
|
|
(vars.CLAUDE_REVIEW_ALLOWLIST != '' && contains(fromJSON(vars.CLAUDE_REVIEW_ALLOWLIST), github.event.pull_request.user.login)))
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
preview_url: ${{ steps.deploy.outputs.preview_url }}
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
# pull_request_target checks out base by default; explicitly use PR head.
|
|
# Safe: the preview CLI only reads files to build artifacts.
|
|
# NOTE: The allowlist gate (safe-to-review, PREVIEW_ENV_ALLOWLIST,
|
|
# CLAUDE_REVIEW_ALLOWLIST)
|
|
# authorizes running the fork's cmd/preview code with SPRITES_API_TOKEN.
|
|
# The safe-to-review label remains an explicit maintainer approval for
|
|
# follow-up pushes until the maintainer removes it.
|
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
|
with:
|
|
go-version-file: apps/backend/go.mod
|
|
cache-dependency-path: apps/backend/go.sum
|
|
|
|
- uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
|
|
with:
|
|
version: "9.15.9"
|
|
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: Install frontend dependencies
|
|
run: pnpm -C apps install --frozen-lockfile
|
|
|
|
- name: Deploy preview environment
|
|
id: deploy
|
|
working-directory: apps/backend
|
|
run: |
|
|
set -euo pipefail
|
|
deploy_output="$(go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install --skip-description)"
|
|
printf '%s\n' "$deploy_output"
|
|
preview_url="$(printf '%s\n' "$deploy_output" | sed -n 's/^preview URL: //p' | tail -n 1)"
|
|
test -n "$preview_url"
|
|
printf 'preview_url=%s\n' "$preview_url" >> "$GITHUB_OUTPUT"
|
|
env:
|
|
PR: ${{ github.event.pull_request.number }}
|
|
SHA: ${{ github.event.pull_request.head.sha }}
|
|
REPO: ${{ github.repository }}
|
|
SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Update the PR description after deployment.
|
|
# This short trusted job owns the shared description-write lock. Keeping it
|
|
# separate prevents dependency installation and sprite health checks from
|
|
# starving walkthrough or reconciliation writers.
|
|
# -----------------------------------------------------------------------
|
|
update-description-same-repo:
|
|
if: needs.deploy-same-repo.result == 'success'
|
|
needs: deploy-same-repo
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: pr-description-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: false
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: ${{ github.workflow_sha }}
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
|
with:
|
|
go-version-file: apps/backend/go.mod
|
|
cache-dependency-path: apps/backend/go.sum
|
|
|
|
- name: Update preview description
|
|
working-directory: apps/backend
|
|
run: |
|
|
set -euo pipefail
|
|
test -n "$PREVIEW_URL"
|
|
go run ./cmd/preview update-description \
|
|
--pr "$PR" \
|
|
--sha "$SHA" \
|
|
--repo "$REPO" \
|
|
--url "$PREVIEW_URL"
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR: ${{ github.event.pull_request.number }}
|
|
SHA: ${{ github.event.pull_request.head.sha }}
|
|
REPO: ${{ github.repository }}
|
|
PREVIEW_URL: ${{ needs.deploy-same-repo.outputs.preview_url }}
|
|
|
|
update-description-fork:
|
|
if: needs.deploy-fork.result == 'success'
|
|
needs: deploy-fork
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: pr-description-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: false
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: ${{ github.workflow_sha }}
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
|
with:
|
|
go-version-file: apps/backend/go.mod
|
|
cache-dependency-path: apps/backend/go.sum
|
|
|
|
- name: Update preview description
|
|
working-directory: apps/backend
|
|
run: |
|
|
set -euo pipefail
|
|
test -n "$PREVIEW_URL"
|
|
go run ./cmd/preview update-description \
|
|
--pr "$PR" \
|
|
--sha "$SHA" \
|
|
--repo "$REPO" \
|
|
--url "$PREVIEW_URL"
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR: ${{ github.event.pull_request.number }}
|
|
SHA: ${{ github.event.pull_request.head.sha }}
|
|
REPO: ${{ github.repository }}
|
|
PREVIEW_URL: ${{ needs.deploy-fork.outputs.preview_url }}
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Cleanup (all closed PRs — no allowlist check)
|
|
# The sprite must be destroyed regardless of how the PR was closed or
|
|
# whether a preview was ever successfully deployed. Handles gracefully
|
|
# if the sprite does not exist (was never deployed or already destroyed).
|
|
# Runs only on pull_request_target to avoid duplicate runs — both events
|
|
# fire `closed`, but only pull_request_target has secrets for all PRs.
|
|
# -----------------------------------------------------------------------
|
|
cleanup-preview:
|
|
if: >
|
|
github.event.action == 'closed' &&
|
|
github.event_name == 'pull_request_target'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
|
with:
|
|
go-version-file: apps/backend/go.mod
|
|
cache-dependency-path: apps/backend/go.sum
|
|
|
|
- name: Destroy preview environment
|
|
working-directory: apps/backend
|
|
run: go run ./cmd/preview cleanup --pr "$PR" --repo "$REPO" --skip-description
|
|
env:
|
|
PR: ${{ github.event.pull_request.number }}
|
|
REPO: ${{ github.repository }}
|
|
SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }}
|
|
|
|
cleanup-preview-description:
|
|
if: >
|
|
always() &&
|
|
github.event.action == 'closed' &&
|
|
github.event_name == 'pull_request_target' &&
|
|
needs.cleanup-preview.result != 'cancelled'
|
|
needs: cleanup-preview
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: pr-description-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: false
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
with:
|
|
ref: ${{ github.workflow_sha }}
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
|
|
with:
|
|
go-version-file: apps/backend/go.mod
|
|
cache-dependency-path: apps/backend/go.sum
|
|
|
|
- name: Remove preview description
|
|
working-directory: apps/backend
|
|
run: go run ./cmd/preview remove-description --pr "$PR" --repo "$REPO"
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
PR: ${{ github.event.pull_request.number }}
|
|
REPO: ${{ github.repository }}
|