chore(deps): bump trineta.net/gcc-glibc from c5b599c to 4709b23
#9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" |