Skip to content

Commit 7396e4a

Browse files
authored
feat!: Introduce the build_website reusable workflow (#211)
1 parent 0ed106a commit 7396e4a

2 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
name: Build FlowFuse website
2+
3+
# The single definition of a website build. Called by FlowFuse/website's own CI and by
4+
# FlowFuse/flowfuse documentation pull requests, so a change to the build, the link
5+
# check or the preview upload is picked up by both callers without being copied.
6+
on:
7+
workflow_call:
8+
inputs:
9+
website_repository:
10+
description: 'Repository holding the website source.'
11+
type: string
12+
default: 'FlowFuse/website'
13+
website_ref:
14+
description: 'Ref of the website source to build.'
15+
type: string
16+
default: 'main'
17+
docs_repository:
18+
description: 'Repository providing the docs/ tree. Empty lets docs-sync clone FlowFuse/flowfuse main itself.'
19+
type: string
20+
default: ''
21+
docs_ref:
22+
description: 'Ref or commit to check out from docs_repository.'
23+
type: string
24+
default: ''
25+
run_unit_tests:
26+
description: 'Run the website unit tests.'
27+
type: boolean
28+
default: false
29+
deploy_preview:
30+
description: 'Build with images and upload the result to Netlify as an aliased, unpublished deploy.'
31+
type: boolean
32+
default: false
33+
preview_alias:
34+
description: 'Netlify deploy alias. Required when deploy_preview is true.'
35+
type: string
36+
default: ''
37+
pr_number:
38+
description: 'Pull request to comment on with the preview URL. 0 disables the comment.'
39+
type: number
40+
default: 0
41+
secrets:
42+
ci_app_id:
43+
description: 'GitHub App id able to read the private blueprint-library repository.'
44+
required: true
45+
ci_app_key:
46+
description: 'Private key for ci_app_id.'
47+
required: true
48+
netlify_auth_token:
49+
description: 'Required when deploy_preview is true.'
50+
required: false
51+
netlify_site_id:
52+
description: 'Required when deploy_preview is true.'
53+
required: false
54+
outputs:
55+
preview_url:
56+
description: 'URL of the uploaded preview. Empty when deploy_preview is false.'
57+
value: ${{ jobs.build.outputs.preview_url }}
58+
59+
jobs:
60+
build:
61+
name: Build and check
62+
runs-on: ubuntu-latest
63+
outputs:
64+
preview_url: ${{ steps.deploy.outputs.preview_url }}
65+
steps:
66+
- name: Generate a token
67+
id: generate_token
68+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
69+
with:
70+
client-id: ${{ secrets.ci_app_id }}
71+
private-key: ${{ secrets.ci_app_key }}
72+
owner: ${{ github.repository_owner }}
73+
# Only the blueprint-library checkout below consumes this token. The website and
74+
# documentation checkouts are public and use the caller's default GITHUB_TOKEN.
75+
repositories: blueprint-library
76+
- name: Check out the website repository
77+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
78+
with:
79+
repository: ${{ inputs.website_repository }}
80+
ref: ${{ inputs.website_ref }}
81+
path: 'website'
82+
# nuxt/lib/docs-sync.mjs prefers a checkout sitting next to the website over cloning
83+
# main, so this is what makes the build render the caller's docs rather than main's.
84+
- name: Check out the documentation source
85+
if: inputs.docs_repository != ''
86+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
87+
with:
88+
repository: ${{ inputs.docs_repository }}
89+
ref: ${{ inputs.docs_ref }}
90+
path: 'flowfuse'
91+
- name: Check out FlowFuse/blueprint-library repository (to access the blueprints)
92+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
93+
with:
94+
repository: 'FlowFuse/blueprint-library'
95+
ref: main
96+
path: 'blueprint-library'
97+
token: ${{ steps.generate_token.outputs.token }}
98+
# Only a preview processes images. Every other build sets SKIP_IMAGES, which drops
99+
# nuxt.config.ts's image provider to none, so there is nothing to cache or restore.
100+
- name: Cache image pipeline output
101+
if: inputs.deploy_preview
102+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
103+
with:
104+
# Keyed on the source images, so a run that changes no image hits exactly and
105+
# writes nothing back. These are the two directories netlify.toml's cache plugin
106+
# keeps warm on Netlify's own builds.
107+
key: img-pipeline-${{ hashFiles('website/src/**/*.png', 'website/src/**/*.jpg', 'website/src/**/*.jpeg', 'website/src/**/*.gif', 'website/src/**/*.webp', 'website/src/**/*.svg') }}
108+
restore-keys: img-pipeline-
109+
path: |
110+
website/nuxt/public/img
111+
website/.cache/images
112+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
113+
with:
114+
node-version: 24
115+
cache: 'npm'
116+
cache-dependency-path: './website/package-lock.json'
117+
- run: npm run blueprints
118+
working-directory: 'website'
119+
- name: Install Dependencies
120+
run: npm install
121+
working-directory: 'website'
122+
- name: Run unit tests
123+
if: inputs.run_unit_tests
124+
run: npm test
125+
working-directory: 'website'
126+
# A preview needs real images, and nuxt.config.ts drops the image provider to none
127+
# when SKIP_IMAGES is set. This matches netlify.toml's deploy-preview context.
128+
- name: Build the forge
129+
run: npm run ${{ inputs.deploy_preview && 'build:nuxt' || 'build:nuxt:skip-images' }}
130+
working-directory: 'website'
131+
- name: Check links
132+
uses: untitaker/hyperlink@9375bc4063712ad490d5eb3d54df0b6aade15e54 # 0.3.2
133+
with:
134+
args: website/nuxt/dist/ --check-anchors --sources website/src
135+
- name: Upload an unpublished preview to Netlify
136+
id: deploy
137+
if: inputs.deploy_preview
138+
working-directory: 'website'
139+
env:
140+
NETLIFY_AUTH_TOKEN: ${{ secrets.netlify_auth_token }}
141+
NETLIFY_SITE_ID: ${{ secrets.netlify_site_id }}
142+
NETLIFY_TELEMETRY_DISABLED: '1'
143+
PREVIEW_ALIAS: ${{ inputs.preview_alias }}
144+
run: |
145+
if [ -z "${NETLIFY_AUTH_TOKEN}" ] || [ -z "${NETLIFY_SITE_ID}" ]; then
146+
echo "deploy_preview is set but netlify_auth_token or netlify_site_id was not passed." >&2
147+
exit 1
148+
fi
149+
# Deliberately no --prod and no --build. netlify.toml supplies the publish and
150+
# functions directories, and skipping the build means its build command, which
151+
# reindexes Algolia, never runs against a preview.
152+
npx --yes netlify-cli@27.1.0 deploy \
153+
--alias "${PREVIEW_ALIAS}" \
154+
--message "Preview ${PREVIEW_ALIAS}. Do not publish: built outside the production context." \
155+
--json > deploy.json
156+
preview_url="$(jq -r '.deploy_url // empty' deploy.json)"
157+
production_url="$(jq -r '.url // empty' deploy.json)"
158+
if [ -z "${preview_url}" ] || [ "${preview_url}" = "${production_url}" ]; then
159+
echo "Netlify reported the production URL; refusing to publish it as a preview." >&2
160+
exit 1
161+
fi
162+
echo "preview_url=${preview_url}" >> "$GITHUB_OUTPUT"
163+
- name: Record the preview as a deployment
164+
if: inputs.deploy_preview
165+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
166+
env:
167+
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
168+
with:
169+
script: |
170+
const { owner, repo } = context.repo
171+
const deployment = await github.rest.repos.createDeployment({
172+
owner,
173+
repo,
174+
ref: context.payload.pull_request?.head.sha || context.sha,
175+
environment: 'Preview',
176+
description: 'Website preview of this pull request',
177+
auto_merge: false,
178+
required_contexts: [],
179+
transient_environment: true,
180+
production_environment: false
181+
})
182+
await github.rest.repos.createDeploymentStatus({
183+
owner,
184+
repo,
185+
deployment_id: deployment.data.id,
186+
state: 'success',
187+
environment_url: process.env.PREVIEW_URL,
188+
description: 'Uploaded to Netlify',
189+
auto_inactive: true
190+
})
191+
- name: Comment the preview URL on the pull request
192+
if: inputs.deploy_preview && inputs.pr_number > 0
193+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
194+
env:
195+
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
196+
PR_NUMBER: ${{ inputs.pr_number }}
197+
WEBSITE_SOURCE: ${{ inputs.website_repository }}@${{ inputs.website_ref }}
198+
with:
199+
script: |
200+
// One comment per pull request, rewritten in place, so a busy branch does not
201+
// accumulate a comment per push.
202+
const marker = '<!-- website-preview -->'
203+
const sha = context.payload.pull_request?.head.sha || context.sha
204+
const body = [
205+
marker,
206+
`Website preview: ${process.env.PREVIEW_URL}`,
207+
'',
208+
`Built from ${sha} against ${process.env.WEBSITE_SOURCE}.`,
209+
'This is a preview build, so scheduled blog posts are visible and the deploy is never published.'
210+
].join('\n')
211+
const { owner, repo } = context.repo
212+
const issue_number = Number(process.env.PR_NUMBER)
213+
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number })
214+
const previous = comments.find(comment => comment.body?.includes(marker))
215+
if (previous) {
216+
await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body })
217+
} else {
218+
await github.rest.issues.createComment({ owner, repo, issue_number, body })
219+
}

.github/workflows/release.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jobs:
3434
- { name: update-nr-flows, path: actions/update-nr-flows }
3535
- { name: project-automation, path: .github/workflows/project-automation.yaml }
3636
- { name: recurring_sprint_issue, path: .github/workflows/recurring_sprint_issue.yml }
37+
- { name: build_website, path: .github/workflows/build_website.yml }
3738
steps:
3839
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
3940
with:

0 commit comments

Comments
 (0)