Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/scripts/check-renovate-patterns.py
Original file line number Diff line number Diff line change
@@ -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.")
121 changes: 121 additions & 0 deletions .github/workflows/config-validation.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions .github/workflows/hugo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 39 additions & 11 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(?<version>.+)$
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 <page>/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:
Expand Down
Loading
Loading