From 516861d3bbcd2bfe78a95a8f93242a765296f41b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 15:57:53 +0000 Subject: [PATCH] fix: recompute tool checksums on Renovate's pull requests Renovate bumps the pinned tool versions but cannot compute a checksum, so every bump arrives with the previous release's hash still in place and the build stops at "computed checksum did NOT match". It has already happened in THectic.nl on a Hugo bump; actionlint and lychee are pinned the same way here and would fail the same way. This follows the pattern already in use in Stensel8/scripts for the nginx installer: a script that recalculates the hashes, and a workflow that runs it on Renovate's own branches and commits the result back. One detail carried over from there that is easy to miss: gitIgnoredAuthors. Without it Renovate sees the bot's commit as the branch having been modified by someone else, and stops maintaining the pull request. The hash is not simply whatever the download returned. Both projects publish a checksum file next to their release; the script verifies the download against that first and refuses to write anything on a mismatch, so a hash only lands here if upstream vouches for it too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s --- .github/scripts/update-tool-checksums.sh | 158 +++++++++++++++++++++++ .github/workflows/update-checksums.yml | 56 ++++++++ renovate.json | 4 + 3 files changed, 218 insertions(+) create mode 100755 .github/scripts/update-tool-checksums.sh create mode 100644 .github/workflows/update-checksums.yml diff --git a/.github/scripts/update-tool-checksums.sh b/.github/scripts/update-tool-checksums.sh new file mode 100755 index 0000000..9b4f15e --- /dev/null +++ b/.github/scripts/update-tool-checksums.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# +# Recalculate and optionally apply the SHA-256 checksums of the pinned CI tools. +# +# Renovate bumps the version numbers but cannot compute a checksum, so without +# this the pinned hash keeps pointing at the previous release and every bump +# fails the build with "computed checksum did NOT match". The workflow in +# .github/workflows/update-checksums.yml runs this on Renovate's own pull +# requests and commits the result back onto the branch. +# +# The hash is not simply taken from whatever the download happened to return. +# Each project publishes its own checksum file next to the release; the +# download is verified against that first, and only a verified hash is written +# into the repository. +# +# Usage: +# .github/scripts/update-tool-checksums.sh # show, then ask +# .github/scripts/update-tool-checksums.sh --apply # write without asking +# + +set -euo pipefail + +readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' + +Write-Log() { + local level=$1; shift + local color=$NC + case $level in + INFO) color=$BLUE ;; + SUCCESS) color=$GREEN ;; + WARN) color=$YELLOW ;; + ERROR) color=$RED ;; + esac + if [[ $level == ERROR ]]; then + echo -e "${color}[$level]${NC} $*" >&2 + else + echo -e "${color}[$level]${NC} $*" + fi +} + +Stop-Script() { + Write-Log ERROR "$1" + exit 1 +} + +Show-Usage() { + cat <<'EOF' +Usage: update-tool-checksums.sh [--apply] + +Options: + --apply Write the checksums without prompting + -h, --help Show this help +EOF +} + +APPLY=false +while [[ $# -gt 0 ]]; do + case "$1" in + --apply) APPLY=true; shift ;; + -h|--help) Show-Usage; exit 0 ;; + *) Write-Log ERROR "Unknown argument: $1"; Show-Usage; exit 1 ;; + esac +done + +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" + +# ── Reading and writing the pinned values ─────────────────────────────────── + +# Usage: Get-KeyValue -> value of `KEY: "value"` +Get-KeyValue() { + sed -n "s/^[[:space:]]*$2:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$1" | head -n1 +} + +# Usage: Set-KeyValue +Set-KeyValue() { + sed -i "s|^\([[:space:]]*$2:[[:space:]]*\"\)[^\"]*\"|\1$3\"|" "$1" +} + +# ── Fetching and verifying ────────────────────────────────────────────────── + +TEMP_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEMP_DIR"' EXIT + +# Usage: Get-VerifiedHash +# Downloads the artifact, checks it against the hash the project published, and +# echoes that hash. Refuses to return anything if the two disagree. +Get-VerifiedHash() { + local name=$1 url=$2 expected=$3 + local file="$TEMP_DIR/$name" + + [[ "$expected" =~ ^[a-f0-9]{64}$ ]] || Stop-Script "$name: no valid checksum published upstream (got: '$expected')" + + curl -sSL --fail-with-body --retry 5 --retry-delay 3 --retry-all-errors -o "$file" "$url" \ + || Stop-Script "$name: download failed ($url)" + + local actual + actual="$(sha256sum "$file" | awk '{print $1}')" + + if [[ "$actual" != "$expected" ]]; then + Stop-Script "$name: download does not match the published checksum. published=$expected downloaded=$actual" + fi + + echo "$actual" +} + +# Usage: Get-PublishedHash +# Pulls one line out of a checksums file and returns the hash on it. +Get-PublishedHash() { + curl -sSL --fail-with-body --retry 5 --retry-delay 3 --retry-all-errors "$1" \ + | grep -- "$2" | awk '{print $1}' | head -n1 +} + +# ── The tools ─────────────────────────────────────────────────────────────── + +ACTIONLINT_VERSION="$(Get-KeyValue "$CONFIG_VALIDATION" ACTIONLINT_VERSION)" +LYCHEE_VERSION="$(Get-KeyValue "$PR_CHECKS" LYCHEE_VERSION)" + +for pair in "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 " actionlint: $ACTIONLINT_VERSION" +echo " lychee: $LYCHEE_VERSION" +echo + +Write-Log INFO "Downloading and verifying against the published checksums..." + +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")")" +Write-Log SUCCESS "actionlint: $ACTIONLINT_SHA256" + +LYCHEE_SHA256="$(Get-VerifiedHash "lychee.tar.gz" \ + "https://github.com/lycheeverse/lychee/releases/download/lychee-v${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz" \ + "$(Get-PublishedHash "https://github.com/lycheeverse/lychee/releases/download/lychee-v${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz.sha256" "")")" +Write-Log SUCCESS "lychee: $LYCHEE_SHA256" + +echo +if [[ "$APPLY" != true ]]; then + read -rp "Write these checksums into the repository? [y/N] " response + if [[ ! "$response" =~ ^[Yy]$ ]]; then + Write-Log INFO "No changes made" + exit 0 + fi +fi + +Set-KeyValue "$CONFIG_VALIDATION" ACTIONLINT_SHA256 "$ACTIONLINT_SHA256" +Set-KeyValue "$PR_CHECKS" LYCHEE_SHA256 "$LYCHEE_SHA256" + +Write-Log SUCCESS "Updated:" +echo " - $CONFIG_VALIDATION" +echo " - $PR_CHECKS" diff --git a/.github/workflows/update-checksums.yml b/.github/workflows/update-checksums.yml new file mode 100644 index 0000000..32dfa43 --- /dev/null +++ b/.github/workflows/update-checksums.yml @@ -0,0 +1,56 @@ +# Copyright (C) 2026 Sten Tijhuis +# SPDX-License-Identifier: MIT +name: Update tool SHA256 checksums + +# Renovate bumps the pinned tool versions but cannot compute a checksum, so on +# its own every bump lands with the previous release's hash still in place and +# the build stops at "computed checksum did NOT match". This recalculates the +# hashes on Renovate's pull requests and commits them back onto the branch. +# +# Renovate must be told to ignore those commits, or it treats the branch as +# modified by someone else and stops maintaining the pull request. That is the +# gitIgnoredAuthors entry in renovate.json. + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, development] + paths: + - '.github/workflows/config-validation.yml' + - '.github/workflows/pr-checks.yml' + - '.github/scripts/update-tool-checksums.sh' + +permissions: {} + +jobs: + update-checksums: + name: Recalculate SHA256 checksums + runs-on: ubuntu-latest + # Only Renovate's own branches. Running this on a human's pull request + # would mean pushing commits to a branch someone is actively working on. + if: startsWith(github.head_ref, 'renovate/') && github.actor == 'renovate[bot]' + permissions: + contents: write + steps: + - name: Check out the pull request branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.head_ref }} + + # The script verifies each download against the checksum the project + # publishes next to the release before writing anything, so a hash only + # lands here if upstream vouches for it too. + - name: Recalculate and apply checksums + run: .github/scripts/update-tool-checksums.sh --apply + + - name: Commit updated checksums + 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 + if git diff --staged --quiet; then + echo "Checksums are already up to date, nothing to commit." + else + git commit -m "chore: update tool SHA256 checksums" + git push + fi diff --git a/renovate.json b/renovate.json index 94c4088..d8e6177 100644 --- a/renovate.json +++ b/renovate.json @@ -6,6 +6,10 @@ "timezone": "Europe/Amsterdam", "forkProcessing": "enabled", "pinDigests": true, + "gitIgnoredAuthors": [ + "github-actions[bot]@users.noreply.github.com", + "41898282+github-actions[bot]@users.noreply.github.com" + ], "assigneesFromCodeOwners": true, "reviewersFromCodeOwners": true, "enabledManagers": [