Skip to content

Commit 43ea3a0

Browse files
Link cloudflare previews in PR description
1 parent 0da8147 commit 43ea3a0

3 files changed

Lines changed: 114 additions & 39 deletions

File tree

‎.github/scripts/preview-panel.cjs‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Preview panel in the PR description, kept between two markers.
2+
3+
const START = '<!-- cloudflare-preview:start -->';
4+
const END = '<!-- cloudflare-preview:end -->';
5+
const SECTION = new RegExp(`${START}[\\s\\S]*?${END}`);
6+
7+
function alert(kind, heading, lines = []) {
8+
return [`> [!${kind}]`, `> **${heading}**`, ...(lines.length ? ['>', ...lines.map((l) => `> ${l}`)] : [])];
9+
}
10+
11+
function pending() {
12+
return alert('NOTE', 'Preview deployment pending', [
13+
'This panel updates automatically when the Cloudflare previews finish building.',
14+
]);
15+
}
16+
17+
function result(previews, { sha, logs }) {
18+
const failed = previews.filter((p) => p.status !== 'success');
19+
const head = failed.length
20+
? alert('CAUTION', `Preview deployment failed for ${failed.map((p) => p.site).join(', ')}`, [
21+
`[View workflow logs](${logs})`,
22+
])
23+
: alert('IMPORTANT', 'Preview ready', [`[View workflow logs](${logs})`]);
24+
const rows = previews.map((p) =>
25+
p.status === 'success'
26+
? `| ${p.site} | [Latest on branch](${p.branch}) | [This commit](${p.commit}) | ✅ Deployed |`
27+
: `| ${p.site} | | | ❌ Failed |`,
28+
);
29+
return [...head, '', `Commit \`${sha.substring(0, 7)}\``, '', '| Site | Branch | Commit | Status |', '| --- | --- | --- | --- |', ...rows];
30+
}
31+
32+
async function update({ github, context }, lines) {
33+
const block = [START, ...lines, END].join('\n');
34+
const { owner, repo } = context.repo;
35+
const pull_number = context.payload.pull_request.number;
36+
// Re-read so edits made during the build are kept.
37+
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
38+
const current = pr.body || '';
39+
const body = SECTION.test(current) ? current.replace(SECTION, block) : `${block}\n\n${current}`;
40+
if (body.replace(/\r\n/g, '\n') === current.replace(/\r\n/g, '\n')) return;
41+
await github.rest.pulls.update({ owner, repo, pull_number, body });
42+
}
43+
44+
module.exports = { pending, result, update };

‎.github/workflows/preview.yml‎

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: Preview
22

3-
# Builds every site under demo/sites/ for a pull request and deploys each to
4-
# its own Cloudflare Pages project (uq-rcc-<site>). Each preview is served at
5-
# the root of its own hostname, so root-relative links need no base path.
6-
# Production stays on GitHub Pages; this workflow never deploys main.
3+
# Deploys each site under demo/sites/ to Cloudflare Pages project uq-rcc-<site>.
74

85
on:
96
pull_request:
@@ -32,6 +29,24 @@ jobs:
3229
list=$(find demo/sites -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort | jq -Rsc 'split("\n") | map(select(length > 0))')
3330
echo "list=$list" >> "$GITHUB_OUTPUT"
3431
32+
pending:
33+
needs: sites
34+
runs-on: ubuntu-24.04
35+
permissions:
36+
contents: read
37+
pull-requests: write
38+
steps:
39+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40+
with:
41+
sparse-checkout: .github/scripts
42+
43+
- name: Mark preview as pending
44+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
45+
with:
46+
script: |
47+
const panel = require('./.github/scripts/preview-panel.cjs');
48+
await panel.update({ github, context }, panel.pending());
49+
3550
deploy:
3651
needs: sites
3752
runs-on: ubuntu-24.04
@@ -58,52 +73,71 @@ jobs:
5873
run: npx astro build
5974

6075
- name: Deploy to Cloudflare Pages
76+
id: deploy
6177
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4
6278
with:
6379
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
6480
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
6581
command: >-
6682
pages deploy demo/sites/${{ matrix.site }}/dist
6783
--project-name=uq-rcc-${{ matrix.site }}
68-
--branch=pr-${{ github.event.pull_request.number }}
84+
--branch=${{ github.head_ref }}
6985
70-
comment:
71-
needs: [sites, deploy]
86+
# Matrix jobs cannot merge outputs, so pass results on as artifacts.
87+
- name: Record preview result
88+
if: always()
89+
env:
90+
SITE: ${{ matrix.site }}
91+
STATUS: ${{ steps.deploy.outcome }}
92+
COMMIT_URL: ${{ steps.deploy.outputs.deployment-url }}
93+
BRANCH_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }}
94+
run: |
95+
jq -n '{site: env.SITE, status: env.STATUS, commit: env.COMMIT_URL, branch: env.BRANCH_URL}' > preview.json
96+
97+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
98+
if: always()
99+
with:
100+
name: preview-${{ matrix.site }}
101+
path: preview.json
102+
retention-days: 1
103+
104+
panel:
105+
needs: [sites, pending, deploy]
106+
if: always() && needs.sites.result == 'success'
72107
runs-on: ubuntu-24.04
73108
permissions:
109+
contents: read
74110
pull-requests: write
75111
steps:
76-
- name: Post preview links
112+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
113+
with:
114+
sparse-checkout: .github/scripts
115+
116+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
117+
with:
118+
pattern: preview-*
119+
path: previews
120+
121+
- name: Update preview panel
77122
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
78123
env:
79124
SITES: ${{ needs.sites.outputs.list }}
80125
with:
81126
script: |
82-
const pr = context.payload.pull_request;
83-
const marker = '<!-- preview-links -->';
84-
const sha = pr.head.sha.substring(0, 7);
85-
// --branch=pr-N gives each project a stable alias for this PR.
86-
const rows = JSON.parse(process.env.SITES).map(
87-
(site) => `| ${site} | https://pr-${pr.number}.uq-rcc-${site}.pages.dev |`,
88-
);
89-
const body = [
90-
marker,
91-
`**Previews** for \`${sha}\``,
92-
'',
93-
'| Site | URL |',
94-
'| --- | --- |',
95-
...rows,
96-
].join('\n');
97-
98-
const { owner, repo } = context.repo;
99-
const comments = await github.paginate(github.rest.issues.listComments, {
100-
owner, repo, issue_number: pr.number,
127+
const fs = require('node:fs');
128+
const path = require('node:path');
129+
const panel = require('./.github/scripts/preview-panel.cjs');
130+
131+
const previews = JSON.parse(process.env.SITES).map((site) => {
132+
const file = path.join('previews', `preview-${site}`, 'preview.json');
133+
return fs.existsSync(file)
134+
? JSON.parse(fs.readFileSync(file, 'utf8'))
135+
: { site, status: 'failure' };
101136
});
102-
const existing = comments.find(
103-
(c) => c.user?.type === 'Bot' && c.body?.startsWith(marker),
104-
);
105-
if (existing) {
106-
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
107-
} else {
108-
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
109-
}
137+
138+
const logs = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
139+
const sha = context.payload.pull_request.head.sha;
140+
await panel.update({ github, context }, panel.result(previews, { sha, logs }));
141+
142+
const failed = previews.filter((p) => p.status !== 'success');
143+
if (failed.length) core.setFailed(`Preview failed for ${failed.map((p) => p.site).join(', ')}`);

‎demo/shared/define-site.mjs‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
// Shared config for every demo site. A site passes only what differs; the
2-
// styles and theme toggle come straight from the live XNAT site in src/, to
3-
// show the framework can be shared without copying it.
1+
// Shared config for the demo sites, reusing the live site's styles from src/.
42
import { fileURLToPath } from 'node:url';
53
import { defineConfig } from 'astro/config';
64
import starlight from '@astrojs/starlight';
@@ -10,7 +8,6 @@ const repo = (path) => fileURLToPath(new URL(`../../${path}`, import.meta.url));
108
export function defineSite({ url, title, description, sidebar }) {
119
return defineConfig({
1210
site: url,
13-
// Sites import shared files from outside their own folder.
1411
vite: { server: { fs: { allow: [repo('')] } } },
1512
integrations: [
1613
starlight({

0 commit comments

Comments
 (0)