diff --git a/.github/actions/setup-hugo/action.yml b/.github/actions/setup-hugo/action.yml new file mode 100644 index 0000000..f52d0bf --- /dev/null +++ b/.github/actions/setup-hugo/action.yml @@ -0,0 +1,60 @@ +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +name: Set up Hugo +description: > + Downloads a pinned Hugo extended release, verifies its checksum and puts it + on PATH. The version and the checksum live here and nowhere else, so the + build in quality.yml can never drift away from the build in deploy-bunny.yml. + + Extended rather than plain: this site has no SCSS today and does not need it, + but every other Hugo site in the organisation runs extended, and extended is + a superset. One flavour across the organisation is worth more than the few + megabytes saved by running a build that behaves subtly differently here. + +inputs: + version: + description: Hugo version to install, without the leading "v". + required: false + # renovate: datasource=github-releases depName=gohugoio/hugo + default: "0.165.0" + sha256: + description: > + SHA-256 of hugo_extended__linux-amd64.tar.gz. Bump this together + with the version; the value is the matching line in the release's + hugo__checksums.txt: + + curl -sSL https://github.com/gohugoio/hugo/releases/download/v/hugo__checksums.txt \ + | grep hugo_extended__linux-amd64.tar.gz + + Mind the hugo_extended_ prefix: the plain hugo_ line is a different + archive with a different checksum. + + You should not normally have to touch this by hand. Renovate cannot + compute a checksum, so update-checksums.yml recalculates it on Renovate's + pull requests and commits it back. Doing it manually is only needed when + the version is changed outside that flow. + required: false + default: "f43494894cdf4a8630a201d5c828051c77f523cc66bb3938b30806835470ac20" + +runs: + using: composite + steps: + # The release comes off the network, so nothing is executed before the + # checksum says it is the archive we pinned. + # + # --retry: the release CDN hands out an occasional 503, and without this a + # single one fails the whole build. curl retries 5xx and timeouts on its + # own; --retry-all-errors extends that to connection failures. + - name: Download and verify Hugo + shell: bash + env: + HUGO_VERSION: ${{ inputs.version }} + HUGO_SHA256: ${{ inputs.sha256 }} + run: | + curl -sSL --fail-with-body -o "${RUNNER_TEMP}/hugo.tar.gz" \ + --retry 5 --retry-delay 3 --retry-all-errors \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" + echo "${HUGO_SHA256} ${RUNNER_TEMP}/hugo.tar.gz" | sha256sum -c - + tar -xzf "${RUNNER_TEMP}/hugo.tar.gz" -C "${RUNNER_TEMP}" hugo + sudo install -m 0755 "${RUNNER_TEMP}/hugo" /usr/local/bin/hugo + hugo version diff --git a/.github/scripts/update-tool-checksums.sh b/.github/scripts/update-tool-checksums.sh index 9b4f15e..89de944 100755 --- a/.github/scripts/update-tool-checksums.sh +++ b/.github/scripts/update-tool-checksums.sh @@ -66,8 +66,8 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" readonly REPO_ROOT cd "$REPO_ROOT" -readonly CONFIG_VALIDATION=".github/workflows/config-validation.yml" -readonly PR_CHECKS=".github/workflows/pr-checks.yml" +readonly HUGO_ACTION=".github/actions/setup-hugo/action.yml" +readonly QUALITY=".github/workflows/quality.yml" # ── Reading and writing the pinned values ─────────────────────────────────── @@ -81,6 +81,18 @@ Set-KeyValue() { sed -i "s|^\([[:space:]]*$2:[[:space:]]*\"\)[^\"]*\"|\1$3\"|" "$1" } +# Hugo's version and checksum are input defaults in the composite action, so +# there is no key to match on. The version is the `default:` directly under the +# renovate annotation; the checksum is the only `default:` holding 64 hex +# characters. +Get-HugoVersion() { + grep -A1 'depName=gohugoio/hugo' "$HUGO_ACTION" | sed -n 's/.*default: "\([^"]*\)".*/\1/p' | head -n1 +} + +Set-HugoSha() { + sed -i "s|^\([[:space:]]*default: \"\)[a-f0-9]\{64\}\"|\1$1\"|" "$HUGO_ACTION" +} + # ── Fetching and verifying ────────────────────────────────────────────────── TEMP_DIR="$(mktemp -d)" @@ -117,20 +129,27 @@ Get-PublishedHash() { # ── The tools ─────────────────────────────────────────────────────────────── -ACTIONLINT_VERSION="$(Get-KeyValue "$CONFIG_VALIDATION" ACTIONLINT_VERSION)" -LYCHEE_VERSION="$(Get-KeyValue "$PR_CHECKS" LYCHEE_VERSION)" +HUGO_VERSION="$(Get-HugoVersion)" +ACTIONLINT_VERSION="$(Get-KeyValue "$QUALITY" ACTIONLINT_VERSION)" +LYCHEE_VERSION="$(Get-KeyValue "$QUALITY" LYCHEE_VERSION)" -for pair in "actionlint:$ACTIONLINT_VERSION" "lychee:$LYCHEE_VERSION"; do +for pair in "Hugo:$HUGO_VERSION" "actionlint:$ACTIONLINT_VERSION" "lychee:$LYCHEE_VERSION"; do [[ -n "${pair#*:}" ]] || Stop-Script "Could not read the ${pair%%:*} version. Did the file layout change?" done Write-Log INFO "Versions found in the repository:" +echo " Hugo: $HUGO_VERSION" echo " actionlint: $ACTIONLINT_VERSION" echo " lychee: $LYCHEE_VERSION" echo Write-Log INFO "Downloading and verifying against the published checksums..." +HUGO_SHA256="$(Get-VerifiedHash "hugo.tar.gz" \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" \ + "$(Get-PublishedHash "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_checksums.txt" "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz")")" +Write-Log SUCCESS "Hugo: $HUGO_SHA256" + ACTIONLINT_SHA256="$(Get-VerifiedHash "actionlint.tar.gz" \ "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \ "$(Get-PublishedHash "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_checksums.txt" "linux_amd64.tar.gz")")" @@ -150,9 +169,10 @@ if [[ "$APPLY" != true ]]; then fi fi -Set-KeyValue "$CONFIG_VALIDATION" ACTIONLINT_SHA256 "$ACTIONLINT_SHA256" -Set-KeyValue "$PR_CHECKS" LYCHEE_SHA256 "$LYCHEE_SHA256" +Set-HugoSha "$HUGO_SHA256" +Set-KeyValue "$QUALITY" ACTIONLINT_SHA256 "$ACTIONLINT_SHA256" +Set-KeyValue "$QUALITY" LYCHEE_SHA256 "$LYCHEE_SHA256" Write-Log SUCCESS "Updated:" -echo " - $CONFIG_VALIDATION" -echo " - $PR_CHECKS" +echo " - $HUGO_ACTION" +echo " - $QUALITY" diff --git a/.github/workflows/config-validation.yml b/.github/workflows/config-validation.yml index a38c79c..8716fd4 100644 --- a/.github/workflows/config-validation.yml +++ b/.github/workflows/config-validation.yml @@ -15,9 +15,7 @@ on: - '.github/dependabot.yml' - '.github/dependabot.yaml' - '.github/scripts/check-renovate-patterns.py' - # Broader than the other repos: the actionlint job below covers every - # workflow, so every workflow change is relevant here. - - '.github/workflows/**' + - '.github/workflows/config-validation.yml' pull_request: branches: [main, development] paths: @@ -26,9 +24,7 @@ on: - '.github/dependabot.yml' - '.github/dependabot.yaml' - '.github/scripts/check-renovate-patterns.py' - # Broader than the other repos: the actionlint job below covers every - # workflow, so every workflow change is relevant here. - - '.github/workflows/**' + - '.github/workflows/config-validation.yml' workflow_dispatch: permissions: {} @@ -95,32 +91,3 @@ jobs: fi pipx install "check-jsonschema==${CHECK_JSONSCHEMA_VERSION}" check-jsonschema --builtin-schema vendor.dependabot "$config" - - # De workflowbestanden zijn ook config. De andere repositories draaien - # actionlint vanuit hun quality-workflow; deze had geen equivalent, dus - # het hoort hier. - # - # Als stap en niet als eigen job: GitHub rekent per job en rondt naar - # boven af op een hele minuut. actionlint is in vijf seconden klaar en - # heeft dezelfde checkout nodig als de stappen hierboven, dus een eigen - # job kostte een volle minuut extra voor niets. - # - # Vanaf hier draait elke stap op !cancelled(), zodat één rode controle de - # andere niet verbergt. De job faalt alsnog zodra er iets fout is. - - name: Install actionlint - if: ${{ !cancelled() }} - env: - # renovate: datasource=github-releases depName=rhysd/actionlint - ACTIONLINT_VERSION: "1.7.12" - ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" - run: | - curl -sSL --fail-with-body -o actionlint.tar.gz \ - --retry 5 --retry-delay 3 --retry-all-errors \ - "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" - echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum -c - - tar -xzf actionlint.tar.gz actionlint - sudo install -m 0755 actionlint /usr/local/bin/actionlint - - - name: Run actionlint - if: ${{ !cancelled() }} - run: actionlint -color diff --git a/.github/workflows/deploy-bunny.yml b/.github/workflows/deploy-bunny.yml new file mode 100644 index 0000000..a98d076 --- /dev/null +++ b/.github/workflows/deploy-bunny.yml @@ -0,0 +1,93 @@ +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +# +# The deploy: build, sync to Bunny Storage, purge the Pull Zone cache. +# +# This replaced a GitHub Pages deploy. The site now lives in a Bunny Storage +# zone and is served from the edge by a Pull Zone, the same as every other Hugo +# site in the organisation. +name: Deploy to Bunny.net + +on: + push: + branches: + - main + paths: + - 'src/**' + - '.github/workflows/deploy-bunny.yml' + - '.github/actions/setup-hugo/**' + workflow_dispatch: + +concurrency: + group: deploy + cancel-in-progress: true + +# No token needed; the job that reads the checkout asks for read access itself. +permissions: {} + +jobs: + deploy: + name: Build and deploy to Bunny Storage + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # This site pulls Hextra in as a Hugo Module (src/go.mod), so Hugo needs + # Go on PATH before it can build. The other repos in the organisation are + # not module sites and skip this step. + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: src/go.mod + + # Pinned by version and checksum inside the action, so this build and the + # one in quality.yml always use the same Hugo. + - name: Set up Hugo + uses: ./.github/actions/setup-hugo + + # No --baseURL: it lives in src/hugo.toml and belongs on one line only, so + # a domain change happens in one place. HUGO_ENVIRONMENT=production is what + # gives the live site the permissive robots.txt; see src/layouts/robots.txt. + - name: Build site + working-directory: src + env: + TZ: Europe/Amsterdam + HUGO_ENVIRONMENT: production + run: hugo --minify --gc + + # An empty public/ would let the --delete below wipe the whole zone. That + # can only happen if the build is broken, and then a failed deploy is far + # better than an offline site. + - name: Check that the build produced a site + run: test -s src/public/index.html + + - name: Sync to Bunny Storage (S3) + env: + AWS_ACCESS_KEY_ID: ${{ secrets.BUNNY_STORAGE_ZONE }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.BUNNY_ACCESS_KEY }} + AWS_DEFAULT_REGION: de + STORAGE_ZONE: ${{ secrets.BUNNY_STORAGE_ZONE }} + STORAGE_ENDPOINT: ${{ secrets.BUNNY_STORAGE_ENDPOINT }} + run: | + aws s3 sync src/public/ "s3://${STORAGE_ZONE}/" \ + --endpoint-url "${STORAGE_ENDPOINT}" \ + --delete \ + --no-progress + + - name: Wait for storage replication + run: sleep 15 + + - name: Purge Bunny Pull Zone cache + env: + PULL_ZONE_ID: ${{ secrets.BUNNY_PULL_ZONE_ID }} + API_KEY: ${{ secrets.BUNNY_API_KEY }} + run: | + curl -sS --fail-with-body -X POST \ + "https://api.bunny.net/pullzone/${PULL_ZONE_ID}/purgeCache" \ + -H "AccessKey: ${API_KEY}" \ + -H "Content-Type: application/json" diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml deleted file mode 100644 index f96d001..0000000 --- a/.github/workflows/hugo.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Deploy Hugo site to Pages - -on: - push: - branches: ["main"] - workflow_dispatch: - -# Nothing by default. pages: write and id-token: write belong to the deploy -# job alone; at the top they were also handed to the build job, which only -# needs to read the checkout. -permissions: {} - -concurrency: - group: "pages" - cancel-in-progress: false - -defaults: - run: - shell: bash - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - env: - HUGO_VERSION: 0.165.0 - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Setup Go - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: src/go.mod - - - name: Install Hugo - run: | - wget -O "${{ runner.temp }}/hugo.deb" \ - "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb" \ - && sudo dpkg -i "${{ runner.temp }}/hugo.deb" - - - name: Setup Pages - id: pages - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - - - name: Build with Hugo - env: - HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache - HUGO_ENVIRONMENT: production - TZ: Europe/Amsterdam - # Through env rather than straight into the script: an expression - # interpolated into run: is expanded before bash ever sees it. - BASE_URL: ${{ steps.pages.outputs.base_url }} - run: | - cd src && hugo \ - --gc \ - --minify \ - --baseURL "${BASE_URL}/" - - - name: Upload artifact - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 - with: - path: ./src/public - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - permissions: - pages: write - id-token: write - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 9be2d63..d65dc77 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -1,11 +1,23 @@ -name: PR Checks +# 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] -# Snel achter elkaar naar dezelfde pull request pushen startte evenveel volledige -# runs, en de eerste zijn dan al achterhaald. concurrency: group: pr-checks-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -13,94 +25,44 @@ concurrency: permissions: {} jobs: - # Alle controles op een pull request, in een job. - # - # Dit waren er zes: markdownlint, de EN/NL-pariteit, de AVIF-controle, de - # Hugo-build, de linkcheck en het bijwerken van de checklist. Ze duurden 3, 4, - # 3, 12, 6 en 6 seconden -- vierendertig seconden werk, verdeeld over zes - # runners. GitHub rekent per job en rondt elke job naar boven af op een hele - # minuut, dus dat waren zes gefactureerde minuten. - # - # Er verdwijnt meer dan die vijf minuten. De linkcheck kreeg de gebouwde site - # via een artefact aangeleverd, met een upload, een download en de opslag - # erbij; nu leest hij gewoon de map die de build ernaast heeft neergezet. - # - # Elke stap draait op !cancelled(), zodat een rode markdownlint de Hugo-build - # niet verbergt. Je wilt alle fouten in een run zien, niet de tweede pas nadat - # je de eerste hebt opgelost. De job faalt alsnog zodra er iets fout is. - # - # De job draagt `pull-requests: write` omdat twee stappen op de pull request - # zelf schrijven: de AVIF-controle plaatst een comment en de laatste stap werkt - # de checklist bij. Dat is de prijs van het samenvoegen; alle actions staan op - # een vastgezette SHA. pr-checks: - name: PR checks + name: PR comment and checklist runs-on: ubuntu-latest permissions: contents: read pull-requests: write - env: - HUGO_VERSION: 0.165.0 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # fetch-depth: 0 voor Hugo's .GitInfo en .Lastmod. - fetch-depth: 0 persist-credentials: false - # ── 1. Markdown-opmaak ────────────────────────────────────────────────── - - name: Markdown lint - id: markdown - if: ${{ !cancelled() }} - uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0 - with: - globs: "src/content/**/*.md" - - # ── 2. Elk Engels document heeft een Nederlandse tegenhanger ──────────── - - name: Check every .md has a matching .nl.md - id: bilingual - if: ${{ !cancelled() }} - run: | - missing="" - for en in src/content/docs/*.md; do - base="${en%.md}" - nl="${base}.nl.md" - # Bestanden die zelf al .nl.md zijn overslaan - [[ "$en" == *.nl.md ]] && continue - if [ ! -f "$nl" ]; then - missing="$missing\n $en → $nl missing" - fi - done - if [ -n "$missing" ]; then - echo -e "::error::Missing Dutch translation(s):$missing" - exit 1 - fi - echo "All docs have EN + NL versions." - - # ── 3. Afbeeldingen moeten AVIF zijn ──────────────────────────────────── - - name: Find non-AVIF images - id: images - if: ${{ !cancelled() }} + # 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 "files<> "$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" - count=$(find src/static/images -type f \( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" \) | wc -l) - if [ "$count" -gt 0 ]; then - echo "found=true" >> "$GITHUB_OUTPUT" - else - echo "found=false" >> "$GITHUB_OUTPUT" - echo "All images are AVIF." - fi + # 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: Post PR comment - if: ${{ !cancelled() && steps.images.outputs.found == 'true' }} + - name: Comment on non-AVIF images + if: ${{ steps.checks.outputs.images_ok == 'false' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - FILES: ${{ steps.images.outputs.files }} + FILES: ${{ steps.checks.outputs.images }} ACTOR: ${{ github.event.pull_request.user.login }} with: script: | @@ -133,130 +95,15 @@ jobs: body, }); - - name: Annotate and fail - if: ${{ !cancelled() && steps.images.outputs.found == 'true' }} - env: - FILES: ${{ steps.images.outputs.files }} - run: | - while IFS= read -r f; do - echo "::error file=$f::Convert to AVIF before merging (see README → Image assets)" - done <<< "$FILES" - exit 1 - - # ── 4. Python: stijl en beveiliging ───────────────────────────────────── - # - # Stond in python-checks.yml als eigen job, die op elke pull request - # draaide zonder padfilter -- een volle gefactureerde minuut, ook op een - # pull request die alleen content aanraakte. Als stap hier kost hij geen - # extra job. python-checks.yml houdt zijn wekelijkse run en zijn run op - # main: daar zit de waarde van een herhaalde bandit-scan, want die vindt - # met nieuwe regels iets in code die niet veranderd is. - - name: Set up Python - if: ${{ !cancelled() }} - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.14' - - - name: Install flake8 and bandit - if: ${{ !cancelled() }} - run: | - python -m pip install --upgrade pip - pip install flake8 bandit - - - name: Lint with flake8 - if: ${{ !cancelled() }} - run: flake8 src/static/scripts/ --max-line-length=120 - - - name: Security scan with bandit - if: ${{ !cancelled() }} - run: bandit -r src/static/scripts/ -ll - - # ── 5. Hugo bouwt zonder fouten ───────────────────────────────────────── - - name: Setup Go - if: ${{ !cancelled() }} - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: src/go.mod - - - name: Install Hugo - if: ${{ !cancelled() }} - run: | - wget -O "${{ runner.temp }}/hugo.deb" \ - "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb" \ - && sudo dpkg -i "${{ runner.temp }}/hugo.deb" - - - name: Build - id: hugo - if: ${{ !cancelled() }} - env: - HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache - HUGO_ENVIRONMENT: production - TZ: Europe/Amsterdam - run: cd src && hugo --gc --minify --baseURL "http://localhost/" - - # ── 6. Kapotte interne links ──────────────────────────────────────────── - # - # Met de hand geïnstalleerd in plaats van via lycheeverse/lychee-action. - # Die actie haalt zijn binary op met een kale `curl -sfLO`: geen retry, en - # geen controle op wat er terugkomt. Deze stap en een in THectic.nl - # faalden binnen een kwartier allebei op die download toen GitHubs - # release-CDN een slechte dag had, zonder ook maar een link te hebben - # gecontroleerd. Vastgezette versie, geverifieerde checksum, retry. - - name: Install lychee - if: ${{ !cancelled() }} - env: - # extractVersion: lychee tagt zijn releases als "lychee-v0.24.2" en - # niet als "v0.24.2", dus het standaardpatroon leest de versie er niet - # uit. - # renovate: datasource=github-releases depName=lycheeverse/lychee extractVersion=^lychee-v(?.+)$ - LYCHEE_VERSION: "0.24.2" - # Uit de lychee-x86_64-unknown-linux-gnu.tar.gz.sha256 van de release zelf - LYCHEE_SHA256: "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a" - run: | - curl -sSL --fail-with-body -o lychee.tar.gz \ - --retry 5 --retry-delay 3 --retry-all-errors \ - "https://github.com/lycheeverse/lychee/releases/download/lychee-v${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz" - echo "${LYCHEE_SHA256} lychee.tar.gz" | sha256sum -c - - tar -xzf lychee.tar.gz lychee-x86_64-unknown-linux-gnu/lychee - sudo install -m 0755 lychee-x86_64-unknown-linux-gnu/lychee /usr/local/bin/lychee - lychee --version - - # Leest src/public rechtstreeks. Dat ging via een artefact omdat de - # linkcheck een eigen runner was; nu staat de build ernaast. - # - # --index-files: zonder die vlag ziet lychee een link naar - # /docs/applications/ als een link naar een map en stopt hij daar, dus kan - # hij nooit naar binnen kijken voor het #fragment. Elke anker-link naar een - # andere pagina meldt dan "Cannot find fragment" terwijl de kop er gewoon - # staat. Hugo levert elke pagina uit als /index.html, dus deze vlag - # is wat --include-fragments hier bruikbaar maakt. - # - # De glob staat bewust tussen quotes. Zonder quotes expandeert bash hem - # eerst, en zonder globstar klapt ** dan in tot een mapniveau -- daardoor - # controleerde deze stap ooit 95 links in plaats van 3379. - - name: Check internal links - id: links - if: ${{ !cancelled() }} - run: | - lychee --offline --include-fragments --index-files index.html \ - --root-dir "${GITHUB_WORKSPACE}/src/public" "src/public/**/*.html" - - # ── 7. Checklist in de omschrijving bijwerken ─────────────────────────── - # - # Leest de uitkomst van de stappen hierboven in plaats van van losse jobs. - # Dat was hiervoor `needs: [...]` met vier jobresultaten; in een job is het - # steps..outcome, en dat scheelt de zesde runner. - # - # Draait op !cancelled() en niet op success(), want juist bij een rode - # controle wil je de checklist bijgewerkt zien. + # 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 - if: ${{ !cancelled() }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - RESULT_BILINGUAL: ${{ steps.bilingual.outcome }} - RESULT_IMAGES: ${{ steps.images.outputs.found }} - RESULT_HUGO: ${{ steps.hugo.outcome }} - RESULT_LINKS: ${{ steps.links.outcome }} + RESULT_BILINGUAL: ${{ steps.checks.outputs.bilingual_ok }} + RESULT_IMAGES: ${{ steps.checks.outputs.images_ok }} with: script: | const { data: pr } = await github.rest.pulls.get({ @@ -275,22 +122,18 @@ jobs: ); }; - // Dezelfde typelijst als pr-title.yml en CONTRIBUTING.md. Een scope - // en een `!` voor een breaking change zijn toegestaan: feat(nav)!: ... + // 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 === 'success'); - // De AVIF-stap slaagt ook als hij bestanden vindt; het oordeel zit - // in zijn output, niet in zijn uitkomst. - setCheck('Media is in AVIF', process.env.RESULT_IMAGES === 'false'); - setCheck('No broken image', process.env.RESULT_LINKS === 'success'); - setCheck('Tested locally', process.env.RESULT_HUGO === 'success'); + setCheck('Both EN and NL', process.env.RESULT_BILINGUAL === 'true'); + setCheck('Media is in AVIF', process.env.RESULT_IMAGES === 'true'); - // De niet-gekozen types weghalen, maar alleen als er al een gekozen - // is. Zonder die voorwaarde stript de eerste run alle acht regels weg - // voordat de auteur er een heeft aangevinkt. + // 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'); diff --git a/.github/workflows/python-checks.yml b/.github/workflows/python-checks.yml deleted file mode 100644 index 7edfc5c..0000000 --- a/.github/workflows/python-checks.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Python Checks - -# Geen pull_request meer: flake8 en bandit draaien daar als stap in -# pr-checks.yml, waar ze geen eigen gefactureerde job kosten. Hier bleven de -# wekelijkse run en de run op main staan, en daar zit de waarde van een -# herhaalde scan: bandit vindt met nieuwe regels iets in code die zelf niet -# veranderd is, en dat merk je nooit als hij alleen op gewijzigde code draait. -on: - push: - branches: [ main, development ] - schedule: - - cron: '0 5 * * 0' - workflow_dispatch: - -# Zonder dit erft de workflow wat de repository-default ook is. -permissions: {} - -jobs: - lint: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: '3.14' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 bandit - - - name: Lint with flake8 - run: flake8 src/static/scripts/ --max-line-length=120 - - - name: Security scan with bandit - run: bandit -r src/static/scripts/ -ll diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..d16d791 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,228 @@ +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +name: Code quality + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + +# No token needed; jobs that do ask for one explicitly. +permissions: {} + +# Pushing to the same PR three times in a row started three full runs, and the +# first two are already stale by then. On main do not cancel: there the run is +# the record that the commit passed the checks. +concurrency: + group: quality-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + # Everything that needs the built site, in one job. + # + # Splitting the build, the translation check and the link check into separate + # jobs costs a runner and a Hugo setup each, and GitHub rounds every job up to + # a whole minute. Together they finish inside one. + site: + name: Build and check the site + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # fetch-depth: 0 for Hugo's .GitInfo and .Lastmod (enableGitInfo). + fetch-depth: 0 + persist-credentials: false + + # Hextra is a Hugo Module (src/go.mod), so Hugo needs Go on PATH. + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: src/go.mod + + - name: Set up Hugo + uses: ./.github/actions/setup-hugo + + # Every step runs on !cancelled(), so a red markdown check does not hide + # the Hugo build. You want every failure in one run, not the second only + # after fixing the first. The job still fails as soon as anything does. + + # A build that fails here is a broken deploy caught in time. --panicOnWarning + # is deliberately stricter than the deploy build: a new Hugo deprecation + # should block a merge, not a release that is already on its way. + - name: Build site + if: ${{ !cancelled() }} + working-directory: src + env: + HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache + HUGO_ENVIRONMENT: production + TZ: Europe/Amsterdam + run: hugo --gc --minify --printPathWarnings --panicOnWarning --baseURL "http://localhost/" + + # Every English document must have a Dutch counterpart. + - name: Check every .md has a matching .nl.md + if: ${{ !cancelled() }} + run: | + missing="" + for en in src/content/docs/*.md; do + base="${en%.md}" + nl="${base}.nl.md" + # Skip files that are already .nl.md + [[ "$en" == *.nl.md ]] && continue + if [ ! -f "$nl" ]; then + missing="$missing\n $en → $nl missing" + fi + done + if [ -n "$missing" ]; then + echo -e "::error::Missing Dutch translation(s):$missing" + exit 1 + fi + echo "All docs have EN + NL versions." + + # Images must be AVIF. The friendly "here is how to convert" comment lives + # in pr-checks.yml, which needs pull-requests: write; this is the hard + # gate, and it runs on push too. + - name: Check images are AVIF + if: ${{ !cancelled() }} + run: | + mapfile -t offenders < <(find src/static/images -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) | sort) + if [ "${#offenders[@]}" -gt 0 ]; then + for f in "${offenders[@]}"; do + echo "::error file=$f::Convert to AVIF before merging (see README → Image assets)" + done + exit 1 + fi + echo "All images are AVIF." + + # Installed by hand rather than through lycheeverse/lychee-action, which + # fetches its binary with a bare `curl -sfLO`: no retry, and no check on + # what comes back. Pinned version, verified checksum, retry. + - name: Install lychee + if: ${{ !cancelled() }} + env: + # extractVersion: lychee tags its releases as "lychee-v0.24.2", not + # "v0.24.2", so the default pattern does not read the version out. + # renovate: datasource=github-releases depName=lycheeverse/lychee extractVersion=^lychee-v(?.+)$ + LYCHEE_VERSION: "0.24.2" + # From the release's own lychee-x86_64-unknown-linux-gnu.tar.gz.sha256 + LYCHEE_SHA256: "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a" + run: | + curl -sSL --fail-with-body -o lychee.tar.gz \ + --retry 5 --retry-delay 3 --retry-all-errors \ + "https://github.com/lycheeverse/lychee/releases/download/lychee-v${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz" + echo "${LYCHEE_SHA256} lychee.tar.gz" | sha256sum -c - + tar -xzf lychee.tar.gz lychee-x86_64-unknown-linux-gnu/lychee + sudo install -m 0755 lychee-x86_64-unknown-linux-gnu/lychee /usr/local/bin/lychee + lychee --version + + # Reads src/public directly. --index-files: Hugo serves every page as + # /index.html, and without this lychee stops at the directory, so a + # link to /docs/applications/ can never be checked for its #fragment. The + # glob is quoted on purpose: unquoted, bash expands it first and ** without + # globstar collapses to a single directory level. + - name: Check internal links + if: ${{ !cancelled() }} + run: | + lychee --offline --include-fragments --index-files index.html \ + --root-dir "${GITHUB_WORKSPACE}/src/public" "src/public/**/*.html" + + # Everything that has nothing to do with the built site: the Markdown, the + # workflow files, and the Python. Hugo is not needed for any of it. + # + # Every step runs on !cancelled(), so one red linter does not hide the others. + # The job still fails as soon as anything does. + repo: + name: Check the repository + runs-on: ubuntu-latest + permissions: + contents: read + # For zizmor's SARIF upload below. + security-events: write + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # markdownlint: keeps content/ and the repo's own Markdown consistent. + - name: Run markdownlint + if: ${{ !cancelled() }} + uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0 + with: + globs: | + src/content/**/*.md + *.md + + # Pinned release plus checksum, rather than piping a script off a branch + # straight through bash. + - name: Install actionlint + if: ${{ !cancelled() }} + env: + # renovate: datasource=github-releases depName=rhysd/actionlint + ACTIONLINT_VERSION: "1.7.12" + ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" + run: | + curl -sSL --fail-with-body -o actionlint.tar.gz \ + --retry 5 --retry-delay 3 --retry-all-errors \ + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" + echo "${ACTIONLINT_SHA256} actionlint.tar.gz" | sha256sum -c - + tar -xzf actionlint.tar.gz actionlint + sudo install -m 0755 actionlint /usr/local/bin/actionlint + + # actionlint: syntax errors and wrong expressions in the workflows. + - name: Run actionlint + if: ${{ !cancelled() }} + run: actionlint -color + + # zizmor: a linter on the same files as actionlint, only for security + # rather than syntax. pipx and not pip: the runner's system Python is + # externally managed (PEP 668), so a plain pip install aborts. + - name: Install zizmor + if: ${{ !cancelled() }} + env: + # renovate: datasource=pypi depName=zizmor + ZIZMOR_VERSION: "1.29.0" + run: pipx install "zizmor==${ZIZMOR_VERSION}" + + - name: Run zizmor + if: ${{ !cancelled() }} + run: zizmor --format sarif . > zizmor.sarif || true + + # The results show up on the repository's Security tab. Advanced Security + # is on organisation-wide, so this works on a private repository too. + - name: Upload zizmor results to GitHub Security + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + if: ${{ !cancelled() }} + continue-on-error: true + with: + sarif_file: zizmor.sarif + category: zizmor + + # ── Python: style and security ───────────────────────────────────────── + # + # src/static/scripts/ ships one script, saxion-eduroam.py, that visitors + # download and run. flake8 and bandit also get a weekly re-run in + # security.yml, where new rules can flag something in code that has not + # changed. + - name: Set up Python + if: ${{ !cancelled() }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + + - name: Install flake8 and bandit + if: ${{ !cancelled() }} + run: | + python -m pip install --upgrade pip + pip install flake8 bandit + + - name: Lint with flake8 + if: ${{ !cancelled() }} + run: flake8 src/static/scripts/ --max-line-length=120 + + - name: Security scan with bandit + if: ${{ !cancelled() }} + run: bandit -r src/static/scripts/ -ll diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..b9ef82e --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,116 @@ +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +name: Security + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + # Weekly scan, to catch new vulnerabilities in existing code. + schedule: + - cron: '0 5 * * 1' + # So Scorecard can be run on demand; it no longer runs on PRs. + workflow_dispatch: + +# No token needed; jobs that do ask for one explicitly. +permissions: {} + +# Pushing to the same PR three times in a row started three full scans, and the +# first two are already stale by then. On main do not cancel: there the run is +# the record that the commit was scanned. +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + # Semgrep: SAST scanner for the templates, the bit of CSS and the Python + # script the site ships. + semgrep: + name: Semgrep SAST scan + runs-on: ubuntu-latest + permissions: + contents: read + container: + image: semgrep/semgrep@sha256:67319956da3dcb58baf5b322899c15458e3963e7018a86aeeb5cd224e69cb77a + # Skip Renovate PRs: no token available there. + if: github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]' + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Exceptions live in .semgrepignore + - name: Run Semgrep scan + run: semgrep scan --config auto --error src/layouts/ src/assets/ src/static/scripts/ + + # Python: style and security on saxion-eduroam.py. Also a step in quality.yml + # on every push and PR; the value of the weekly re-run is that a newer bandit + # can flag something in a script that has not itself changed. + python-audit: + name: Python style and security + runs-on: ubuntu-latest + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + permissions: + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + + - name: Install flake8 and bandit + run: | + python -m pip install --upgrade pip + pip install flake8 bandit + + - name: Lint with flake8 + run: flake8 src/static/scripts/ --max-line-length=120 + + - name: Security scan with bandit + run: bandit -r src/static/scripts/ -ll + + # OpenSSF Scorecard: rates the repository's security hygiene -- branch + # protection, pinned dependencies, code review. Weekly and on main, not on + # every PR: it rates the repository and not the commit, so running it per PR + # spent a runner on an outcome that was the same anyway. + scorecard: + name: OpenSSF Scorecard + runs-on: ubuntu-latest + if: >- + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && github.ref == 'refs/heads/main') + permissions: + security-events: write + id-token: write + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run Scorecard analysis + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + continue-on-error: true + with: + results_file: scorecard.sarif + results_format: sarif + repo_token: ${{ secrets.GITHUB_TOKEN }} + publish_results: false + + # The results show up on the repository's Security tab. Advanced Security + # is on organisation-wide, so this works on a private repository too. + - name: Upload results to GitHub Security + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + continue-on-error: true + with: + sarif_file: scorecard.sarif + category: scorecard diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml deleted file mode 100644 index 6422c97..0000000 --- a/.github/workflows/trivy-scan.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: "Trivy filesystem scan" - -on: - schedule: - - cron: '0 2 * * 0' - workflow_dispatch: - -permissions: - contents: read - security-events: write - -jobs: - trivy-scan: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Run Trivy filesystem scan - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - with: - scan-type: fs - severity: CRITICAL,HIGH - format: sarif - output: trivy-results.sarif - - - name: Upload Trivy results to GitHub Security tab - uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 - if: always() - with: - sarif_file: trivy-results.sarif diff --git a/.github/workflows/update-checksums.yml b/.github/workflows/update-checksums.yml index 80119e0..fc5a9aa 100644 --- a/.github/workflows/update-checksums.yml +++ b/.github/workflows/update-checksums.yml @@ -16,8 +16,8 @@ on: types: [opened, synchronize, reopened] branches: [main, development] paths: - - '.github/workflows/config-validation.yml' - - '.github/workflows/pr-checks.yml' + - '.github/actions/setup-hugo/action.yml' + - '.github/workflows/quality.yml' - '.github/scripts/update-tool-checksums.sh' permissions: {} @@ -75,7 +75,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add .github/workflows/config-validation.yml .github/workflows/pr-checks.yml + git add .github/actions/setup-hugo/action.yml .github/workflows/quality.yml if git diff --staged --quiet; then echo "Checksums are already up to date, nothing to commit." else diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..d38c9ec --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,3 @@ +# Build output +src/public/ +src/resources/ diff --git a/README.md b/README.md index 1b310dd..c941ed2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ English | [Nederlands](README.nl.md) CachyOS on the ASUS ROG Zephyrus G16 GA605WV (2024). My personal setup log: documenting what worked, what didn't, and how I fixed it. -**Browse the full documentation site: [zephyrus-linux.stensel.nl](https://zephyrus-linux.stensel.nl/)** +**Browse the full documentation site: [zephyrus-linux.thectic.nl](https://zephyrus-linux.thectic.nl/)** ## About this project @@ -55,7 +55,7 @@ cd src hugo --gc --minify ``` -The output is written to `./src/public/`. On push to `main`, GitHub Actions builds and deploys to GitHub Pages automatically. +The output is written to `./src/public/`. On push to `main`, GitHub Actions builds the site and deploys it to Bunny.net (Storage zone + Pull Zone) at [zephyrus-linux.thectic.nl](https://zephyrus-linux.thectic.nl/). ## Image assets diff --git a/README.nl.md b/README.nl.md index 0bac8be..c618386 100644 --- a/README.nl.md +++ b/README.nl.md @@ -4,7 +4,7 @@ Nederlands | [English](README.md) CachyOS op de ASUS ROG Zephyrus G16 GA605WV (2024). Mijn persoonlijke setup-log: wat werkte, wat niet, en hoe ik het heb opgelost. -**Bekijk de volledige documentatiesite: [zephyrus-linux.stensel.nl](https://zephyrus-linux.stensel.nl/nl/)** +**Bekijk de volledige documentatiesite: [zephyrus-linux.thectic.nl](https://zephyrus-linux.thectic.nl/nl/)** ## Over dit project @@ -55,7 +55,7 @@ cd src hugo --gc --minify ``` -De output wordt geschreven naar `./src/public/`. Bij een push naar `main` bouwt GitHub Actions de site automatisch en deployt naar GitHub Pages. +De output wordt geschreven naar `./src/public/`. Bij een push naar `main` bouwt GitHub Actions de site en deployt die naar Bunny.net (Storage-zone + Pull Zone) op [zephyrus-linux.thectic.nl](https://zephyrus-linux.thectic.nl/). ## Afbeeldingen diff --git a/renovate.json b/renovate.json index 1c1dde5..d7e33fd 100644 --- a/renovate.json +++ b/renovate.json @@ -52,7 +52,7 @@ ] }, { - "description": "Versions pinned by hand in the workflows. Not automerged: actionlint is pinned alongside a checksum that has to be updated in the same PR.", + "description": "Hugo, actionlint, lychee and zizmor, pinned by hand in the workflows and the setup-hugo action. Not automerged: Hugo, actionlint and lychee are also pinned by SHA-256, and that checksum has to be updated in the same PR (update-checksums.yml does this on Renovate's branches).", "matchManagers": [ "custom.regex" ], @@ -66,33 +66,10 @@ "customManagers": [ { "customType": "regex", + "description": "Tool versions pinned in workflows and composite actions, annotated with a `# renovate:` comment on the line above", "managerFilePatterns": [ - "/^\\.github/workflows/.*\\.ya?ml$/" - ], - "matchStrings": [ - "HUGO_VERSION:\\s*(?\\d+\\.\\d+\\.\\d+)" - ], - "depNameTemplate": "gohugoio/hugo", - "datasourceTemplate": "github-releases", - "versioningTemplate": "semver" - }, - { - "customType": "regex", - "managerFilePatterns": [ - "/^\\.github/workflows/.*\\.ya?ml$/" - ], - "matchStrings": [ - "hugo-version:\\s*['\"]?(?\\d+\\.\\d+\\.\\d+)['\"]?" - ], - "depNameTemplate": "gohugoio/hugo", - "datasourceTemplate": "github-releases", - "versioningTemplate": "semver" - }, - { - "customType": "regex", - "description": "Tool versions pinned in workflows, annotated with a `# renovate:` comment on the line above", - "managerFilePatterns": [ - "/^\\.github/workflows/.*\\.ya?ml$/" + "/^\\.github/workflows/.*\\.ya?ml$/", + "/^\\.github/actions/.*/action\\.ya?ml$/" ], "matchStrings": [ "# renovate: datasource=(?[a-z-]+) depName=(?\\S+)(?: extractVersion=(?\\S+))?\\s+[A-Za-z_]+: \"(?[^\"]+)\"" diff --git a/src/content/docs/networking/eduroam-network-installation.md b/src/content/docs/networking/eduroam-network-installation.md index 1137eb4..ced4c72 100644 --- a/src/content/docs/networking/eduroam-network-installation.md +++ b/src/content/docs/networking/eduroam-network-installation.md @@ -129,7 +129,7 @@ A Python script automates the full `nmcli` connection setup for Saxion: ```bash # 1. Download -curl -LO https://zephyrus-linux.stensel.nl/scripts/saxion-eduroam.py +curl -LO https://zephyrus-linux.thectic.nl/scripts/saxion-eduroam.py # 2. Verify checksum echo "17cd13c629ce480ece1a7896aff7d4061347ea0082b32dfa6b23dac6b34882ad saxion-eduroam.py" | sha256sum -c diff --git a/src/content/docs/networking/eduroam-network-installation.nl.md b/src/content/docs/networking/eduroam-network-installation.nl.md index 28824ac..81945f9 100644 --- a/src/content/docs/networking/eduroam-network-installation.nl.md +++ b/src/content/docs/networking/eduroam-network-installation.nl.md @@ -129,7 +129,7 @@ Een Python-script automatiseert de volledige `nmcli`-verbindingsconfiguratie voo ```bash # 1. Download -curl -LO https://zephyrus-linux.stensel.nl/scripts/saxion-eduroam.py +curl -LO https://zephyrus-linux.thectic.nl/scripts/saxion-eduroam.py # 2. Controleer de checksum echo "17cd13c629ce480ece1a7896aff7d4061347ea0082b32dfa6b23dac6b34882ad saxion-eduroam.py" | sha256sum -c diff --git a/src/hugo.toml b/src/hugo.toml index f94245c..46a00e6 100644 --- a/src/hugo.toml +++ b/src/hugo.toml @@ -1,4 +1,4 @@ -baseURL = 'https://zephyrus-linux.stensel.nl/' +baseURL = 'https://zephyrus-linux.thectic.nl/' title = 'Zephyrus Linux' defaultContentLanguage = 'en' enableRobotsTXT = true diff --git a/src/layouts/robots.txt b/src/layouts/robots.txt index e10def0..c8294ff 100644 --- a/src/layouts/robots.txt +++ b/src/layouts/robots.txt @@ -1,4 +1,42 @@ +{{- if hugo.IsProduction -}} User-agent: * -Disallow: +Allow: / -Sitemap: {{ .Site.BaseURL }}sitemap.xml +Sitemap: {{ "sitemap.xml" | absURL }} + +# Crawlers that collect material to train AI models are not welcome. This is +# obeyed voluntarily; blocking at the network level is what actually enforces it. +User-agent: GPTBot +User-agent: OAI-SearchBot +User-agent: ChatGPT-User +User-agent: ClaudeBot +User-agent: Claude-Web +User-agent: anthropic-ai +User-agent: Google-Extended +User-agent: Applebot-Extended +User-agent: meta-externalagent +User-agent: FacebookBot +User-agent: PerplexityBot +User-agent: Bytespider +User-agent: CCBot +User-agent: Amazonbot +User-agent: cohere-ai +User-agent: Diffbot +User-agent: ImagesiftBot +User-agent: Omgilibot +User-agent: Timpibot +User-agent: YouBot +Disallow: / +{{- else -}} +# This is not the live site. This build came from something other than a +# production deploy; the site itself is at https://zephyrus-linux.thectic.nl/. +# +# Everything locked, because a test copy that gets indexed is, to Google, the +# same site at two addresses, and then Google picks which of the two to show. +# Every page also carries a noindex for the same reason; robots.txt alone is not +# enough, since a page someone links to can still surface without that meta tag. +# +# No Sitemap line here: it would point crawlers straight at the pages. +User-agent: * +Disallow: / +{{- end -}} diff --git a/src/static/.well-known/security.txt b/src/static/.well-known/security.txt index 39ce28b..75eb78e 100644 --- a/src/static/.well-known/security.txt +++ b/src/static/.well-known/security.txt @@ -1,4 +1,4 @@ Contact: https://github.com/THectic-NL/Zephyrus-Linux/issues -Canonical: https://zephyrus-linux.stensel.nl/.well-known/security.txt +Canonical: https://zephyrus-linux.thectic.nl/.well-known/security.txt Expires: 2027-02-18T00:00:00.000Z Preferred-Languages: en, nl diff --git a/src/static/CNAME b/src/static/CNAME deleted file mode 100644 index 97bb7dc..0000000 --- a/src/static/CNAME +++ /dev/null @@ -1 +0,0 @@ -zephyrus-linux.stensel.nl