-
Notifications
You must be signed in to change notification settings - Fork 10
190 lines (175 loc) · 7.73 KB
/
Copy pathpreview.yml
File metadata and controls
190 lines (175 loc) · 7.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
name: PR Preview
on:
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: "preview-${{ github.head_ref }}"
cancel-in-progress: true
jobs:
deploy-preview:
runs-on: ubuntu-latest
# Secrets (and write access to gh-pages) are unavailable for forked PRs, so only
# deploy previews for branches pushed to this repository.
if: github.event.pull_request.head.repo.full_name == github.repository
permissions:
contents: write
pull-requests: write
issues: write
deployments: read
steps:
- name: Mark existing preview comment as updating
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = context.payload.pull_request.number;
const sha = context.payload.pull_request.head.sha;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr,
});
const existing = comments.find(
c => c.user.login === 'github-actions[bot]' && c.body.includes('PR Preview Deployed')
);
if (existing) {
// Strip any prior "(deploying ...)" marker rather than stacking, in case
// a previous run was cancelled (via concurrency) before it could clean up.
const body = existing.body
.replace(/ _\(deploying .+?\)_$/, '')
.trimEnd();
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: body + ` _(deploying ${sha}...)_`,
});
}
- name: Checkout code
uses: actions/checkout@v4
with:
# The deploy below authenticates with an explicit token URL, so the
# checkout credential is unused and best not persisted before pnpm install --frozen-lockfile.
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v6
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build site (preview base URL)
run: pnpm run build
env:
BASE_URL: /previews/pr-${{ github.event.pull_request.number }}/
REPO_GITHUB_API_TOKEN: ${{ secrets.REPO_GITHUB_API_TOKEN }}
- name: Deploy to gh-pages branch (preview subdirectory)
id: deploy
run: |
set -euo pipefail
repo_url="https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
# gh-pages commits in its own temporary clone, so set the identity globally
# rather than via --user (whose parser rejects the github-actions[bot]
# brackets). Retry on a non-fast-forward rejection in case a concurrent
# gh-pages write (another preview, the cleanup job, or the production deploy)
# lands first; each run re-clones the branch, so clear the cache between attempts.
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
pushed=false
for attempt in 1 2 3; do
rm -rf node_modules/.cache/gh-pages
if pnpm exec gh-pages \
--dist dist/public \
--dest previews/pr-${{ github.event.pull_request.number }} \
--branch gh-pages \
--nojekyll \
--add \
--message "preview: pr-${{ github.event.pull_request.number }} @ ${{ github.sha }}" \
--repo "$repo_url"; then
pushed=true
break
fi
echo "gh-pages push failed (attempt ${attempt}); retrying."
sleep $((attempt * 3))
done
$pushed || { echo "Failed to deploy preview after 3 attempts."; exit 1; }
# gh-pages nests its clone under find-cache-dir's path, not directly at
# node_modules/.cache/gh-pages, so read the pushed tip back from the
# remote rather than relying on that cache layout.
echo "sha=$(git ls-remote "$repo_url" gh-pages | cut -f1)" >> "$GITHUB_OUTPUT"
- name: Wait for GitHub Pages deployment
run: |
set -uo pipefail
sha="${{ steps.deploy.outputs.sha }}"
api="https://api.github.com/repos/${{ github.repository }}"
auth=(-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github+json")
gh_api() { curl -s --max-time 15 "${auth[@]}" "$@"; }
id=""
for attempt in $(seq 1 30); do
if [ -z "$id" ]; then
# Query by exact commit SHA; the legacy /pages/builds/latest endpoint
# isn't a reliable per-commit signal.
id=$(gh_api "${api}/deployments?sha=${sha}&environment=github-pages" | jq -r '.[0].id // empty')
if [ -z "$id" ]; then
echo "No GitHub Pages deployment recorded yet for ${sha} (attempt ${attempt}/30)..."
sleep 10
continue
fi
fi
state=$(gh_api "${api}/deployments/${id}/statuses" | jq -r '.[0].state // empty')
if [ "$state" = "success" ]; then
echo "GitHub Pages deployment for ${sha} succeeded."
exit 0
fi
if [ "$state" = "failure" ] || [ "$state" = "error" ]; then
echo "GitHub Pages deployment for ${sha} failed (state: ${state})."
exit 1
fi
echo "GitHub Pages deployment for ${sha} is ${state:-pending} (attempt ${attempt}/30)..."
sleep 10
done
echo "GitHub Pages deployment did not confirm within 5 minutes; posting the link anyway."
- name: Post preview URL to PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pr = context.payload.pull_request.number;
const sha = context.payload.pull_request.head.sha.slice(0, 7);
const url = `https://markojs.com/previews/pr-${pr}/`;
// Find and update an existing bot comment, or create a new one.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr,
});
const existing = comments.find(
c => c.user.login === 'github-actions[bot]' && c.body.includes('PR Preview Deployed')
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: existing.body
.replace('### PR Preview Deployed _(removed)_', '### PR Preview Deployed')
.replace(/ _\(deploying .+?\)_$/, '')
.replace(/###### commit .+$/, `###### commit ${sha}`),
});
} else {
const body = [
`### PR Preview Deployed`,
``,
`Your changes are live at [**markojs.com/previews/pr-${pr}**](${url}).`,
``,
`###### commit ${sha}`,
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr,
body,
});
}