Skip to content

chore(deps): bump trineta.net/gcc-glibc from c5b599c to 4709b23 #9

chore(deps): bump trineta.net/gcc-glibc from c5b599c to 4709b23

chore(deps): bump trineta.net/gcc-glibc from c5b599c to 4709b23 #9

name: Dependabot image diff enrichment
# Runs on Dependabot PRs that bump container image digests, computes
# `chainctl images diff` for each updated image, and appends the result
# to the PR body so reviewers see the package/CVE delta.
#
# pull_request_target is required because:
# 1. Dependabot PRs run with a read-only GITHUB_TOKEN under `pull_request`,
# so they can't edit the PR body.
# 2. We need OIDC (id-token: write) for setup-chainctl, which only the
# base-branch token grants.
# Safety: this workflow does NOT check out PR-controlled code. It only reads
# the PR diff via the API and runs trusted scripts from the base branch.
on:
pull_request_target:
paths:
- "**/Dockerfile*"
- "**/*.yaml"
- "**/*.yml"
concurrency:
group: dependabot-image-diff-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
enrich:
if: github.actor == 'dependabot[bot]'
name: Enrich PR with chainctl images diff
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: write
id-token: write # OIDC token for setup-chainctl
steps:
- name: Checkout base ref (trusted)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Check required configuration
id: check-config
run: |
if [ -z "${{ vars.CHAINGUARD_IDENTITY }}" ]; then
echo "CHAINGUARD_IDENTITY not configured — skipping enrichment."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup chainctl
if: steps.check-config.outputs.skip == 'false'
uses: chainguard-dev/setup-chainctl@f52718d822dc73d21a04ef2082822c4a203163b3 # v0.2.2
with:
identity: ${{ vars.CHAINGUARD_IDENTITY }}
- name: Install grype
if: steps.check-config.outputs.skip == 'false'
env:
GRYPE_VERSION: v0.112.0
GRYPE_SHA256: acb14a030010fe9bdb9594b4ae108d9d14ef2f926d936aa0916dc62c89c058ea
run: |
cd "$RUNNER_TEMP"
curl -sSfL -o grype.tar.gz \
"https://github.com/anchore/grype/releases/download/${GRYPE_VERSION}/grype_${GRYPE_VERSION#v}_linux_amd64.tar.gz"
echo "${GRYPE_SHA256} grype.tar.gz" | sha256sum --check
tar -xzf grype.tar.gz -C /usr/local/bin grype
- name: Build updates JSON from PR diff
if: steps.check-config.outputs.skip == 'false'
id: parse
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Fetch each changed file's patch and extract image@sha256 pairs
# of the form removed→added, building a JSON array of updates that
# mirrors digestabot's output schema:
# [{file, image, digest, updated_digest}, ...]
files_json=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}/files" --paginate)
updates='[]'
while read -r file_entry; do
filename=$(jq -r '.filename' <<<"$file_entry")
patch=$(jq -r '.patch // ""' <<<"$file_entry")
[ -z "$patch" ] && continue
# Pull all "-" and "+" lines that contain @sha256: digests, in order.
mapfile -t removed < <(grep -E '^-[^-].*@sha256:[a-f0-9]+' <<<"$patch" | grep -oE '[a-z0-9][a-z0-9._/:-]*@sha256:[a-f0-9]+' || true)
mapfile -t added < <(grep -E '^\+[^+].*@sha256:[a-f0-9]+' <<<"$patch" | grep -oE '[a-z0-9][a-z0-9._/:-]*@sha256:[a-f0-9]+' || true)
# Pair removed/added by image:tag (everything before the @).
for old_ref in "${removed[@]}"; do
old_image="${old_ref%@*}"
old_digest="${old_ref##*@}"
for new_ref in "${added[@]}"; do
new_image="${new_ref%@*}"
new_digest="${new_ref##*@}"
if [ "$old_image" = "$new_image" ] && [ "$old_digest" != "$new_digest" ]; then
updates=$(jq -c \
--arg file "$filename" \
--arg image "$old_image" \
--arg digest "$old_digest" \
--arg updated_digest "$new_digest" \
'. += [{file: $file, image: $image, digest: $digest, updated_digest: $updated_digest}]' \
<<<"$updates")
break
fi
done
done
done < <(jq -c '.[]' <<<"$files_json")
count=$(jq 'length' <<<"$updates")
echo "Found $count digest updates"
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "json=$(jq -c '{updates: .}' <<<"$updates")" >> "$GITHUB_OUTPUT"
- name: Compute chainctl image diff and update PR body
if: steps.check-config.outputs.skip == 'false' && steps.parse.outputs.count != '0'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
UPDATES_JSON: ${{ steps.parse.outputs.json }}
run: |
body_file=$(mktemp)
# Preserve the original Dependabot PR body and append below a separator.
gh pr view "${PR_NUMBER}" --repo "${{ github.repository }}" --json body --jq .body > "${body_file}"
{
echo ""
echo "---"
echo ""
echo "## chainctl images diff"
echo ""
} >> "${body_file}"
processed_images='[]'
while read -r item; do
image=$(jq -r '.image + "@" + .updated_digest' <<<"$item")
old_image=$(jq -r '.image + "@" + .digest' <<<"$item")
old_digest=$(jq -r '.digest' <<<"$item")
new_digest=$(jq -r '.updated_digest' <<<"$item")
# Deduplicate
image_pair_hash=$(sha256sum <<<"${image}${old_image}" | awk '{print $1}')
if [[ $(jq -e ".[] | select(. == \"${image_pair_hash}\")" <<<"${processed_images}") ]]; then
continue
fi
processed_images=$(jq -c ". += [\"${image_pair_hash}\"]" <<<"${processed_images}")
echo "Computing diff for ${image}"
if ! diff=$(chainctl images diff "${old_image}" "${image}" -o json 2>/tmp/chainctl-diff.log); then
err=$(cat /tmp/chainctl-diff.log)
echo "chainctl diff failed: ${err}"
image_name=$(jq -r '.image' <<<"$item")
{
echo "### ${image_name}"
echo ""
echo "| | Digest |"
echo "| ---- | ------ |"
echo "| From | \`${old_digest}\` |"
echo "| To | \`${new_digest}\` |"
echo ""
echo "> :warning: \`chainctl images diff\` failed for this image."
echo ""
echo "<details><summary>Error</summary>"
echo ""
echo '```'
echo "${err}"
echo '```'
echo ""
echo "</details>"
echo ""
} >> "${body_file}"
continue
fi
# chainctl images diff JSON structure:
# .vulnerabilities.{removed,added}[] => {id, severity}
# .packages.{added,removed}[] => {name, version, reference}
# .packages.changed[] => {name, current.{version,reference}, previous.version}
# Filter to pkg:apk/ refs to avoid duplicate CPE entries.
total_changes=$(jq '
(.vulnerabilities.removed // [] | length) +
(.vulnerabilities.added // [] | length) +
(.packages.changed // [] | map(select(.current.reference | startswith("pkg:apk/"))) | length) +
(.packages.added // [] | map(select(.reference | startswith("pkg:apk/"))) | length) +
(.packages.removed // [] | map(select(.reference | startswith("pkg:apk/"))) | length)
' <<<"${diff}")
if [[ ${total_changes} -lt 1 ]]; then
echo "No changes for ${image}, skipping"
continue
fi
image_name=$(jq -r '.image' <<<"$item")
echo "### ${image_name}" >> "${body_file}"
echo "" >> "${body_file}"
echo "| | Digest |" >> "${body_file}"
echo "| ---- | ------ |" >> "${body_file}"
echo "| From | \`${old_digest}\` |" >> "${body_file}"
echo "| To | \`${new_digest}\` |" >> "${body_file}"
echo "" >> "${body_file}"
echo "<details>" >> "${body_file}"
echo "" >> "${body_file}"
echo "#### Vulnerabilities Added" >> "${body_file}"
vuln_added=$(jq -r '.vulnerabilities.added // []' <<<"${diff}")
if [[ $(echo "${vuln_added}" | jq 'length') -gt 0 ]]; then
echo "| Id | Severity |" >> "${body_file}"
echo "| -- | -------- |" >> "${body_file}"
echo "${vuln_added}" | jq -r '.[] | "| \(.id) | \(.severity) |"' >> "${body_file}"
else
echo "None" >> "${body_file}"
fi
echo "" >> "${body_file}"
echo "#### Vulnerabilities Removed" >> "${body_file}"
vuln_removed=$(jq -r '.vulnerabilities.removed // []' <<<"${diff}")
if [[ $(echo "${vuln_removed}" | jq 'length') -gt 0 ]]; then
echo "| Id | Severity |" >> "${body_file}"
echo "| -- | -------- |" >> "${body_file}"
echo "${vuln_removed}" | jq -r '.[] | "| \(.id) | \(.severity) |"' >> "${body_file}"
else
echo "None" >> "${body_file}"
fi
echo "" >> "${body_file}"
echo "#### Packages Added" >> "${body_file}"
pkgs_added=$(jq '.packages.added // [] | map(select(.reference | startswith("pkg:apk/")))' <<<"${diff}")
if [[ $(echo "${pkgs_added}" | jq 'length') -gt 0 ]]; then
echo "| Name | Version | Reference |" >> "${body_file}"
echo "| ---- | ------- | --------- |" >> "${body_file}"
echo "${pkgs_added}" | jq -r '.[] | "| \(.name) | \(.version) | \(.reference) |"' >> "${body_file}"
else
echo "None" >> "${body_file}"
fi
echo "" >> "${body_file}"
echo "#### Packages Removed" >> "${body_file}"
pkgs_removed=$(jq '.packages.removed // [] | map(select(.reference | startswith("pkg:apk/")))' <<<"${diff}")
if [[ $(echo "${pkgs_removed}" | jq 'length') -gt 0 ]]; then
echo "| Name | Version | Reference |" >> "${body_file}"
echo "| ---- | ------- | --------- |" >> "${body_file}"
echo "${pkgs_removed}" | jq -r '.[] | "| \(.name) | \(.version) | \(.reference) |"' >> "${body_file}"
else
echo "None" >> "${body_file}"
fi
echo "" >> "${body_file}"
echo "#### Packages Changed" >> "${body_file}"
pkgs_changed=$(jq '.packages.changed // [] | map(select(.current.reference | startswith("pkg:apk/")))' <<<"${diff}")
if [[ $(echo "${pkgs_changed}" | jq 'length') -gt 0 ]]; then
echo "| Name | Previous Version | Current Version |" >> "${body_file}"
echo "| ---- | ---------------- | --------------- |" >> "${body_file}"
echo "${pkgs_changed}" | jq -r '.[] | "| \(.name) | \(.previous.version) | \(.current.version) |"' >> "${body_file}"
else
echo "None" >> "${body_file}"
fi
echo "" >> "${body_file}"
echo "</details>" >> "${body_file}"
echo "" >> "${body_file}"
done < <(jq -c '.updates // [] | .[]' <<<"${UPDATES_JSON}")
# GitHub PR bodies are capped at 65,536 characters.
max_body_bytes=65000
if [[ $(wc -c < "${body_file}") -gt ${max_body_bytes} ]]; then
truncated_file=$(mktemp)
head -c ${max_body_bytes} "${body_file}" > "${truncated_file}"
{
echo ""
echo "---"
echo ""
echo "> :warning: PR body truncated to fit the 65,536 character limit. See workflow logs for the full diff."
} >> "${truncated_file}"
body_file="${truncated_file}"
fi
gh pr edit "${PR_NUMBER}" --body-file "${body_file}" --repo "${{ github.repository }}"