Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions .github/workflows/docs-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,28 @@ jobs:
- name: 🔨 Build docs
run: pnpm docs:build

# Fork PRs receive no repository secrets, so CLOUDFLARE_API_TOKEN is empty
# and the deploy is a guaranteed failure. Skip it for them — the docs
# BUILD above still runs, which is the part that can actually regress.
# The `push` arm is required: pushes to main have no pull_request context.
- name: 📥 Install Wrangler
run: npm install -g wrangler
if: github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository
# Pinned to the major: this step hands CLOUDFLARE_API_TOKEN to whatever
# it installs, so resolving "latest" at runtime would let a compromised
# release reach the credential. Major-pinned rather than exact so patch
# and minor fixes still land — an exact pin goes stale and breaks the
# deploy when Cloudflare moves their API forward.
# Installed globally rather than via pnpm on purpose (acb25d0d1).
run: npm install -g wrangler@4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

- name: 🚀 Deploy to Cloudflare Pages
run: wrangler pages deploy apps/docs/.vitepress/dist --project-name=shelf-docs --branch=${{ github.event_name == 'push' && 'main' || github.head_ref }}
if: github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# github.head_ref is attacker-controlled PR input (a branch name), so
# it goes through env instead of being interpolated into the script.
DEPLOY_BRANCH: ${{ github.event_name == 'push' && 'main' || github.head_ref }}
run: wrangler pages deploy apps/docs/.vitepress/dist --project-name=shelf-docs --branch="$DEPLOY_BRANCH"
48 changes: 32 additions & 16 deletions .github/workflows/react-doctor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name: 🩺 React Doctor
# 2. Fails the check if NEW errors are introduced — warnings stay advisory.

on:
pull_request_target:
pull_request:
paths:
- "apps/webapp/**/*.ts"
- "apps/webapp/**/*.tsx"
Expand All @@ -20,17 +20,12 @@ on:
- ".github/workflows/react-doctor.yml"
- ".github/scripts/react-doctor-pr-comment.mjs"

jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || '' }}
runs-on: ubuntu-latest
steps:
- run: echo ✓
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
doctor:
needs: authorize
name: 🩺 React Doctor (${{ matrix.app.name }})
runs-on: ubuntu-latest
permissions:
Expand All @@ -47,17 +42,19 @@ jobs:
- name: companion
dir: apps/companion
steps:
- name: 🛑 Cancel previous runs
uses: styfle/cancel-workflow-action@0.11.0

- name: ⬇️ Checkout PR head
- name: ⬇️ Checkout PR merge ref
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
# No `ref:` — the default checkout resolves to the PR merge ref and is
# exempt from actions/checkout's fork-PR guard. fetch-depth: 0 is
# still required so `origin/<base>` exists for the --diff below.
fetch-depth: 0
# Nothing after this step needs git auth, so the token never lands in
# .git/config where fork-controlled code could read it back out.
persist-credentials: false

- name: 🪢 Re-attach HEAD to a named branch
# actions/checkout leaves us on a DETACHED HEAD when given a sha.
# actions/checkout leaves us on a DETACHED HEAD at the PR merge ref.
# react-doctor's --diff path calls `git rev-parse --abbrev-ref HEAD`
# to find the current branch; in detached state that returns the
# literal "HEAD", which the CLI interprets as "no branch" and silently
Expand Down Expand Up @@ -157,14 +154,33 @@ jobs:
cat "$RUNNER_TEMP/comment.md"

- name: 💬 Post or update PR comment
# Fork and Dependabot PRs run with a read-only GITHUB_TOKEN, so the
# comment API 403s for them. Those PRs surface findings through the
# inline annotations (emitted as log commands, which need no token) and
# the job summary written below.
#
# Predicate on pull_request.user.login, NOT github.actor: the read-only
# token is keyed to who AUTHORED the PR, but github.actor becomes the
# re-runner's login on a manual re-run — which would wrongly re-enable
# this step on a Dependabot PR re-run.
if: always() && steps.changes.outputs.changed == 'true'
&& github.event.pull_request.head.repo.full_name == github.repository
&& github.event.pull_request.user.login != 'dependabot[bot]'
# Backstop for any read-only-token case not enumerated above.
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@v2
with:
# Per-app header so the two matrix jobs maintain separate sticky
# comments instead of overwriting each other.
header: react-doctor-${{ matrix.app.name }}
path: ${{ runner.temp }}/comment.md

- name: 📋 Write findings to job summary
# Runs for EVERY PR including forks and Dependabot — this is the only
# aggregated report those PRs get, since the sticky comment is skipped.
if: always() && steps.changes.outputs.changed == 'true'
run: cat "$RUNNER_TEMP/comment.md" >> "$GITHUB_STEP_SUMMARY"

- name: ❌ Fail if errors introduced
if: steps.changes.outputs.changed == 'true' && steps.doctor.outputs.exit_code != '0'
run: |
Expand Down
81 changes: 46 additions & 35 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
name: 🧪 Test PR from fork
name: 🧪 Test

# SECURITY INVARIANT — read this before adding a job to this workflow.
#
# This workflow runs on `pull_request`, so fork PRs execute here. GitHub gives
# those runs a read-only GITHUB_TOKEN and NO repository secrets — that is
# precisely what makes it safe to run untrusted contributor code automatically.
#
# Therefore: any job that consumes a secret MUST NOT live in this workflow.
# Secret-bearing jobs (e.g. the Playwright/E2E suite below) belong in a separate
# workflow gated behind the `external` environment, and must never check out
# fork PR code in that trusted context.
#
# Do NOT "fix" a fork PR checkout failure by setting `allow-unsafe-pr-checkout:
# true` on a checkout step. That re-enables the "pwn request" attack class that
# actions/checkout began blocking in July 2026.

on:
pull_request_target:
pull_request:
# Called by deploy.yml on pushes to main/dev. The secrets below are declared
# for the E2E job that currently lives commented-out at the bottom of this
# file; deploy.yml passes all six, so this block must not be removed.
workflow_call:
secrets:
SESSION_SECRET:
Expand All @@ -17,28 +35,32 @@ on:
DATABASE_URL:
required: true

jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || '' }}
# Least privilege, pinned explicitly. The repo default is already `read`, but
# pinning it means a future org-level default change cannot silently escalate
# these jobs. `contents: read` is what actions/checkout needs to fetch.
permissions:
contents: read

runs-on: ubuntu-latest
steps:
- run: echo ✓
concurrency:
# PR number for PR runs; run_id on the workflow_call path so a push to main
# gets a unique group and can never be cancelled by a PR run. Note that in a
# reusable workflow the `github` context belongs to the CALLER.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

jobs:
lint:
needs: authorize
name: ⬣ ESLint
runs-on: ubuntu-latest
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0

- name: ⬇️ Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
# No `ref:` — the default checkout resolves to the PR merge ref and is
# exempt from actions/checkout's fork-PR guard. No credentials
# persisted: nothing after this step needs git auth, so the token
# never lands in .git/config where fork code could read it.
persist-credentials: false

- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
Expand All @@ -55,17 +77,13 @@ jobs:
run: pnpm run lint

typecheck:
needs: authorize
name: ʦ TypeScript
runs-on: ubuntu-latest
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0

- name: ⬇️ Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
persist-credentials: false

- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
Expand All @@ -82,17 +100,13 @@ jobs:
run: pnpm run typecheck

vitest:
needs: authorize
name: ⚡ Vitest
runs-on: ubuntu-latest
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.11.0

- name: ⬇️ Checkout repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
persist-credentials: false

- name: 📦 Setup pnpm
uses: pnpm/action-setup@v4
Expand All @@ -111,19 +125,16 @@ jobs:
- name: ⚡ Run vitest
run: pnpm --filter @shelf/webapp test -- --run

# NOTE: this job consumes secrets — see the SECURITY INVARIANT at the top of
# this file. Do NOT re-enable it here. It belongs in its own workflow gated
# behind the `external` environment, and must not check out fork PR code.
# playwright:
# needs: authorize
# name: 🎭 Playwright
# timeout-minutes: 60
# runs-on: ubuntu-latest
# steps:
# - name: 🛑 Cancel Previous Runs
# uses: styfle/cancel-workflow-action@0.11.0

# - name: ⬇️ Checkout repo
# uses: actions/checkout@v3
# with:
# ref: ${{ github.event.pull_request.head.sha || github.ref }}
# uses: actions/checkout@v4

# - name: 🔑 Make envfile
# uses: SpicyPizza/create-envfile@v2.0
Expand All @@ -137,9 +148,9 @@ jobs:
# file_name: .env

# - name: ⎔ Setup node
# uses: actions/setup-node@v3
# uses: actions/setup-node@v4
# with:
# node-version: 18
# node-version: 22

# - name: 📥 Download deps
# uses: bahmutov/npm-install@v1
Expand All @@ -153,7 +164,7 @@ jobs:
# - name: Run Playwright tests
# run: npx playwright test

# - uses: actions/upload-artifact@v3
# - uses: actions/upload-artifact@v4
# if: always()
# with:
# name: playwright-report
Expand Down
Loading