Add gh-pages deployment pipeline with PR preview support#32
Conversation
Split deployment into two workflows: - deploy.yml: production deployment on push to main using peaceiris/actions-gh-pages with keep_files to preserve previews - pr-preview.yml: automatic PR preview deployments to /pr-preview/pr-<number>/ using rossjrw/pr-preview-action, with automatic cleanup on PR close The PR preview workflow leverages the existing VITE_BASE_PATH env var to build with the correct base path for each preview subdirectory. https://claude.ai/code/session_01K7Zr3qqq3S12eDnou4ySBU
Split deploy.yml into separate build and deploy jobs. The build job now runs on both push and pull_request events, ensuring the production build (with default base path) is always verified before merging. The deploy job only runs on push to main. https://claude.ai/code/session_01K7Zr3qqq3S12eDnou4ySBU
📝 WalkthroughWalkthroughRestructures CI/CD: main deployment workflow now builds artifacts and deploys via peaceiris/actions-gh-pages@v4, adds a new PR preview workflow to build and deploy preview sites per PR, and updates the local deploy script and messaging (Node.js bumped to 20, .nojekyll moved to dist, concurrent workflow controls adjusted). Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant GH as GitHub Events
participant Runner as Actions Runner
participant Artifact as Actions Artifact Store
participant Pages as GitHub Pages (peaceiris action)
GH->>Runner: trigger deploy workflow (push to main)
Runner->>Runner: checkout, setup node (v20), install, build
Runner->>Artifact: upload build artifact (production-build)
GH->>Runner: start deploy job
Runner->>Artifact: download production-build
Runner->>Pages: invoke peaceiris/actions-gh-pages@v4 (publish_dir -> dist)
Pages-->>GH: publish site to GitHub Pages
sequenceDiagram
autonumber
participant GH as GitHub PR Event
participant Runner as Actions Runner
participant ArtifactStore as PR Preview Action (rossjrw)
participant PreviewHost as PR Preview Hosting
GH->>Runner: trigger pr-preview workflow (PR opened/sync/reopen)
Runner->>Runner: checkout, setup node, install, build (VITE vars set)
Runner->>ArtifactStore: deploy `./dist` to umbrella `pr-preview`
ArtifactStore->>PreviewHost: publish PR-specific preview site
GH->>Runner: if PR closed -> skip build & deploy, optionally remove preview
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/deploy.yml (1)
13-14: Consider grouping by ref for push concurrency.
Using the sha means each main push gets a unique group, so older deployments won’t be cancelled. Grouping bygithub.reffor pushes helps ensure only the latest main deployment runs.♻️ Suggested concurrency grouping
concurrency: - group: pages-${{ github.event_name }}-${{ github.event.number || github.sha }} + group: pages-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/deploy.yml around lines 13 - 14, Change the concurrency group expression so push events are grouped by ref instead of SHA: replace the current "group: pages-${{ github.event_name }}-${{ github.event.number || github.sha }}" with a conditional expression that uses github.ref for push events (e.g. evaluate github.event_name == 'push' ? github.ref : github.event.number || github.sha) so pushes to the same branch share a group and cancel previous runs while keeping the existing behavior for other event types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/pr-preview.yml:
- Around line 16-18: The workflow’s preview job uses the pull_request trigger
which gives forked PRs a read-only GITHUB_TOKEN, causing
rossjrw/pr-preview-action to fail; update the preview job (job name "preview")
to either switch the workflow trigger to pull_request_target and adopt a safe
checkout strategy before running rossjrw/pr-preview-action, or add a guard to
skip forked PRs by adding an if condition on the preview job such as checking
github.event.pull_request.head.repo.fork == false (or ensuring github.repository
== github.event.pull_request.head.repo.full_name) so the action only runs for
non-fork PRs where the GITHUB_TOKEN has write permissions.
---
Nitpick comments:
In @.github/workflows/deploy.yml:
- Around line 13-14: Change the concurrency group expression so push events are
grouped by ref instead of SHA: replace the current "group: pages-${{
github.event_name }}-${{ github.event.number || github.sha }}" with a
conditional expression that uses github.ref for push events (e.g. evaluate
github.event_name == 'push' ? github.ref : github.event.number || github.sha) so
pushes to the same branch share a group and cancel previous runs while keeping
the existing behavior for other event types.
- pr-preview.yml: Skip preview deployment for forked PRs since pull_request provides a read-only GITHUB_TOKEN for forks, which would cause the gh-pages push to fail - deploy.yml: Use event number (PRs) or ref (pushes) for concurrency grouping so concurrent main deploys cancel stale runs instead of each getting a unique SHA-based group https://claude.ai/code/session_01K7Zr3qqq3S12eDnou4ySBU
Deploying git-vegas with
|
| Latest commit: |
ce2177c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6e0cc8df.git-vegas.pages.dev |
| Branch Preview URL: | https://claude-gh-pages-deployment-p.git-vegas.pages.dev |
There was a problem hiding this comment.
Pull request overview
Adds a GitHub Pages deployment pipeline split into production deployments on main and per-PR preview deployments, using the existing Vite base-path configuration to support serving under subdirectories.
Changes:
- Reworks production deployment workflow to publish via
peaceiris/actions-gh-pagesand preserve preview directories. - Adds a dedicated PR preview workflow that deploys to
/pr-preview/pr-<number>/and cleans up on PR close. - Updates
deploy.shmessaging/behavior for local build/manual deployment guidance.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
deploy.sh |
Updates local build script notes and .nojekyll creation to align with GH Actions-based deployments. |
.github/workflows/deploy.yml |
Switches production deployment to actions-gh-pages and adds artifact-based build/deploy separation. |
.github/workflows/pr-preview.yml |
Introduces PR preview builds/deployments using rossjrw/pr-preview-action. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| on: | ||
| push: | ||
| branches: [ main ] | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [ main ] | ||
| branches: [main] | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pages: write | ||
| id-token: write | ||
| contents: write | ||
|
|
There was a problem hiding this comment.
This workflow runs on pull_request but requests contents: write at the workflow level. That grants a write-capable GITHUB_TOKEN to all steps when the workflow runs for same-repo PRs, which is unnecessary for the build and increases blast radius if a PR is compromised. Consider removing the pull_request trigger here (since previews have their own workflow) and/or scoping permissions to contents: read by default with job-level permissions: contents: write only on the deploy job.
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| concurrency: | ||
| group: pr-preview-${{ github.event.number }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| preview: | ||
| if: github.event.pull_request.head.repo.full_name == github.repository | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node.js | ||
| if: github.event.action != 'closed' | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'npm' | ||
|
|
||
| - name: Install dependencies | ||
| if: github.event.action != 'closed' |
There was a problem hiding this comment.
This workflow checks out and executes PR code (npm ci/npm run build) with a write-capable GITHUB_TOKEN (contents: write). That allows same-repo PRs to use the token to push arbitrary commits/tags or otherwise modify the repository beyond the intended gh-pages updates. Consider splitting into a read-only build job and a separate deploy/cleanup job with elevated permissions, and/or gating deployments to same-repo branches only (e.g., skip if github.event.pull_request.head.repo.fork == true).
| VITE_BASE_PATH: /git-vegas/pr-preview/pr-${{ github.event.number }}/ | ||
| VITE_SCOPE: /git-vegas/pr-preview/pr-${{ github.event.number }}/ | ||
| VITE_START_URL: /git-vegas/pr-preview/pr-${{ github.event.number }}/ |
There was a problem hiding this comment.
The preview base path is hard-coded to /git-vegas/.... If the repo name or Pages base path changes, previews will break. Prefer deriving the repo path from GitHub context (e.g., /${{ github.event.repository.name }}/...) to keep previews portable.
| VITE_BASE_PATH: /git-vegas/pr-preview/pr-${{ github.event.number }}/ | |
| VITE_SCOPE: /git-vegas/pr-preview/pr-${{ github.event.number }}/ | |
| VITE_START_URL: /git-vegas/pr-preview/pr-${{ github.event.number }}/ | |
| VITE_BASE_PATH: /${{ github.event.repository.name }}/pr-preview/pr-${{ github.event.number }}/ | |
| VITE_SCOPE: /${{ github.event.repository.name }}/pr-preview/pr-${{ github.event.number }}/ | |
| VITE_START_URL: /${{ github.event.repository.name }}/pr-preview/pr-${{ github.event.number }}/ |
| echo "" | ||
| echo " Automatic (recommended):" | ||
| echo " Push to main -> production deployment to GitHub Pages" | ||
| echo " Open a PR -> preview deployment at /pr-preview/pr-<number>/" |
There was a problem hiding this comment.
The script prints the preview URL as /pr-preview/pr-<number>/, but the build/workflow configuration uses /git-vegas/pr-preview/pr-<number>/ (repo subpath on GitHub Pages). Update the message to reflect the actual served path so local/manual users aren’t misled.
| echo " Open a PR -> preview deployment at /pr-preview/pr-<number>/" | |
| echo " Open a PR -> preview deployment at /git-vegas/pr-preview/pr-<number>/" |
| with: | ||
| github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| publish_dir: ./dist | ||
| keep_files: true |
There was a problem hiding this comment.
keep_files: true preserves PR preview directories, but it also prevents the action from deleting removed/renamed production assets at the site root. Over time this can leave stale JS/CSS and other artifacts in gh-pages, increasing size and potentially serving outdated files if referenced. Consider adding a cleanup step that removes everything except pr-preview/ before publishing, or an alternative deployment strategy that preserves previews without keeping all historical production files.
| keep_files: true |
| steps: | ||
| - name: Download build artifact | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: production-build | ||
| path: ./dist | ||
|
|
||
| - name: Deploy to GitHub Pages | ||
| id: deployment | ||
| uses: actions/deploy-pages@v4 No newline at end of file | ||
| uses: peaceiris/actions-gh-pages@v4 | ||
| with: | ||
| github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| publish_dir: ./dist | ||
| keep_files: true |
There was a problem hiding this comment.
peaceiris/actions-gh-pages expects a git working copy (a .git directory) to commit/push to the gh-pages branch. The deploy job currently only downloads the build artifact, so it will run outside a checked-out repository and is likely to fail. Add an actions/checkout@v4 step (or otherwise ensure a git repo is present) before the deploy action.
| concurrency: | ||
| group: "pages" | ||
| cancel-in-progress: false | ||
| group: pages-${{ github.event.number || github.ref }} | ||
| cancel-in-progress: true |
There was a problem hiding this comment.
The concurrency group includes ${{ github.sha }} for push events, which makes the group unique per commit. That prevents cancel-in-progress from ever canceling prior runs and also allows multiple main deployments to run concurrently, potentially causing racing pushes to gh-pages. Use a stable group key for push to main (e.g., based on github.ref) so deployments serialize or cancel as intended.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/pr-preview.yml (1)
36-42: Usegithub.event.repository.nameinstead of hardcoding the repository name.
This makes the workflow portable to repository renames. The variable is available inpull_requestevent contexts and will correctly reference the target repository.♻️ Suggested change
- VITE_BASE_PATH: /git-vegas/pr-preview/pr-${{ github.event.number }}/ - VITE_SCOPE: /git-vegas/pr-preview/pr-${{ github.event.number }}/ - VITE_START_URL: /git-vegas/pr-preview/pr-${{ github.event.number }}/ + VITE_BASE_PATH: /${{ github.event.repository.name }}/pr-preview/pr-${{ github.event.number }}/ + VITE_SCOPE: /${{ github.event.repository.name }}/pr-preview/pr-${{ github.event.number }}/ + VITE_START_URL: /${{ github.event.repository.name }}/pr-preview/pr-${{ github.event.number }}/🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/pr-preview.yml around lines 36 - 42, Replace the hardcoded repository prefix "/git-vegas" in the VITE_* env vars with the pull request event variable github.event.repository.name so the workflow remains correct after repo renames; update VITE_BASE_PATH, VITE_SCOPE, and VITE_START_URL to build paths like /${{ github.event.repository.name }}/pr-${{ github.event.number }}/ (keeping the existing /pr-${{ github.event.number }}/ suffix) so these variables reference the target repository dynamically in pull_request contexts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/deploy.yml:
- Around line 9-11: The workflow currently sets workflow-level permissions to
"contents: write" which grants write access to all jobs; change the top-level
permissions key to "contents: read" and then add a job-level permissions
override for the deploy job (the job named "deploy") setting "permissions:
contents: write" so only the deploy job has write access while all other jobs
(e.g., build on pull_request) run with read-only content access.
---
Nitpick comments:
In @.github/workflows/pr-preview.yml:
- Around line 36-42: Replace the hardcoded repository prefix "/git-vegas" in the
VITE_* env vars with the pull request event variable
github.event.repository.name so the workflow remains correct after repo renames;
update VITE_BASE_PATH, VITE_SCOPE, and VITE_START_URL to build paths like /${{
github.event.repository.name }}/pr-${{ github.event.number }}/ (keeping the
existing /pr-${{ github.event.number }}/ suffix) so these variables reference
the target repository dynamically in pull_request contexts.
| permissions: | ||
| contents: read | ||
| pages: write | ||
| id-token: write | ||
| contents: write | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine the deploy.yml workflow file
find . -name "deploy.yml" -path "*/.github/workflows/*"Repository: kertal/git-vegas
Length of output: 90
🏁 Script executed:
# Once found, examine the file to see the current permissions setup
cat -n .github/workflows/deploy.yml | head -60Repository: kertal/git-vegas
Length of output: 1731
🌐 Web query:
GitHub Actions job-level permissions override workflow-level permissions
💡 Result:
Yes—job-level permissions override workflow-level permissions for that specific job.
How the effective GITHUB_TOKEN permissions are calculated
- Start with the default permissions set at the enterprise/org/repo level.
- Apply any
permissions:set at the workflow level. - Apply any
jobs.<job_id>.permissions:set at the job level (this is the override step). - If triggered by a fork PR (and write tokens aren’t allowed), any remaining
writepermissions may be reduced. [1]
Important details
- When you use
permissions(at workflow or job scope), any permissions you don’t specify are set tonone(exceptmetadata, which always has read access). [1][2]
Example
permissions:
contents: read # default for all jobs in this workflow
jobs:
build:
runs-on: ubuntu-latest
steps: ...
release:
runs-on: ubuntu-latest
permissions: # overrides workflow-level permissions for this job only
contents: write
steps: ...Reusable workflows nuance
If a job calls a reusable workflow, the called workflow can only downgrade (not elevate) the permissions passed from the caller job. [3]
Sources: GitHub Docs on permission calculation and unspecified permissions behavior [1][2], and reusable workflows permission rules [3].
[1] (docs.github.com)
[2] (docs.github.com)
[3] (docs.github.com)
Citations:
- 1: https://docs.github.com/ru/actions/security-for-github-actions/security-guides/automatic-token-authentication?utm_source=openai
- 2: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions?source=post_page-----b008ed5f3edc&utm_source=openai
- 3: https://docs.github.com/actions/using-workflows/reusing-workflows?utm_source=openai
🌐 Web query:
GitHub Actions pull_request event security considerations write permissions
💡 Result:
pull_request event: security + write-permission reality
-
Fork PRs are intentionally “untrusted”:
- Workflows triggered by
pull_requestfrom forks get a read-onlyGITHUB_TOKENand no repository secrets (even if your workflow YAML requests write). This prevents external contributors from exfiltrating secrets or pushing to your repo. [1], [2]
- Workflows triggered by
-
Same-repo PRs can be “trusted” (but still risky if you run attacker-controlled input):
- For PRs where the author can push branches in the same repo, the workflow is not treated like a fork, so the effective token privileges depend on your repo/org token-permissions settings and the workflow’s
permissions:block. Injection bugs (e.g., unsafe use of PR title/body in scripts) can still be exploited. [3]
- For PRs where the author can push branches in the same repo, the workflow is not treated like a fork, so the effective token privileges depend on your repo/org token-permissions settings and the workflow’s
-
Default token permissions might be broader than you think:
- GitHub provides controls to set the default
GITHUB_TOKENpermissions at the org/repo level (commonly read/write by default, unless you change it). Best practice is to set defaults to read-only and grant write scopes only per-workflow/job. [4], [5]
- GitHub provides controls to set the default
What to do if you need “write” actions on PRs (comments/labels/status updates)
-
Prefer
pull_requestfor building/testing untrusted code, and keep it minimally privileged:- Set explicit least-privilege permissions (often
contents: read) and don’t rely on secrets. [4], [5]
- Set explicit least-privilege permissions (often
-
Be very careful with
pull_request_target:pull_request_targetruns in the base repo context and can have secrets + write token—if you check out and execute PR code in that context, you can create a “pwn request” (untrusted code with elevated privileges). Use it only for safe, non-code-executing tasks like labeling/commenting based on metadata. [6], [7]
-
Safer pattern for “test untrusted code, then do privileged updates”:
- Run CI on
pull_request(untrusted, no secrets), upload results as artifacts; then use a second workflow triggered byworkflow_run(trusted context) to post comments/update PRs using artifacts. [6], [7]
- Run CI on
Minimal example: lock down pull_request
on: pull_request
permissions:
contents: read(Then add only the exact additional scopes you need per job.) [4], [5]
Special case: Dependabot PRs
- Workflows triggered by Dependabot PRs are treated like fork PRs for
pull_request: read-only token, no secrets, so writes will fail unless you redesign using trusted triggers/patterns. [8]
Sources
[1] GitHub Security Lab: preventing “pwn requests” and event behaviors. (securitylab.github.com)
[2] Synacktiv: fork PR GITHUB_TOKEN restrictions + no secrets. (synacktiv.com)
[3] GitHub Blog: handling untrusted input + notes on pull_request privilege boundaries/injection risk. (github.blog)
[4] GitHub Docs: automatic token authentication / modifying GITHUB_TOKEN permissions. (docs.github.com)
[5] GitHub Changelog: controls for GITHUB_TOKEN permissions; fork PRs remain read-only. (github.blog)
[6] GitHub Well-Architected Actions security: avoid pull_request_target pitfalls. (wellarchitected.github.com)
[7] GitHub Blog (product news): pull_request_target and workflow_run trusted-context behavior. (github.blog)
[8] GitHub Changelog: Dependabot PRs run with read-only permissions. (github.blog)
Citations:
- 1: https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/?utm_source=openai
- 2: https://www.synacktiv.com/publications/github-actions-exploitation-introduction?utm_source=openai
- 3: https://github.blog/security/supply-chain-security/four-tips-to-keep-your-github-actions-workflows-secure/?utm_source=openai
- 4: https://docs.github.com/en/actions/security-guides/automatic-token-authentication?utm_source=openai
- 5: https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token/?utm_source=openai
- 6: https://wellarchitected.github.com/library/application-security/recommendations/actions-security/?utm_source=openai
- 7: https://github.blog/news-insights/product-news/github-actions-improvements-for-fork-and-pull-request-workflows/?utm_source=openai
- 8: https://github.blog/changelog/2021-02-19-github-actions-workflows-triggered-by-dependabot-prs-will-run-with-read-only-permissions/?utm_source=openai
Scope write permissions to deploy job only.
The workflow grants contents: write for all jobs including pull_request events, where the build job executes PR code via npm run build. Tighten to least privilege by setting workflow-level permissions to contents: read and granting contents: write only to the deploy job, which is the only job that requires write access.
Suggested change
permissions:
- contents: write
+ contents: read
concurrency:
group: pages-${{ github.event.number || github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Create .nojekyll
run: touch ./dist/.nojekyll
- name: Upload build artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: production-build
path: ./dist
deploy:
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
+ permissions:
+ contents: write
runs-on: ubuntu-latest📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| contents: write | |
| permissions: | |
| contents: read |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/deploy.yml around lines 9 - 11, The workflow currently
sets workflow-level permissions to "contents: write" which grants write access
to all jobs; change the top-level permissions key to "contents: read" and then
add a job-level permissions override for the deploy job (the job named "deploy")
setting "permissions: contents: write" so only the deploy job has write access
while all other jobs (e.g., build on pull_request) run with read-only content
access.
Split deployment into two workflows:
peaceiris/actions-gh-pages with keep_files to preserve previews
/pr-preview/pr-/ using rossjrw/pr-preview-action,
with automatic cleanup on PR close
The PR preview workflow leverages the existing VITE_BASE_PATH env var
to build with the correct base path for each preview subdirectory.
https://claude.ai/code/session_01K7Zr3qqq3S12eDnou4ySBU