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
158 changes: 158 additions & 0 deletions .github/scripts/update-tool-checksums.sh
Original file line number Diff line number Diff line change
@@ -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 <file> <KEY> -> value of `KEY: "value"`
Get-KeyValue() {
sed -n "s/^[[:space:]]*$2:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$1" | head -n1
}

# Usage: Set-KeyValue <file> <KEY> <value>
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 <name> <artifact-url> <expected-sha256>
# 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 <url> <grep-pattern>
# 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"
56 changes: 56 additions & 0 deletions .github/workflows/update-checksums.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Loading