ci: deploy fork-PR previews via label-gated workflow_run#965
ci: deploy fork-PR previews via label-gated workflow_run#965dante01yoon wants to merge 1 commit into
Conversation
Fork PRs never receive repo secrets on pull_request events, so the Vercel deploy step failed with a missing --token value. Split the preview into two workflows: - preview-site.yml now only builds the Astro site (no secrets) and uploads the prebuilt .vercel/output plus the PR number as an artifact. - deploy-preview.yml runs on workflow_run in the base-repo context (secrets available), gated by the maintainer-only 'preview' label, and deploys only the prebuilt artifact. The Vercel token never touches fork-authored build code.
📝 WalkthroughWalkthroughThe ChangesTwo-workflow preview pipeline
Sequence DiagramsequenceDiagram
participant PR as Pull Request
participant PreviewSite as preview-site.yml
participant Artifact as vercel-preview-output
participant DeployPreview as deploy-preview.yml
participant GH_CLI as gh CLI
participant Vercel as Vercel CLI
participant Comment as Sticky PR Comment
PR->>PreviewSite: Trigger on opened/sync/reopened/labeled
PreviewSite->>PreviewSite: Build Astro site
PreviewSite->>Artifact: Upload prebuilt output + pr-number metadata
Artifact-->>DeployPreview: workflow_run completion event
DeployPreview->>Artifact: Download artifact, read pr-number
DeployPreview->>GH_CLI: gh pr view --json labels
GH_CLI-->>DeployPreview: approved = true/false
DeployPreview->>Vercel: npx vercel deploy --prebuilt (if approved)
Vercel-->>DeployPreview: Preview URL
DeployPreview->>Comment: Post/update sticky comment with preview URL
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/deploy-preview.yml:
- Around line 35-41: The preview deployment workflow is passing an untrusted
artifact value from artifact/preview-meta/pr-number directly into GITHUB_OUTPUT,
which can later be expanded in shell templates; validate the value in the
artifact-handling step before setting number/found. In the workflow job that
reads the PR number, ensure the value is strictly numeric (for example, using a
pattern check) and only then emit it; otherwise mark the artifact invalid and
stop before the later shell usage in the deployment steps that reference the PR
number.
- Around line 79-80: The deploy-preview workflow currently uses a mutable Vercel
CLI invocation via npx vercel@latest while VERCEL_TOKEN is available, so update
the command in the deploy step to use a pinned, reviewed Vercel CLI version or a
lockfile-backed installation instead. Keep the change localized to the workflow
job that runs the deploy command, and make sure the pinned reference is explicit
so the CLI version cannot change unexpectedly.
- Line 25: The deploy preview workflow currently references mutable action tags,
so pin the actions used in this job to full commit SHAs instead of version tags.
Update the `actions/download-artifact` and
`marocchino/sticky-pull-request-comment` references in the workflow to fixed
commit hashes so the privileged deploy flow always runs against immutable action
versions.
In @.github/workflows/preview-site.yml:
- Line 54: The workflow step using actions/upload-artifact is still referenced
by a floating version tag, so pin it to an immutable commit SHA instead. Update
the upload-artifact usage in the preview-site workflow to the SHA-backed
reference for the same v4 release, keeping the rest of the job unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 39235756-f6cd-45da-8b6c-9999496cb44e
📒 Files selected for processing (2)
.github/workflows/deploy-preview.yml.github/workflows/preview-site.yml
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Download prebuilt output | ||
| uses: actions/download-artifact@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -e yml -e yaml .github/workflows -x rg -n 'uses:\s+[^#]+@(?:v[0-9]+|main|master|latest)\b' {}Repository: Comfy-Org/workflow_templates
Length of output: 540
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== files ==\n'
fd --type f --hidden --glob '.github/workflows/*' . | sed 's#^\./##' | sort
printf '\n== deploy-preview.yml ==\n'
nl -ba .github/workflows/deploy-preview.yml | sed -n '1,220p'
printf '\n== mutable uses refs in workflows ==\n'
python3 - <<'PY'
from pathlib import Path
import re
for path in sorted(Path('.github/workflows').rglob('*.[yY][aA][mM][lL]')) + sorted(Path('.github/workflows').rglob('*.yml')):
text = path.read_text()
for i, line in enumerate(text.splitlines(), 1):
m = re.search(r'^\s*uses:\s*([^#\s]+)@([^\s#]+)', line)
if m:
ref = m.group(2)
if ref in {'main', 'master', 'latest'} or re.fullmatch(r'v\d+(\.\d+)*', ref):
print(f"{path}:{i}:{m.group(1)}@{ref}")
PYRepository: Comfy-Org/workflow_templates
Length of output: 245
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== files ==\n'
fd --type f --hidden --glob '.github/workflows/*' . | sed 's#^\./##' | sort
printf '\n== deploy-preview.yml ==\n'
nl -ba .github/workflows/deploy-preview.yml | sed -n '1,220p'
printf '\n== mutable uses refs in workflows ==\n'
python3 - <<'PY'
from pathlib import Path
import re
for path in sorted(Path('.github/workflows').rglob('*.[yY][aA][mM][lL]')) + sorted(Path('.github/workflows').rglob('*.yml')):
text = path.read_text()
for i, line in enumerate(text.splitlines(), 1):
m = re.search(r'^\s*uses:\s*([^#\s]+)@([^\s#]+)', line)
if m:
ref = m.group(2)
if ref in {'main', 'master', 'latest'} or re.fullmatch(r'v\d+(\.\d+)*', ref):
print(f"{path}:{i}:{m.group(1)}@{ref}")
PYRepository: Comfy-Org/workflow_templates
Length of output: 245
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== deploy-preview.yml ==\n'
sed -n '1,220p' .github/workflows/deploy-preview.yml | cat -n
printf '\n== mutable uses refs in workflows ==\n'
python3 - <<'PY'
from pathlib import Path
import re
workflow_dir = Path('.github/workflows')
for path in sorted([p for p in workflow_dir.rglob('*') if p.suffix.lower() in {'.yml', '.yaml'}]):
for i, line in enumerate(path.read_text().splitlines(), 1):
m = re.search(r'^\s*uses:\s*([^#\s]+)@([^\s#]+)', line)
if m:
ref = m.group(2)
if ref in {'main', 'master', 'latest'} or re.fullmatch(r'v\d+(\.\d+)*', ref):
print(f"{path}:{i}:{m.group(1)}@{ref}")
PYRepository: Comfy-Org/workflow_templates
Length of output: 10443
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== deploy-preview.yml ==\n'
sed -n '1,220p' .github/workflows/deploy-preview.yml | cat -n
printf '\n== mutable uses refs in workflows ==\n'
python3 - <<'PY'
from pathlib import Path
import re
workflow_dir = Path('.github/workflows')
for path in sorted([p for p in workflow_dir.rglob('*') if p.suffix.lower() in {'.yml', '.yaml'}]):
for i, line in enumerate(path.read_text().splitlines(), 1):
m = re.search(r'^\s*uses:\s*([^#\s]+)@([^\s#]+)', line)
if m:
ref = m.group(2)
if ref in {'main', 'master', 'latest'} or re.fullmatch(r'v\d+(\.\d+)*', ref):
print(f"{path}:{i}:{m.group(1)}@{ref}")
PYRepository: Comfy-Org/workflow_templates
Length of output: 10443
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== deploy-preview.yml ==\n'
sed -n '1,220p' .github/workflows/deploy-preview.yml | cat -n
printf '\n== mutable uses refs in workflows ==\n'
python3 - <<'PY'
from pathlib import Path
import re
workflow_dir = Path('.github/workflows')
for path in sorted([p for p in workflow_dir.rglob('*') if p.suffix.lower() in {'.yml', '.yaml'}]):
for i, line in enumerate(path.read_text().splitlines(), 1):
m = re.search(r'^\s*uses:\s*([^#\s]+)@([^\s#]+)', line)
if m:
ref = m.group(2)
if ref in {'main', 'master', 'latest'} or re.fullmatch(r'v\d+(\.\d+)*', ref):
print(f"{path}:{i}:{m.group(1)}@{ref}")
PYRepository: Comfy-Org/workflow_templates
Length of output: 10443
Pin the workflow actions
actions/download-artifact@v4 and marocchino/sticky-pull-request-comment@v2 are mutable tags. Pin both to full commit SHAs before this privileged deploy runs—no tag-drift gremlins.
🧰 Tools
🪛 zizmor (1.26.1)
[error] 25-25: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-preview.yml at line 25, The deploy preview workflow
currently references mutable action tags, so pin the actions used in this job to
full commit SHAs instead of version tags. Update the `actions/download-artifact`
and `marocchino/sticky-pull-request-comment` references in the workflow to fixed
commit hashes so the privileged deploy flow always runs against immutable action
versions.
Source: Linters/SAST tools
| number="$(tr -d '[:space:]' < artifact/preview-meta/pr-number)" | ||
| if [ -z "$number" ]; then | ||
| echo "No PR number in artifact; nothing to deploy." | ||
| echo "found=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "found=true" >> "$GITHUB_OUTPUT" | ||
| echo "number=$number" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Validate the artifact PR number before using it in shell templates.
artifact/preview-meta/pr-number crosses from the untrusted build artifact into a privileged workflow_run. Without numeric validation, a crafted value can be emitted as an output and expanded into the shell at Lines 50 and 55, bypassing the label gate or running unintended commands.
Suggested change
- name: Resolve PR number
id: pr
run: |
number="$(tr -d '[:space:]' < artifact/preview-meta/pr-number)"
- if [ -z "$number" ]; then
- echo "No PR number in artifact; nothing to deploy."
+ if [[ ! "$number" =~ ^[0-9]+$ ]]; then
+ echo "Invalid PR number in artifact; nothing to deploy." >&2
echo "found=false" >> "$GITHUB_OUTPUT"
else
echo "found=true" >> "$GITHUB_OUTPUT"
echo "number=$number" >> "$GITHUB_OUTPUT"
fi
@@
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ steps.pr.outputs.number }}
+ REPOSITORY: ${{ github.repository }}
run: |
- labels="$(gh pr view "${{ steps.pr.outputs.number }}" --repo "${{ github.repository }}" --json labels --jq '.labels[].name')"
+ labels="$(gh pr view "$PR_NUMBER" --repo "$REPOSITORY" --json labels --jq '.labels[].name')"
if printf '%s\n' "$labels" | grep -qx 'preview'; then
echo "approved=true" >> "$GITHUB_OUTPUT"
else
echo "approved=false" >> "$GITHUB_OUTPUT"
- echo "PR #${{ steps.pr.outputs.number }} has no 'preview' label — skipping deploy."
+ echo "PR #$PR_NUMBER has no 'preview' label — skipping deploy."
fiAlso applies to: 50-55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-preview.yml around lines 35 - 41, The preview
deployment workflow is passing an untrusted artifact value from
artifact/preview-meta/pr-number directly into GITHUB_OUTPUT, which can later be
expanded in shell templates; validate the value in the artifact-handling step
before setting number/found. In the workflow job that reads the PR number,
ensure the value is strictly numeric (for example, using a pattern check) and
only then emit it; otherwise mark the artifact invalid and stop before the later
shell usage in the deployment steps that reference the PR number.
Source: Linters/SAST tools
| if npx vercel@latest deploy --cwd "$GITHUB_WORKSPACE" --prebuilt --yes \ | ||
| --token="$VERCEL_TOKEN" >"$stdout_file" 2>"$stderr_file"; then |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -e json -e lock -e yml -e yaml -e toml -e js -e ts | xargs rg -n '"vercel"|vercel@|npx vercel'Repository: Comfy-Org/workflow_templates
Length of output: 480
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deploy-preview workflow context =="
nl -ba .github/workflows/deploy-preview.yml | sed -n '60,95p'
echo
echo "== repo search for Vercel CLI references =="
rg -n -C 2 'npx vercel|vercel@latest|vercel@' . --glob '!site/pnpm-lock.yaml'
echo
echo "== package manifests mentioning vercel =="
rg -n -C 2 '"vercel"|vercel@' package.json site/package.json pnpm-lock.yaml site/pnpm-lock.yaml .github/workflows/*.yml .github/workflows/**/*.ymlRepository: Comfy-Org/workflow_templates
Length of output: 244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deploy-preview workflow context =="
nl -ba .github/workflows/deploy-preview.yml | sed -n '60,95p'
echo
echo "== repo search for Vercel CLI references =="
rg -n -C 2 'npx vercel|vercel@latest|vercel@' . --glob '!site/pnpm-lock.yaml'
echo
echo "== package manifests mentioning vercel =="
rg -n -C 2 '"vercel"|vercel@' package.json site/package.json pnpm-lock.yaml site/pnpm-lock.yaml .github/workflows/*.yml .github/workflows/**/*.ymlRepository: Comfy-Org/workflow_templates
Length of output: 244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== deploy-preview workflow context =="
awk 'NR>=60 && NR<=95 { printf "%4d %s\n", NR, $0 }' .github/workflows/deploy-preview.yml
echo
echo "== repo search for Vercel CLI references =="
rg -n -C 2 'npx vercel|vercel@latest|vercel@' . --glob '!site/pnpm-lock.yaml'
echo
echo "== files that mention vercel =="
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|.*\.yml|.*\.yaml|.*\.json|.*\.js|.*\.ts)$' | xargs rg -n -C 1 '"vercel"|vercel@|npx vercel' --glob '!site/pnpm-lock.yaml'Repository: Comfy-Org/workflow_templates
Length of output: 1783
Pin the Vercel CLI in .github/workflows/deploy-preview.yml:79
npx vercel@latest can pull mutable code while VERCEL_TOKEN is in scope. Replace it with a pinned version or a reviewed lockfile-backed install; there’s no existing CLI pin in the repo to reuse.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/deploy-preview.yml around lines 79 - 80, The
deploy-preview workflow currently uses a mutable Vercel CLI invocation via npx
vercel@latest while VERCEL_TOKEN is available, so update the command in the
deploy step to use a pinned, reviewed Vercel CLI version or a lockfile-backed
installation instead. Keep the change localized to the workflow job that runs
the deploy command, and make sure the pinned reference is explicit so the CLI
version cannot change unexpectedly.
| if: github.event_name == 'pull_request' | ||
| uses: marocchino/sticky-pull-request-comment@v2 | ||
| - name: Upload prebuilt output | ||
| uses: actions/upload-artifact@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -e yml -e yaml .github/workflows -x rg -n 'uses:\s+[^#]+@(?:v[0-9]+|main|master|latest)\b' {}Repository: Comfy-Org/workflow_templates
Length of output: 540
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the workflow around the referenced line and list all action refs.
echo '--- preview-site.yml (relevant section) ---'
sed -n '1,220p' .github/workflows/preview-site.yml | cat -n
echo
echo '--- all uses: entries in .github/workflows ---'
rg -n --glob '.github/workflows/**/*.yml' --glob '.github/workflows/**/*.yaml' '^\s*uses:\s*[^#]+' .github/workflowsRepository: Comfy-Org/workflow_templates
Length of output: 11801
Pin actions/upload-artifact to a commit SHA. actions/upload-artifact@v4 is mutable; use an immutable SHA here to avoid supply-chain drift and keep the workflow from going off-key.
🧰 Tools
🪛 zizmor (1.26.1)
[error] 54-54: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/preview-site.yml at line 54, The workflow step using
actions/upload-artifact is still referenced by a floating version tag, so pin it
to an immutable commit SHA instead. Update the upload-artifact usage in the
preview-site workflow to the SHA-backed reference for the same v4 release,
keeping the rest of the job unchanged.
Source: Linters/SAST tools
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened, labeled] |
There was a problem hiding this comment.
issue: labeled event type is functionally dead when combined with the paths: ['site/**'] filter. GitHub evaluates event types and paths as a conjunction, so adding the preview label to a fork PR that has no site/** changes will not trigger this workflow. The "remove + re-add the label to redeploy" workaround described in the PR description also fails for the same reason -- the only working path for a first deploy is a new push (synchronize) after the label is present.
Since the label gate is correctly enforced at deploy-time in deploy-preview.yml, the labeled type here is not needed. Suggest removing it, or moving label-triggered builds to a separate workflow file without a paths filter.
| group: deploy-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: |
There was a problem hiding this comment.
issue: cancel-in-progress: true on a deploy workflow can leave a broken Vercel preview if a second push arrives while vercel deploy is mid-flight -- the runner is killed after the upload has started but before it completes, and Vercel may surface a stale or errored deployment. deploy-site.yml uses cancel-in-progress: false for exactly this reason. Deploy workflows should queue rather than cancel.
|
|
||
| - name: Check for preview label | ||
| id: gate | ||
| if: steps.pr.outputs.found == 'true' |
There was a problem hiding this comment.
issue: label approval can be consumed by a later, unreviewed commit. Sequence: (1) maintainer adds preview label to commit A; (2) fork author immediately pushes commit B; (3) Preview Site runs on commit B and uploads a new artifact; (4) this workflow fires, checks the label -- still present -- and deploys commit B's content. The label granted for commit A now covers commit B without maintainer re-review.
Two options: (a) add a pull_request: [synchronize] workflow that strips the preview label on every new push, forcing re-approval per commit; or (b) record the approved SHA when labeling and verify github.event.workflow_run.head_sha matches at deploy time.
| id: pr | ||
| run: | | ||
| number="$(tr -d '[:space:]' < artifact/preview-meta/pr-number)" | ||
| if [ -z "$number" ]; then |
There was a problem hiding this comment.
nitpick: (non-blocking) tr -d '[:space:]' strips whitespace but does not validate the result is a non-negative integer before writing to GITHUB_OUTPUT and passing to gh pr view. In practice github.event.pull_request.number is always a positive integer, but a defensive check costs nothing: if [[ ! "$number" =~ ^[0-9]+$ ]]; then echo "Invalid PR number" >&2; exit 1; fi
| permissions: | ||
| contents: read | ||
| actions: read | ||
| pull-requests: write |
There was a problem hiding this comment.
nitpick: (non-blocking) contents: read is listed but this workflow has no actions/checkout step and reads no repo contents. actions/download-artifact with run-id uses actions: read (already present). The existing workflow_run workflows in this repo (spellcheck-comment.yml, link-check-comment.yml) omit contents: read and work correctly.
Problem
Preview deploys fail for PRs opened from forks (e.g. #950). On
pull_requestevents from a fork, GitHub withholds repository secrets, so the Vercel step runs with an empty token:The build itself passes — only the deploy step fails, surfacing a red X to external contributors.
Approach
Split the preview into build (untrusted, no secrets) and deploy (trusted, secrets) so the Vercel token never touches fork-authored build code:
preview-site.yml(pull_request) now only builds the Astro site and uploads the prebuilt.vercel/output+ PR number as an artifact. No secrets, no deploy. Addedlabeledto the trigger types so applying the label re-runs the build and chains a deploy.deploy-preview.yml(workflow_run) runs in the base-repo context (secrets available), is gated by the maintainer-onlypreviewlabel, downloads the prebuilt artifact, and runsvercel deploy --prebuilt. It posts the preview URL as a sticky PR comment.Because the deploy job only ships an already-built artifact (no
pnpm buildof fork code runs with the token in scope), this avoids the secret-exfiltration risk ofpull_request_target/issue_commentpatterns.Maintainer usage
Add the
previewlabel to a fork PR → the build re-runs → a preview is deployed and the URL is commented. Remove + re-add the label to redeploy after reviewing new commits.Notes
PUBLIC_HUB_API_URL/PUBLIC_COMFY_CLOUD_URLare baked at build time and remain unset for fork builds (pre-existing). If correct preview endpoints are needed for fork PRs, move these non-secret public values to repo Variables. Out of scope here.workflow_runworkflows don't appear as PR status checks, so contributors no longer see a red X for the deploy.