diff --git a/.github/scripts/check-renovate-patterns.py b/.github/scripts/check-renovate-patterns.py new file mode 100755 index 0000000..bb7c695 --- /dev/null +++ b/.github/scripts/check-renovate-patterns.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +"""Flag Renovate file patterns that look like a regex but are not delimited. + +managerFilePatterns and matchFileNames accept "RegEx (re2) and glob patterns". +A value counts as a regex only when it is wrapped in slashes; everything else +is read as a glob. So a pattern like + + "^\\.github/workflows/.*\\.ya?ml$" + +matches no file at all, and the custom manager around it never fires. Nothing +reports this: renovate-config-validator says the config is valid, because it +is -- it just silently does nothing. The only visible symptom is a dependency +that stops receiving updates, which is easy to miss for months. + +Usage: check-renovate-patterns.py [config.json ...] +Missing files are skipped, so the same call works in every repository. +""" +import json +import pathlib +import re +import sys + +# Constructs that carry meaning in a regex but not in a glob. +REGEXY = re.compile(r"^\^|\$$|\\\.|\.\*|\.\+|\(\?|\[\^|\\d|\\w|\\s|[)?]\|") + +# Renovate options whose values are matched as "regex or glob". +PATTERN_KEYS = { + "managerFilePatterns", + "matchFileNames", + "fileMatch", + "matchPackageNames", +} + +problems = [] + + +def walk(node, path, source): + if isinstance(node, dict): + for key, value in node.items(): + if key in PATTERN_KEYS and isinstance(value, list): + for index, pattern in enumerate(value): + if not isinstance(pattern, str): + continue + # A trailing "i" flag is allowed: /pattern/i + delimited = pattern.startswith("/") and pattern.rstrip("i").endswith("/") + if REGEXY.search(pattern) and not delimited: + problems.append((source, f"{path}.{key}[{index}]", pattern)) + walk(value, f"{path}.{key}", source) + elif isinstance(node, list): + for index, item in enumerate(node): + walk(item, f"{path}[{index}]", source) + + +files = [path for path in (pathlib.Path(a) for a in sys.argv[1:]) if path.is_file()] +if not files: + print("No Renovate config found to check.") + sys.exit(0) + +for config in files: + walk(json.loads(config.read_text()), "$", str(config)) + +if problems: + print("Renovate file patterns that look like a regex but are not wrapped in slashes.") + print("Renovate reads these as globs, so they match nothing and the rule never fires.\n") + for source, where, pattern in problems: + print(f"::error file={source}::{where}: {pattern!r} is read as a glob, not a regex") + print(f" {source} {where}") + print(f" found: {pattern!r}") + print(f" expect: '/{pattern}/'\n") + sys.exit(1) + +print(f"Checked {len(files)} Renovate config file(s): all file patterns are well formed.") diff --git a/.github/workflows/config-validation.yml b/.github/workflows/config-validation.yml new file mode 100644 index 0000000..d6bd757 --- /dev/null +++ b/.github/workflows/config-validation.yml @@ -0,0 +1,121 @@ +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +name: Config validation + +# The bot configs are the one part of CI that nothing else exercises: a broken +# renovate.json or dependabot.yml does not fail a build, it just quietly stops +# doing its job. This workflow is the thing that notices. + +on: + push: + branches: [main, development] + paths: + - 'renovate.json' + - '.github/renovate.json' + - '.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/**' + pull_request: + branches: [main, development] + paths: + - 'renovate.json' + - '.github/renovate.json' + - '.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/**' + workflow_dispatch: + +permissions: {} + +jobs: + bot-configs: + name: Renovate and Dependabot config + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: 'lts/*' + + # Renovate's own validator. --strict also fails on warnings, such as an + # option that is valid but deprecated. Given no arguments it finds the + # config files itself and validates them as repository config; passing a + # path instead makes it validate them as global config, which is a + # different and weaker set of rules. + - name: Validate Renovate config + env: + # renovate: datasource=npm depName=renovate + RENOVATE_VERSION: "42.99.0" + run: npx --yes --package "renovate@${RENOVATE_VERSION}" -- renovate-config-validator --strict + + # The validator above accepts a well-formed pattern that matches nothing, + # so this covers the gap it leaves. + - name: Check Renovate file patterns + run: python3 .github/scripts/check-renovate-patterns.py renovate.json .github/renovate.json + + # GitHub validates dependabot.yml only after it is on the default branch, + # and reports the result on a tab nobody opens. This brings that forward + # to the pull request. + - name: Validate Dependabot config + env: + # renovate: datasource=pypi depName=check-jsonschema + CHECK_JSONSCHEMA_VERSION: "0.38.0" + run: | + config="" + for candidate in .github/dependabot.yml .github/dependabot.yaml; do + if [ -f "$candidate" ]; then + config="$candidate" + break + fi + done + if [ -z "$config" ]; then + echo "No dependabot.yml in this repository; nothing to validate." + exit 0 + fi + pipx install "check-jsonschema==${CHECK_JSONSCHEMA_VERSION}" + check-jsonschema --builtin-schema vendor.dependabot "$config" + + # The workflow files are config too. The other repositories in the + # organisation run actionlint from their quality workflow; this one had no + # equivalent, so it lives here. + workflow-lint: + name: Check workflow files + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Pinned release plus checksum, rather than piping a script from a + # branch straight into bash. + - name: Install actionlint + 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 + run: actionlint -color diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml index 9d219b5..800a8af 100644 --- a/.github/workflows/hugo.yml +++ b/.github/workflows/hugo.yml @@ -36,9 +36,9 @@ jobs: - 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 + 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 diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 53bbd92..cb8fc69 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -163,9 +163,9 @@ jobs: 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 + 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 env: HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache @@ -189,15 +189,43 @@ jobs: with: name: hugo-public path: public/ + # Installed by hand rather than through lycheeverse/lychee-action. That + # action fetches its binary with a bare `curl -sfLO`: no retry, and no + # check on what comes back. This job and one in THectic.nl both failed on + # that download within a quarter of an hour when GitHub's release CDN was + # having a bad day, neither having checked a single link. Pinned version, + # verified checksum, retried download. + - name: Install lychee + env: + # extractVersion: lychee tags its releases "lychee-v0.24.2", not + # "v0.24.2", so the default pattern cannot 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 + + # --index-files: without it lychee treats a link to /docs/applications/ as + # a link to a directory and stops there, so it can never look inside for + # the #fragment. Every anchor into another page then reports "Cannot find + # fragment" even though the heading is right there. Hugo serves every page + # as /index.html, so this flag is what makes --include-fragments + # usable at all here. + # + # The glob is quoted deliberately. Unquoted, bash expands it first, and + # without globstar ** collapses to a single level -- which is why this job + # was checking 95 links instead of 3379. - name: Check internal links - uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 - with: - args: >- - --offline - --include-fragments - --root-dir ./public - public/**/*.html - fail: true + run: | + lychee --offline --include-fragments --index-files index.html \ + --root-dir ./public "public/**/*.html" # ── 8. Auto-tick PR checklist ──────────────────────────────────────────────── update-checklist: diff --git a/renovate.json b/renovate.json index 917e300..94c4088 100644 --- a/renovate.json +++ b/renovate.json @@ -1,80 +1,102 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ], - "timezone": "Europe/Amsterdam", - "forkProcessing": "enabled", - "pinDigests": true, - "assigneesFromCodeOwners": true, - "reviewersFromCodeOwners": true, - "enabledManagers": [ - "github-actions", - "gomod", - "custom.regex" - ], - "prHourlyLimit": 2, - "prConcurrentLimit": 5, - "labels": [ - "dependencies" - ], - "packageRules": [ - { - "matchManagers": [ - "dockerfile", - "docker-compose" - ], - "groupName": "Docker images", - "addLabels": [ - "docker" - ] - }, - { - "matchManagers": [ - "github-actions" - ], - "groupName": "GitHub Actions", - "addLabels": [ - "github-actions" - ] - }, - { - "matchManagers": [ - "gomod" - ], - "groupName": "Go modules", - "addLabels": [ - "go" - ] - } - ], - "customManagers": [ - { - "customType": "regex", - "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" - } - ], - "automerge": true, - "automergeType": "pr", - "semanticCommits": "enabled" +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "timezone": "Europe/Amsterdam", + "forkProcessing": "enabled", + "pinDigests": true, + "assigneesFromCodeOwners": true, + "reviewersFromCodeOwners": true, + "enabledManagers": [ + "github-actions", + "gomod", + "custom.regex" + ], + "prHourlyLimit": 2, + "prConcurrentLimit": 5, + "labels": [ + "dependencies" + ], + "packageRules": [ + { + "matchManagers": [ + "dockerfile", + "docker-compose" + ], + "groupName": "Docker images", + "addLabels": [ + "docker" + ] + }, + { + "matchManagers": [ + "github-actions" + ], + "groupName": "GitHub Actions", + "addLabels": [ + "github-actions" + ] + }, + { + "matchManagers": [ + "gomod" + ], + "groupName": "Go modules", + "addLabels": [ + "go" + ] + }, + { + "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.", + "matchManagers": [ + "custom.regex" + ], + "groupName": "Build tooling versions", + "addLabels": [ + "build-tooling" + ], + "automerge": false + } + ], + "customManagers": [ + { + "customType": "regex", + "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$/" + ], + "matchStrings": [ + "# renovate: datasource=(?[a-z-]+) depName=(?\\S+)(?: extractVersion=(?\\S+))?\\s+[A-Za-z_]+: \"(?[^\"]+)\"" + ], + "extractVersionTemplate": "^v?(?.*)$" + } + ], + "automerge": true, + "automergeType": "pr", + "semanticCommits": "enabled" } diff --git a/src/content/docs/known-issues.nl.md b/src/content/docs/known-issues.nl.md index e911e55..1d9e19b 100644 --- a/src/content/docs/known-issues.nl.md +++ b/src/content/docs/known-issues.nl.md @@ -361,7 +361,7 @@ Scrollen met het touchpad voelde aanzienlijk sneller aan dan normaal in Brave en Een GNOME/Wayland-probleem, niet specifiek aan Brave. GNOME normaliseert scroll-events niet zoals het zou moeten, waardoor apps die niet via GTK's inputstack gaan rauwe hoge-precisie events van libinput ontvangen. Firefox en native GTK-apps werken wel goed omdat zij via GTK gaan. Veel andere apps hadden hetzelfde probleem. **Oplossing:** -[wayland-scroll-factor]({{< relref "/docs/applications#touchpad-scroll-speed-still-no-native-gnome-setting" >}}) lost dit op op GNOME-niveau door libinput-aanroepen binnen gnome-shell te onderscheppen en een scrollvermenigvuldiger toe te passen. Alles komt genormaliseerd uit. Het onderliggende GNOME-probleem staat nog steeds open upstream, maar WSF maakt het in de praktijk geen probleem meer. +[wayland-scroll-factor]({{< relref "/docs/applications#touchpad-scrollsnelheid-nog-steeds-geen-native-gnome-instelling" >}}) lost dit op op GNOME-niveau door libinput-aanroepen binnen gnome-shell te onderscheppen en een scrollvermenigvuldiger toe te passen. Alles komt genormaliseerd uit. Het onderliggende GNOME-probleem staat nog steeds open upstream, maar WSF maakt het in de praktijk geen probleem meer. **Bronnen:** - [brave-browser #36569: native touchpad scrolling op Linux Wayland](https://github.com/brave/brave-browser/issues/36569)