From 9018ba5800cef169e709c6c7c0fd3135ccf6a00e Mon Sep 17 00:00:00 2001 From: Cristiano Betta Date: Fri, 23 Oct 2026 16:39:30 +0100 Subject: [PATCH 1/4] fix(ci): a digest-only refresh is one whose diff changes nothing but @sha256 digests Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dc69e5..959091e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,6 @@ jobs: UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }} NEW_VERSION: ${{ steps.meta.outputs.new-version }} - PREVIOUS_VERSION: ${{ steps.meta.outputs.previous-version }} run: | merge=false if [ "$HAS_APP" != true ]; then @@ -78,9 +77,12 @@ jobs: elif [ "$FROM_DEPENDABOT" = true ]; then case "$ECOSYSTEM:$UPDATE_TYPE" in github_actions:*) reason="Actions updates wait for a person" ;; - # Same tag, new digest: Dependabot names the old digest as the previous version (a new tag names the old tag). + # Same tags, new digests: every changed line differs only in its @sha256 digest (grouped updates included). docker:*|docker_compose:*) - if [[ "$(tr -d "\`" <<< "$PREVIOUS_VERSION")" =~ ^[0-9a-f]{7,}$ ]]; then merge=true; else reason="a new image tag waits for a person"; fi ;; + diff="$(gh pr diff "$PR" -R "$GITHUB_REPOSITORY")" + old="$({ grep '^-[^-]' <<< "$diff" || true; } | sed -E 's/^-//; s/@sha256:[0-9a-f]{64}//g' | sort)" + new="$({ grep '^+[^+]' <<< "$diff" || true; } | sed -E 's/^\+//; s/@sha256:[0-9a-f]{64}//g' | sort)" + if [ -n "$old" ] && [ "$old" = "$new" ]; then merge=true; else reason="it changes more than image digests"; fi ;; *:version-update:semver-patch) merge=true ;; *:version-update:semver-minor) if [ "${NEW_VERSION%%.*}" = 0 ]; then reason="a 0.x minor can break"; else merge=true; fi ;; From 20322a9fc379737966e4deff2cc97018b9b6eee0 Mon Sep 17 00:00:00 2001 From: Cristiano Betta Date: Fri, 23 Oct 2026 17:05:00 +0100 Subject: [PATCH 2/4] fix(ci): a digest refresh is judged file by file, with digests as a marker, so pins can't be dropped and nothing can move Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 959091e..207c102 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,12 +77,20 @@ jobs: elif [ "$FROM_DEPENDABOT" = true ]; then case "$ECOSYSTEM:$UPDATE_TYPE" in github_actions:*) reason="Actions updates wait for a person" ;; - # Same tags, new digests: every changed line differs only in its @sha256 digest (grouped updates included). + # Same tags, new digests: every changed file is modified in place, and is the same before and after once each digest is a marker (so a pin can't be dropped, and nothing can move). docker:*|docker_compose:*) - diff="$(gh pr diff "$PR" -R "$GITHUB_REPOSITORY")" - old="$({ grep '^-[^-]' <<< "$diff" || true; } | sed -E 's/^-//; s/@sha256:[0-9a-f]{64}//g' | sort)" - new="$({ grep '^+[^+]' <<< "$diff" || true; } | sed -E 's/^\+//; s/@sha256:[0-9a-f]{64}//g' | sort)" - if [ -n "$old" ] && [ "$old" = "$new" ]; then merge=true; else reason="it changes more than image digests"; fi ;; + merge=true + head="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .head.sha)" || merge=false + base="$(gh api "repos/$GITHUB_REPOSITORY/compare/$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .base.sha)...$head" --jq .merge_base_commit.sha)" || merge=false + files="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR/files" --paginate --jq '.[] | [.status, .filename] | @tsv')" || merge=false + [ -n "$files" ] || merge=false + while [ "$merge" = true ] && IFS=$'\t' read -r status file; do + [ "$status" = modified ] || { merge=false; break; } + a="$(gh api "repos/$GITHUB_REPOSITORY/contents/$file?ref=$base" --jq .content | base64 -d | sed -E 's/@sha256:[0-9a-f]{64}/@sha256:DIGEST/g')" || { merge=false; break; } + b="$(gh api "repos/$GITHUB_REPOSITORY/contents/$file?ref=$head" --jq .content | base64 -d | sed -E 's/@sha256:[0-9a-f]{64}/@sha256:DIGEST/g')" || { merge=false; break; } + [ "$a" = "$b" ] || { merge=false; break; } + done <<< "$files" + [ "$merge" = true ] || reason="it changes more than image digests" ;; *:version-update:semver-patch) merge=true ;; *:version-update:semver-minor) if [ "${NEW_VERSION%%.*}" = 0 ]; then reason="a 0.x minor can break"; else merge=true; fi ;; From 0ea7287d80969d7498835c1e2887e01c229bebd4 Mon Sep 17 00:00:00 2001 From: Cristiano Betta Date: Fri, 23 Oct 2026 17:24:10 +0100 Subject: [PATCH 3/4] fix(ci): the digest-refresh check fails closed, and catches mode changes and trailing newlines Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 207c102..8319acb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,18 +77,20 @@ jobs: elif [ "$FROM_DEPENDABOT" = true ]; then case "$ECOSYSTEM:$UPDATE_TYPE" in github_actions:*) reason="Actions updates wait for a person" ;; - # Same tags, new digests: every changed file is modified in place, and is the same before and after once each digest is a marker (so a pin can't be dropped, and nothing can move). + # Same tags, new digests: every changed file is modified in place with its mode kept, and its bytes are the same before and after once each digest is a marker (so a pin can't be dropped, nothing can move, and a failed lookup refuses). docker:*|docker_compose:*) + set -o pipefail merge=true + mode() { gh api "repos/$GITHUB_REPOSITORY/git/trees/$1?recursive=1" --jq ".tree[] | select(.path == \"$2\") | .mode"; } + norm() { gh api "repos/$GITHUB_REPOSITORY/contents/$2?ref=$1" --jq .content | base64 -d | sed -E 's/@sha256:[0-9a-f]{64}/@sha256:DIGEST/g' | sha256sum; } head="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .head.sha)" || merge=false base="$(gh api "repos/$GITHUB_REPOSITORY/compare/$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .base.sha)...$head" --jq .merge_base_commit.sha)" || merge=false files="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR/files" --paginate --jq '.[] | [.status, .filename] | @tsv')" || merge=false [ -n "$files" ] || merge=false while [ "$merge" = true ] && IFS=$'\t' read -r status file; do [ "$status" = modified ] || { merge=false; break; } - a="$(gh api "repos/$GITHUB_REPOSITORY/contents/$file?ref=$base" --jq .content | base64 -d | sed -E 's/@sha256:[0-9a-f]{64}/@sha256:DIGEST/g')" || { merge=false; break; } - b="$(gh api "repos/$GITHUB_REPOSITORY/contents/$file?ref=$head" --jq .content | base64 -d | sed -E 's/@sha256:[0-9a-f]{64}/@sha256:DIGEST/g')" || { merge=false; break; } - [ "$a" = "$b" ] || { merge=false; break; } + ma="$(mode "$base" "$file")" && mb="$(mode "$head" "$file")" && [ -n "$ma" ] && [ "$ma" = "$mb" ] || { merge=false; break; } + a="$(norm "$base" "$file")" && b="$(norm "$head" "$file")" && [ "$a" = "$b" ] || { merge=false; break; } done <<< "$files" [ "$merge" = true ] || reason="it changes more than image digests" ;; *:version-update:semver-patch) merge=true ;; From 7625b61446fa40724818ea61e339a082398ea182 Mon Sep 17 00:00:00 2001 From: Cristiano Betta Date: Fri, 23 Oct 2026 17:34:14 +0100 Subject: [PATCH 4/4] fix(ci): the digest-refresh check reads raw file bytes, so a file over 1 MB can't compare as empty Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8319acb..6255a4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: set -o pipefail merge=true mode() { gh api "repos/$GITHUB_REPOSITORY/git/trees/$1?recursive=1" --jq ".tree[] | select(.path == \"$2\") | .mode"; } - norm() { gh api "repos/$GITHUB_REPOSITORY/contents/$2?ref=$1" --jq .content | base64 -d | sed -E 's/@sha256:[0-9a-f]{64}/@sha256:DIGEST/g' | sha256sum; } + norm() { gh api -H "Accept: application/vnd.github.raw" "repos/$GITHUB_REPOSITORY/contents/$2?ref=$1" | sed -E 's/@sha256:[0-9a-f]{64}/@sha256:DIGEST/g' | sha256sum; } head="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .head.sha)" || merge=false base="$(gh api "repos/$GITHUB_REPOSITORY/compare/$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .base.sha)...$head" --jq .merge_base_commit.sha)" || merge=false files="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR/files" --paginate --jq '.[] | [.status, .filename] | @tsv')" || merge=false