Skip to content

feat: Migrate deploy to Bunny and revamp CI #162

feat: Migrate deploy to Bunny and revamp CI

feat: Migrate deploy to Bunny and revamp CI #162

Workflow file for this run

# Copyright (C) 2026 Sten Tijhuis
# SPDX-License-Identifier: MIT
name: PR checks
# The gating checks -- the Hugo build, the link check, markdownlint, the EN/NL
# parity check, the AVIF check, the Python scan -- all live in quality.yml, which
# runs on push and pull_request alike. This workflow is only the two things that
# need write access to the pull request itself and have no home in a
# push-triggered run: the friendly "you forgot to convert an image" comment and
# keeping the checklist in the PR body ticked.
#
# It re-runs a couple of cheap checks itself (a regex, a find, a file loop)
# rather than reading quality.yml's results, so the two workflows stay
# independent.
on:
pull_request:
types: [opened, edited, synchronize, reopened]
branches: [main, development]
concurrency:
group: pr-checks-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions: {}
jobs:
pr-checks:
name: PR comment and checklist
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The same cheap checks quality.yml gates on, re-run here only to drive the
# comment and the checklist below.
- name: Re-derive the cheap checks
id: checks
run: |
# AVIF: any raster image that is not AVIF.
{
echo "images<<EOF"
find src/static/images -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort
echo "EOF"
} >> "$GITHUB_OUTPUT"
non_avif=$(find src/static/images -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | wc -l)
[ "$non_avif" -eq 0 ] && echo "images_ok=true" >> "$GITHUB_OUTPUT" || echo "images_ok=false" >> "$GITHUB_OUTPUT"
# EN/NL parity: every docs/*.md has a matching *.nl.md.
bilingual_ok=true
for en in src/content/docs/*.md; do
[[ "$en" == *.nl.md ]] && continue
[ -f "${en%.md}.nl.md" ] || bilingual_ok=false
done
echo "bilingual_ok=$bilingual_ok" >> "$GITHUB_OUTPUT"
- name: Comment on non-AVIF images
if: ${{ steps.checks.outputs.images_ok == 'false' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
FILES: ${{ steps.checks.outputs.images }}
ACTOR: ${{ github.event.pull_request.user.login }}
with:
script: |
const files = process.env.FILES.trim().split('\n').map(f => `- \`${f}\``).join('\n');
const actor = process.env.ACTOR;
const body = [
`Hey @${actor}, looks like you forgot something!`,
'',
'The following images in `src/static/images/` are not in AVIF format:',
files,
'',
'Please convert them before merging. Install `avifenc` first:',
'```bash',
'sudo pacman -S libavif',
'```',
'',
'Then batch-convert all images in `static/images/`:',
'```bash',
'cd src/static/images',
'for f in *.png *.jpg *.jpeg; do',
' [ -f "$f" ] && avifenc -q 80 -s 6 "$f" "${f%.*}.avif" && rm "$f"',
'done',
'```',
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body,
});
# Ticks the boxes this workflow can verify cheaply: the title convention,
# EN/NL parity and the AVIF rule. "No broken image references" and "Tested
# locally" are left as the author set them -- quality.yml is what actually
# gates those.
- name: Update PR checklist
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
RESULT_BILINGUAL: ${{ steps.checks.outputs.bilingual_ok }}
RESULT_IMAGES: ${{ steps.checks.outputs.images_ok }}
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
let body = pr.body || '';
if (!body.trim()) return;
const setCheck = (keyword, passed) => {
body = body.replace(
new RegExp(`- \\[[ xX]\\] (.*${keyword}.*)`, 'i'),
`- [${passed ? 'x' : ' '}] $1`
);
};
// The same type list as pr-title.yml and CONTRIBUTING.md. A scope
// and a `!` for a breaking change are allowed: feat(nav)!: ...
const TITLE_RE =
/^(feat|fix|content|docs|chore|refactor|style|revert)(\([^)]+\))?!?: .+/;
setCheck('PR title follows', TITLE_RE.test(pr.title));
setCheck('Both EN and NL', process.env.RESULT_BILINGUAL === 'true');
setCheck('Media is in AVIF', process.env.RESULT_IMAGES === 'true');
// Drop the type lines the author did not pick, but only once one is
// picked. Without that guard the first run strips all eight lines
// before the author has ticked any.
const TYPE_LINE = /^- \[([ xX])\] `\w+` —[^\n]*\n?/gm;
const ticked = [...body.matchAll(TYPE_LINE)]
.some(m => m[1].toLowerCase() === 'x');
if (ticked) {
body = body.replace(/^- \[ \] `\w+` —[^\n]*\n?/gm, '');
}
body = body.replace(/\n{3,}/g, '\n\n');
if (body !== pr.body) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
body,
});
}