Skip to content

Commit fb70d29

Browse files
committed
Add PR preview deployment workflow for fork contributions
Enables automatic preview deployments for PRs from forks, streamlining translation review and content editing approval. Features: - Builds Jekyll site for fork PRs only (excludes internal PRs) - Deploys to fork's GitHub Pages under pr-{number}/ directory - Auto-creates gh-pages branch if needed - Posts preview URL as PR comment - Uses fine-grained PAT with minimal permissions (Contents + Pages) Setup guide included for fork owners. 💖 Generated with Crush Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
1 parent d82e05f commit fb70d29

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# PR Preview Setup Guide
2+
3+
This workflow automatically builds and deploys preview versions of the site for pull requests coming from forks. This is especially useful for translation reviews and content editing.
4+
5+
## How it works
6+
7+
1. When a PR is opened from a fork, the workflow builds the Jekyll site
8+
2. The built site is deployed to the fork's GitHub Pages under `pr-{number}/`
9+
3. A comment is posted on the PR with the preview URL
10+
4. The preview updates automatically when new commits are pushed
11+
12+
## Setup Required (For Fork Owners)
13+
14+
To enable preview deployments from your fork, you need to:
15+
16+
### 1. Create a Fine-Grained Personal Access Token
17+
- Go to GitHub Settings → Developer settings → Personal access tokens → **Fine-grained tokens**
18+
- Click "Generate new token"
19+
- Configure the token:
20+
- **Token name:** `PR Preview Deploy`
21+
- **Expiration:** Choose your preferred expiration (90 days, 1 year, custom, or no expiration)
22+
- **Resource owner:** Select yourself (the fork owner)
23+
- **Repository access:** Select "Only select repositories"
24+
- Choose your fork repository (e.g., `your-username/training-kit`)
25+
- **Permissions → Repository permissions:**
26+
-**Contents:** Read and write (required to push to gh-pages branch)
27+
-**Pages:** Read and write (required to manage GitHub Pages)
28+
-**Metadata:** Read-only (automatically included)
29+
- Click "Generate token"
30+
- **Copy the token immediately** (you won't see it again)
31+
32+
### 2. Add the token to your fork's secrets
33+
- Go to your fork's Settings → Secrets and variables → Actions
34+
- Click "New repository secret"
35+
- Name: `PREVIEW_DEPLOY_TOKEN`
36+
- Value: Paste the fine-grained personal access token you created
37+
- Click "Add secret"
38+
39+
### 3. Enable GitHub Pages in your fork (after first deployment)
40+
After the first PR preview is deployed, the gh-pages branch will be created automatically. Then:
41+
- Go to your fork's Settings → Pages
42+
- Under "Source", select "Deploy from a branch"
43+
- Select **gh-pages** branch and **/ (root)** folder
44+
- Click Save
45+
- GitHub Pages will be available at: `https://your-username.github.io/training-kit/`
46+
47+
## Preview URL Format
48+
49+
Previews will be available at:
50+
```
51+
https://{fork-owner}.github.io/{repo-name}/pr-{number}/
52+
```
53+
54+
For example:
55+
```
56+
https://johndoe.github.io/training-kit/pr-42/
57+
```
58+
59+
## Security
60+
61+
- Only runs for PRs from forks (not internal PRs)
62+
- Uses `pull_request_target` to run in the context of the base repository
63+
- Checks out the specific PR SHA to build exactly what was submitted
64+
- Uses fine-grained PAT with minimal required permissions (Contents and Pages only)
65+
- Each fork owner controls their own credentials and deployment
66+
67+
## Notes
68+
69+
- The gh-pages branch is created automatically on first deployment
70+
- The first deployment may take a few minutes for GitHub Pages to become available
71+
- Previews persist until manually deleted from the gh-pages branch
72+
- Each PR gets its own isolated preview directory
73+
- You can clean up old previews by deleting directories from the gh-pages branch

.github/workflows/pr-preview.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: PR Preview Deployment
2+
3+
on:
4+
pull_request_target:
5+
branches: ["main"]
6+
types: [opened, synchronize, reopened]
7+
8+
permissions:
9+
contents: read
10+
pull-requests: write
11+
12+
jobs:
13+
# Only run for PRs from forks
14+
check-fork:
15+
runs-on: ubuntu-latest
16+
outputs:
17+
is-fork: ${{ steps.check.outputs.is-fork }}
18+
steps:
19+
- name: Check if PR is from fork
20+
id: check
21+
run: |
22+
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
23+
echo "is-fork=true" >> $GITHUB_OUTPUT
24+
else
25+
echo "is-fork=false" >> $GITHUB_OUTPUT
26+
fi
27+
28+
build-and-deploy-preview:
29+
needs: check-fork
30+
if: needs.check-fork.outputs.is-fork == 'true'
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout PR branch
34+
uses: actions/checkout@v3
35+
with:
36+
ref: ${{ github.event.pull_request.head.sha }}
37+
38+
- name: Set up Node
39+
uses: actions/setup-node@v3
40+
with:
41+
node-version: 16
42+
cache: 'npm'
43+
44+
- name: Install node modules
45+
run: npm install
46+
47+
- name: Set up Ruby
48+
uses: ruby/setup-ruby@ece82769428359c077b5a5eaff268902a303c101
49+
with:
50+
bundler-cache: true
51+
52+
- name: Build with Jekyll
53+
run: |
54+
# Build with baseurl as the fork owner's username for GitHub Pages
55+
bundle exec jekyll build --baseurl "/${{ github.event.pull_request.head.repo.name }}"
56+
env:
57+
JEKYLL_ENV: production
58+
59+
- name: Deploy to fork's GitHub Pages
60+
uses: peaceiris/actions-gh-pages@v3
61+
with:
62+
# This requires the fork to have a fine-grained PAT as a secret
63+
# The fork owner needs to set up PREVIEW_DEPLOY_TOKEN in their repo secrets
64+
personal_token: ${{ secrets.PREVIEW_DEPLOY_TOKEN }}
65+
publish_dir: ./_site
66+
publish_branch: gh-pages
67+
destination_dir: pr-${{ github.event.pull_request.number }}
68+
user_name: 'github-actions[bot]'
69+
user_email: 'github-actions[bot]@users.noreply.github.com'
70+
# Create gh-pages branch if it doesn't exist
71+
force_orphan: true
72+
73+
- name: Comment preview URL
74+
uses: actions/github-script@v6
75+
with:
76+
script: |
77+
const prNumber = context.payload.pull_request.number;
78+
const forkOwner = context.payload.pull_request.head.repo.owner.login;
79+
const forkRepo = context.payload.pull_request.head.repo.name;
80+
const previewUrl = `https://${forkOwner}.github.io/${forkRepo}/pr-${prNumber}/`;
81+
82+
const comment = `## 📝 Preview Deployment
83+
84+
Your preview is ready! 🎉
85+
86+
🔗 **Preview URL:** ${previewUrl}
87+
88+
This preview will be updated automatically when you push new commits to this PR.
89+
90+
> **Note:** The preview is deployed to the fork's GitHub Pages. Make sure GitHub Pages is enabled in your fork's settings.`;
91+
92+
// Find existing preview comment
93+
const { data: comments } = await github.rest.issues.listComments({
94+
owner: context.repo.owner,
95+
repo: context.repo.repo,
96+
issue_number: prNumber,
97+
});
98+
99+
const botComment = comments.find(comment =>
100+
comment.user.type === 'Bot' &&
101+
comment.body.includes('Preview Deployment')
102+
);
103+
104+
if (botComment) {
105+
// Update existing comment
106+
await github.rest.issues.updateComment({
107+
owner: context.repo.owner,
108+
repo: context.repo.repo,
109+
comment_id: botComment.id,
110+
body: comment
111+
});
112+
} else {
113+
// Create new comment
114+
await github.rest.issues.createComment({
115+
owner: context.repo.owner,
116+
repo: context.repo.repo,
117+
issue_number: prNumber,
118+
body: comment
119+
});
120+
}

0 commit comments

Comments
 (0)