feat: Migrate deploy to Bunny and revamp CI #162
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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, | |
| }); | |
| } |