feat: bind repository to Linear/Jira/Sentry issue watchers #29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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-test` to a fork PR | |
| # triggers a deploy against the current head SHA. Subsequent fork pushes | |
| # (synchronize) do NOT re-trigger the label path — see deploy-fork's | |
| # if-gate and strip-safe-to-test below. | |
| 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 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: apps/backend/go.mod | |
| cache-dependency-path: apps/backend/go.sum | |
| - uses: pnpm/action-setup@v5 | |
| with: | |
| version: "9.15.9" | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Install frontend dependencies | |
| run: pnpm -C apps install --frozen-lockfile | |
| - name: Deploy preview environment | |
| working-directory: apps/backend | |
| run: go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install | |
| env: | |
| PR: ${{ github.event.pull_request.number }} | |
| SHA: ${{ github.event.pull_request.head.sha }} | |
| REPO: ${{ github.repository }} | |
| SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ----------------------------------------------------------------------- | |
| # Deploy / update preview (fork PRs, gated by allowlist) | |
| # Uses pull_request_target so secrets are available. | |
| # Two approval paths: | |
| # - PREVIEW_ENV_ALLOWLIST repo variable (trusted login list, all events; | |
| # JSON array of logins) | |
| # - `safe-to-test` label (maintainer opt-in, per-commit). Explicitly | |
| # blocked on `synchronize` so a contributor's push cannot re-use a | |
| # stale label — the maintainer must re-apply it after each push for | |
| # the `labeled` event to trigger a fresh deploy of the new SHA. 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 && | |
| ((github.event.action != 'synchronize' && contains(github.event.pull_request.labels.*.name, 'safe-to-test')) || | |
| (vars.PREVIEW_ENV_ALLOWLIST != '' && contains(fromJSON(vars.PREVIEW_ENV_ALLOWLIST), github.event.pull_request.user.login))) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| # pull_request_target checks out base by default; explicitly use PR head. | |
| # Safe: the preview CLI only reads files to build artifacts and posts comments. | |
| # NOTE: The allowlist gate (safe-to-test, PREVIEW_ENV_ALLOWLIST) | |
| # authorizes running the fork's cmd/preview code with SPRITES_API_TOKEN and GH_TOKEN. | |
| # The safe-to-test label must be re-applied after each new push (see strip-safe-to-test job). | |
| repository: ${{ github.event.pull_request.head.repo.full_name }} | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version-file: apps/backend/go.mod | |
| cache-dependency-path: apps/backend/go.sum | |
| - uses: pnpm/action-setup@v5 | |
| with: | |
| version: "9.15.9" | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Install frontend dependencies | |
| run: pnpm -C apps install --frozen-lockfile | |
| - name: Deploy preview environment | |
| working-directory: apps/backend | |
| run: go run ./cmd/preview deploy --pr "$PR" --sha "$SHA" --repo "$REPO" --skip-web-install | |
| env: | |
| PR: ${{ github.event.pull_request.number }} | |
| SHA: ${{ github.event.pull_request.head.sha }} | |
| REPO: ${{ github.repository }} | |
| SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ----------------------------------------------------------------------- | |
| # Strip safe-to-test label on new pushes to fork PRs. | |
| # Together with the `github.event.action != 'synchronize'` guard on the | |
| # label path in deploy-fork's if-gate, this gives true per-commit opt-in: | |
| # the maintainer must re-apply `safe-to-test` after each push for the | |
| # deploy job to run again. A new push on its own never re-triggers deploy | |
| # on the label path. | |
| # ----------------------------------------------------------------------- | |
| strip-safe-to-test: | |
| if: > | |
| github.event_name == 'pull_request_target' && | |
| github.event.action == 'synchronize' && | |
| github.event.pull_request.head.repo.full_name != github.repository && | |
| contains(github.event.pull_request.labels.*.name, 'safe-to-test') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.pull_request.number, | |
| name: 'safe-to-test', | |
| }); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; | |
| core.info('safe-to-test label already absent; nothing to remove.'); | |
| } | |
| # ----------------------------------------------------------------------- | |
| # 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 | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@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" | |
| env: | |
| PR: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| SPRITES_API_TOKEN: ${{ secrets.SPRITES_API_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |