Skip to content

Commit ababd82

Browse files
authored
ci: build and deploy pull request previews from CI (#82)
* ci: build pull request previews as artifacts The site job already builds the site; hand that build and a browser build of the desktop app to the deploy workflow as artifacts, only for pull requests. CI jobs only read the checkout, so make that explicit. * ci: deploy pull request previews from a trusted workflow Publish the CI artifacts of every pull request, forks included, to the modrex-app-pr and modrex-site-pr Pages projects with Direct Upload, and record the outcome as preview/app and preview/site commit statuses. The workflow runs the default-branch code with the Cloudflare token and never checks out or executes the pull request. The static preview comment moves here from the preview-link workflow. * feat(preview): resolve /pr/<N> from commit statuses Read the preview/app and preview/site statuses of the pull request's head commit instead of parsing Cloudflare check-run HTML. The app host keeps proxying the deployed alias; site-preview.modrex.net redirects to it. Every other state renders a small page: building (with refresh), build failed with its logs, no preview, or pull request not found. * docs(preview): describe the site preview and the /pr/ states
1 parent a389ff8 commit ababd82

6 files changed

Lines changed: 409 additions & 209 deletions

File tree

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ on:
1414
- '.gitignore'
1515
- 'LICENSE'
1616

17+
permissions:
18+
contents: read
19+
1720
jobs:
1821
index:
1922
runs-on: ubuntu-latest
@@ -51,6 +54,42 @@ jobs:
5154
- run: pnpm site:test
5255
- run: pnpm site:build
5356

57+
# Consumed by preview-deploy.yml. Function tests would become routes, so they stay out.
58+
- name: Stage the site preview
59+
if: github.event_name == 'pull_request'
60+
run: |
61+
mkdir -p preview/site/functions
62+
cp -R apps/site/dist preview/site/dist
63+
(cd apps/site/functions && find . -name '*.ts' ! -name '*.test.ts' -exec cp --parents {} ../../../preview/site/functions/ \;)
64+
- uses: actions/upload-artifact@v7
65+
if: github.event_name == 'pull_request'
66+
with:
67+
name: site-preview
68+
path: preview/site
69+
retention-days: 3
70+
if-no-files-found: error
71+
72+
app-preview:
73+
if: github.event_name == 'pull_request'
74+
runs-on: ubuntu-latest
75+
timeout-minutes: 15
76+
steps:
77+
- uses: actions/checkout@v7
78+
79+
- uses: pnpm/action-setup@v6
80+
- uses: actions/setup-node@v7
81+
with:
82+
node-version: 22
83+
cache: pnpm
84+
- run: pnpm install --frozen-lockfile
85+
- run: pnpm --filter modrex build-preview
86+
- uses: actions/upload-artifact@v7
87+
with:
88+
name: app-preview
89+
path: apps/desktop/out/preview
90+
retention-days: 3
91+
if-no-files-found: error
92+
5493
test:
5594
runs-on: ubuntu-latest
5695
steps:
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Preview deploy
2+
3+
# Runs default-branch code with the deploy token; the CI artifacts are the only PR input.
4+
on:
5+
workflow_run:
6+
workflows: [CI]
7+
types: [requested, completed]
8+
9+
permissions: {}
10+
11+
concurrency:
12+
group: preview-${{ github.event.workflow_run.head_sha }}
13+
14+
jobs:
15+
preview:
16+
if: github.event.workflow_run.event == 'pull_request'
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 15
19+
permissions:
20+
actions: read
21+
pull-requests: write
22+
statuses: write
23+
env:
24+
GH_TOKEN: ${{ github.token }}
25+
GH_REPO: ${{ github.repository }}
26+
SHA: ${{ github.event.workflow_run.head_sha }}
27+
CI_URL: ${{ github.event.workflow_run.html_url }}
28+
steps:
29+
- name: Resolve pull request
30+
id: pr
31+
run: |
32+
number=$(gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/pulls" \
33+
--jq "[.[] | select(.state == \"open\" and .head.sha == \"$SHA\")][0].number // empty")
34+
echo "number=$number" >> "$GITHUB_OUTPUT"
35+
[ -n "$number" ] || echo "$SHA is not the head of an open pull request; nothing to do."
36+
37+
- name: Mark previews pending
38+
if: steps.pr.outputs.number && github.event.action == 'requested'
39+
env:
40+
PR: ${{ steps.pr.outputs.number }}
41+
run: |
42+
for name in app site; do
43+
gh api "repos/$GITHUB_REPOSITORY/statuses/$SHA" --silent \
44+
-f context="preview/$name" -f state=pending -f description=Building -f target_url="$CI_URL"
45+
done
46+
47+
marker='<!-- modrex-preview -->'
48+
found=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments?per_page=100" \
49+
--jq "[.[] | select(.body | startswith(\"$marker\"))] | length")
50+
[ "$found" = 0 ] || exit 0
51+
gh pr comment "$PR" --body-file - <<EOF
52+
$marker
53+
## Deploying Modrex previews with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages
54+
55+
<table>
56+
<tr><td><strong>App preview:</strong></td><td><a href="https://app-preview.modrex.net/pr/$PR">https://app-preview.modrex.net/pr/$PR</a></td></tr>
57+
<tr><td><strong>Site preview:</strong></td><td><a href="https://site-preview.modrex.net/pr/$PR">https://site-preview.modrex.net/pr/$PR</a></td></tr>
58+
</table>
59+
60+
View logs: [app](https://dash.cloudflare.com/?to=/:account/pages/view/modrex-app-pr) | [site](https://dash.cloudflare.com/?to=/:account/pages/view/modrex-site-pr)
61+
EOF
62+
63+
- uses: actions/download-artifact@v8
64+
if: steps.pr.outputs.number && github.event.action == 'completed'
65+
id: app
66+
continue-on-error: true
67+
with:
68+
name: app-preview
69+
path: app
70+
run-id: ${{ github.event.workflow_run.id }}
71+
github-token: ${{ github.token }}
72+
73+
- uses: actions/download-artifact@v8
74+
if: steps.pr.outputs.number && github.event.action == 'completed'
75+
id: site
76+
continue-on-error: true
77+
with:
78+
name: site-preview
79+
path: site
80+
run-id: ${{ github.event.workflow_run.id }}
81+
github-token: ${{ github.token }}
82+
83+
- name: Deploy previews
84+
if: steps.pr.outputs.number && github.event.action == 'completed'
85+
env:
86+
PR: ${{ steps.pr.outputs.number }}
87+
CI_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
88+
APP_ARTIFACT: ${{ steps.app.outcome }}
89+
SITE_ARTIFACT: ${{ steps.site.outcome }}
90+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
91+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
92+
run: |
93+
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
94+
status() {
95+
gh api "repos/$GITHUB_REPOSITORY/statuses/$SHA" --silent \
96+
-f context="preview/$1" -f state="$2" -f description="$3" -f target_url="$4"
97+
}
98+
99+
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
100+
for name in app site; do status "$name" error "Preview deploys are not configured" "$run_url"; done
101+
exit 0
102+
fi
103+
case "$CI_CONCLUSION" in
104+
success|failure) ;;
105+
*) for name in app site; do status "$name" error "CI $CI_CONCLUSION" "$CI_URL"; done; exit 0 ;;
106+
esac
107+
108+
failed=0
109+
deploy() {
110+
local name=$1 artifact=$2 dir=$3 assets=$4 project=$5
111+
if [ "$artifact" != success ]; then
112+
status "$name" failure "CI produced no preview" "$CI_URL"
113+
failed=1
114+
return
115+
fi
116+
if (cd "$dir" && npx -y wrangler@4.135.0 pages deploy "$assets" \
117+
--project-name "$project" --branch "pr-$PR" --commit-hash "$SHA" --commit-dirty=true); then
118+
status "$name" success Ready "https://pr-$PR.$project.pages.dev"
119+
else
120+
status "$name" failure "Deploy failed" "$run_url"
121+
failed=1
122+
fi
123+
}
124+
deploy app "$APP_ARTIFACT" app . modrex-app-pr
125+
deploy site "$SITE_ARTIFACT" site dist modrex-site-pr
126+
exit $failed

.github/workflows/preview-link.yml

Lines changed: 0 additions & 50 deletions
This file was deleted.

PREVIEW.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ Every pull request has a number next to its title on GitHub. Put that number aft
3636
`https://app-preview.modrex.net/pr/<pull-request-number>`
3737

3838
The `/pr/` address belongs to the pull request, not to one commit. Keep using the same address
39-
while the pull request is open. After a new commit is pushed and Cloudflare finishes the next
40-
deployment, the same address opens the updated version.
39+
while the pull request is open. After a new commit is pushed and its preview is built, the same
40+
address opens the updated version. While the build runs the address shows **Building**, and it
41+
says so if the build failed or no preview exists for the latest commit.
4142

42-
The Cloudflare bot comments on the pull request when a deployment is ready. Its **Preview URL**
43-
keeps the version from one commit. Its **Branch Preview URL** updates with the pull request. The
44-
Modrex `/pr/` address is the readable link to share for ongoing review.
43+
A comment on every pull request lists this address together with the site preview at
44+
`https://site-preview.modrex.net/pr/<pull-request-number>`, which opens the same commit of
45+
modrex.net.
4546

4647
Preview state parameters also work after the pull request number:
4748

0 commit comments

Comments
 (0)