Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b6b6a44
fix(build-push-docker-manifest): idempotent manifest + cosign reruns …
HashWrangler Jun 17, 2026
49f5771
fix(build-push-docker-manifest): address PR review feedback (RANE-4683)
HashWrangler Jun 17, 2026
372f0db
chore(build-push-docker-manifest): address additional Copilot nits
HashWrangler Jun 17, 2026
16eec5a
chore(build-push-docker-manifest): address latest Copilot nits
HashWrangler Jun 17, 2026
f20a35e
fix(build-push-docker-manifest): de-duplicate platform digests for id…
HashWrangler Jun 17, 2026
435c5a8
fix(build-push-docker-manifest): address follow-up Copilot nits
HashWrangler Jun 17, 2026
d67b417
fix(build-push-docker-manifest): validate sha256 digest format early
HashWrangler Jun 17, 2026
dae4b88
chore(build-push-docker-manifest): replace jq install with availabili…
HashWrangler Jun 18, 2026
18263d8
fix(build-push-docker-manifest): ECR propagation guard + move verify …
HashWrangler Jul 2, 2026
78f8f16
fix(build-push-docker-manifest): read additional tags from action inp…
HashWrangler Jul 2, 2026
d485341
Merge branch 'main' into fix/cosign-verify-retry
HashWrangler Jul 2, 2026
9c15a99
refactor(build-push-docker-manifest): verify signature in its own job…
HashWrangler Jul 13, 2026
6b69057
refactor(verify): extract cosign verify+retry into verify-docker-sign…
HashWrangler Jul 13, 2026
6da27f9
refactor(verify): keep verification inline in the workflow job
HashWrangler Jul 13, 2026
153e045
refactor(verify): single isolated verify step, no in-step retry loop
HashWrangler Jul 13, 2026
ca4d6fb
feat(verify): intelligent verify retry — exponential backoff, 10 min cap
HashWrangler Jul 13, 2026
585dd63
fix(verify): bump verify-manifest-signature timeout to 12m
HashWrangler Jul 13, 2026
3cb5d96
docs(action): comment idempotent manifest create for reviewers
HashWrangler Jul 13, 2026
e1a99c9
fix(verify): include last cosign verify output on timeout
HashWrangler Jul 13, 2026
0609dea
fix(action): require platform os+arch in digest jq filter
HashWrangler Jul 13, 2026
746d6f3
Merge branch 'main' into fix/cosign-verify-retry
HashWrangler Jul 15, 2026
bd785a4
Debloat: reduce to a minimal idempotency guard (RANE-4683)
HashWrangler Jul 15, 2026
919f391
Potential fix for pull request finding
HashWrangler Jul 15, 2026
5bc47d0
Potential fix for pull request finding
HashWrangler Jul 15, 2026
646d28a
Potential fix for pull request finding
HashWrangler Jul 17, 2026
458daeb
Potential fix for pull request finding
HashWrangler Jul 17, 2026
15036e4
Merge branch 'main' into fix/cosign-verify-retry
HashWrangler Jul 17, 2026
9fbddca
Potential fix for pull request finding
HashWrangler Jul 17, 2026
ba27f71
Potential fix for pull request finding
HashWrangler Jul 17, 2026
6402ea1
fix(build-push-docker-manifest): repair idempotency guard
HashWrangler Jul 17, 2026
aeee35e
Potential fix for pull request finding
HashWrangler Jul 17, 2026
3c46d59
Potential fix for pull request finding
HashWrangler Jul 17, 2026
b3beb6f
Merge branch 'main' into fix/cosign-verify-retry
HashWrangler Jul 20, 2026
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
5 changes: 5 additions & 0 deletions .changeset/rane-4683-manifest-cosign-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"build-push-docker-manifest": minor
---

Harden manifest create and cosign sign/verify for idempotent build-publish reruns (RANE-4683): skip imagetools create when the tag already points at the expected platform digests, skip cosign sign when a valid signature is already present, and retry cosign verify after signing to absorb Sigstore propagation lag
167 changes: 159 additions & 8 deletions actions/build-push-docker-manifest/action.yml
Comment thread
HashWrangler marked this conversation as resolved.
Comment thread
HashWrangler marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ runs:
}}
registries: ${{ inputs.aws-account-number }}

- name: Ensure jq is installed
Comment thread
HashWrangler marked this conversation as resolved.
Outdated
shell: bash
run: |
if command -v jq >/dev/null 2>&1; then
echo "jq already installed: $(jq --version)"
else
echo "jq not found; installing..."
sudo apt-get update -qq
sudo apt-get install -y jq
jq --version
fi
Comment thread
HashWrangler marked this conversation as resolved.
Outdated

- name: Generate manifest annotations
id: generate-annotations
shell: bash
Expand Down Expand Up @@ -327,15 +339,102 @@ runs:
${{ steps.generate-annotations.outputs.annotation-flags }}
TAG_FLAGS: ${{ steps.process-additional-tags.outputs.tag-flags }}
run: |
set -euo pipefail

Comment thread
HashWrangler marked this conversation as resolved.
Comment thread
HashWrangler marked this conversation as resolved.
normalize_digest_csv() {
local input="$1"
IFS=',' read -ra _digests <<< "$input"
local normalized=()
for digest in "${_digests[@]}"; do
digest=$(echo "$digest" | xargs)
if [[ -n "$digest" ]]; then
normalized+=("$digest")
fi
done
Comment thread
HashWrangler marked this conversation as resolved.
Outdated
if [[ ${#normalized[@]} -eq 0 ]]; then
echo ""
return
fi
mapfile -t sorted < <(printf '%s\n' "${normalized[@]}" | sort)
local IFS=','
echo "${sorted[*]}"
Comment thread
Copilot marked this conversation as resolved.
Outdated
}

get_existing_platform_digests() {
local tag="$1"
local inspect_json inspect_stderr inspect_status

inspect_stderr=$(mktemp)
set +e
inspect_json=$(docker buildx imagetools inspect "${tag}" --format '{{json .}}' 2>"${inspect_stderr}")
inspect_status=$?
set -e

if [[ "${inspect_status}" -ne 0 ]]; then
if grep -qiE 'not found|manifest unknown|name unknown|404|does not exist|no such manifest' "${inspect_stderr}"; then
rm -f "${inspect_stderr}"
return 1
fi

echo "::error::Failed to inspect manifest tag ${tag}"
cat "${inspect_stderr}" >&2
rm -f "${inspect_stderr}"
exit 1
fi

rm -f "${inspect_stderr}"

echo "${inspect_json}" | jq -r '
.manifest.manifests[]?
| select(
((.platform.os // "") | ascii_downcase) != "unknown"
and ((.annotations."vnd.docker.reference.type" // "") != "attestation-manifest")
)
Comment thread
HashWrangler marked this conversation as resolved.
Outdated
| .digest
' | sort | paste -sd, -
Comment thread
Copilot marked this conversation as resolved.
Outdated
}

DOCKER_MANIFEST_NAME_WITH_TAG="${DOCKER_MANIFEST_NAME}:${DOCKER_MANIFEST_TAG}"
EXPECTED_DIGESTS=$(normalize_digest_csv "${DOCKER_IMAGE_NAME_DIGESTS}")

Comment thread
HashWrangler marked this conversation as resolved.
if [[ -z "${EXPECTED_DIGESTS}" ]]; then
echo "::error::docker-image-name-digests must contain at least one sha256 digest"
exit 1
fi

if EXISTING_DIGESTS=$(get_existing_platform_digests "${DOCKER_MANIFEST_NAME_WITH_TAG}"); then
echo "Found existing manifest for ${DOCKER_MANIFEST_NAME_WITH_TAG}"
echo " Expected platform digests: ${EXPECTED_DIGESTS}"
echo " Existing platform digests: ${EXISTING_DIGESTS}"

if [[ "${EXISTING_DIGESTS}" == "${EXPECTED_DIGESTS}" ]]; then
echo "✅ Manifest already exists with expected platform digests; skipping imagetools create (idempotent rerun)"
echo "manifest-create-skipped=true" | tee -a "${GITHUB_OUTPUT}"
exit 0
fi

echo "::error::Manifest tag ${DOCKER_MANIFEST_NAME_WITH_TAG} already exists with different platform digests"
echo "::error::Expected: ${EXPECTED_DIGESTS}"
echo "::error::Existing: ${EXISTING_DIGESTS}"
exit 1
fi

echo "manifest-create-skipped=false" | tee -a "${GITHUB_OUTPUT}"

# Convert comma-separated list into array and pass as separate arguments
IFS=',' read -ra DIGESTS <<< "$DOCKER_IMAGE_NAME_DIGESTS"
# Map each digest to include the manifest name
PREFIXED_DIGESTS=()
for digest in "${DIGESTS[@]}"; do
PREFIXED_DIGESTS+=("${DOCKER_MANIFEST_NAME}@${digest}")
digest=$(echo "$digest" | xargs)
[[ -n "$digest" ]] && PREFIXED_DIGESTS+=("${DOCKER_MANIFEST_NAME}@${digest}")
done

if [[ ${#PREFIXED_DIGESTS[@]} -eq 0 ]]; then
echo "::error::docker-image-name-digests must contain at least one sha256 digest"
exit 1
fi

# Create Docker manifest
echo "Creating Docker manifest with tag: ${DOCKER_MANIFEST_TAG}"

Expand Down Expand Up @@ -412,17 +511,38 @@ runs:

- name: Sign Docker Manifest using GH OIDC
if: inputs.docker-manifest-sign == 'true'
shell: sh
id: sign-docker-manifest
shell: bash
env:
MANIFEST_NAME_WITH_DIGEST:
${{ steps.inspect-docker-manifest.outputs.manifest-name-with-digest }}
run: cosign sign "${MANIFEST_NAME_WITH_DIGEST}" --yes
GITHUB_WORKFLOW_REPOSITORY: ${{ inputs.github-workflow-repository }}
OIDC_ISSUER: ${{ inputs.cosign-oidc-issuer }}
OIDC_IDENTITY_REGEXP: ${{ inputs.cosign-oidc-identity-regexp }}
run: |
set -euo pipefail

verify_manifest_signature() {
cosign verify "${MANIFEST_NAME_WITH_DIGEST}" \
--certificate-oidc-issuer "${OIDC_ISSUER}" \
--certificate-identity-regexp "${OIDC_IDENTITY_REGEXP}" \
--certificate-github-workflow-repository "${GITHUB_WORKFLOW_REPOSITORY}"
}

if [[ -n "${OIDC_IDENTITY_REGEXP}" ]] && verify_manifest_signature >/dev/null 2>&1; then
echo "✅ Manifest already signed with expected identity; skipping cosign sign (idempotent rerun)"
echo "manifest-sign-skipped=true" | tee -a "${GITHUB_OUTPUT}"
exit 0
fi

echo "manifest-sign-skipped=false" | tee -a "${GITHUB_OUTPUT}"
cosign sign "${MANIFEST_NAME_WITH_DIGEST}" --yes

- name: Verify Docker image signature
if:
inputs.docker-manifest-sign == 'true' &&
inputs.cosign-oidc-identity-regexp != ''
shell: sh
shell: bash
env:
MANIFEST_NAME_WITH_DIGEST: >-
${{
Expand All @@ -432,10 +552,30 @@ runs:
OIDC_ISSUER: ${{ inputs.cosign-oidc-issuer }}
OIDC_IDENTITY_REGEXP: ${{ inputs.cosign-oidc-identity-regexp }}
run: |
cosign verify "${MANIFEST_NAME_WITH_DIGEST}" \
--certificate-oidc-issuer "${OIDC_ISSUER}" \
--certificate-identity-regexp "${OIDC_IDENTITY_REGEXP}" \
--certificate-github-workflow-repository "${GITHUB_WORKFLOW_REPOSITORY}"
MAX_RETRIES=5
RETRY_DELAY=10
VERIFY_OK=false

for i in $(seq 1 $MAX_RETRIES); do
echo "Attempt ${i}/${MAX_RETRIES}: Verifying cosign signature for ${MANIFEST_NAME_WITH_DIGEST}..."

if cosign verify "${MANIFEST_NAME_WITH_DIGEST}" \
--certificate-oidc-issuer "${OIDC_ISSUER}" \
--certificate-identity-regexp "${OIDC_IDENTITY_REGEXP}" \
--certificate-github-workflow-repository "${GITHUB_WORKFLOW_REPOSITORY}"; then
echo "Successfully verified signature on attempt ${i}"
VERIFY_OK=true
break
fi

echo "Attempt ${i}/${MAX_RETRIES}: Signature not yet available, retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
Comment thread
HashWrangler marked this conversation as resolved.
Outdated
done

if [[ "$VERIFY_OK" != "true" ]]; then
echo "::error::Failed to verify cosign signature for ${MANIFEST_NAME_WITH_DIGEST} after ${MAX_RETRIES} attempts"
exit 1
fi

- name: Summary output
shell: bash
Expand All @@ -456,6 +596,11 @@ runs:
steps.inspect-docker-manifest.outputs.manifest-name-with-tag
}}
MANIFEST_TAG: ${{ inputs.docker-manifest-tag }}
MANIFEST_CREATE_SKIPPED:
${{ steps.create-push-docker-manifest.outputs.manifest-create-skipped
}}
Comment thread
HashWrangler marked this conversation as resolved.
Outdated
Comment thread
HashWrangler marked this conversation as resolved.
Outdated
MANIFEST_SIGN_SKIPPED:
${{ steps.sign-docker-manifest.outputs.manifest-sign-skipped }}
OIDC_ISSUER: ${{ inputs.cosign-oidc-issuer }}
OIDC_IDENTITY_REGEXP: ${{ inputs.cosign-oidc-identity-regexp }}
run: |
Expand All @@ -467,10 +612,16 @@ runs:
echo "Manifest additional tags: \`${MANIFEST_ADDITIONAL_TAGS:-None}\`" | tee -a "${GITHUB_STEP_SUMMARY}"
Comment thread
HashWrangler marked this conversation as resolved.
echo "Manifest name with tag: \`${MANIFEST_NAME_WITH_TAG}\`" | tee -a "${GITHUB_STEP_SUMMARY}"
echo "Manifest name with digest: \`${MANIFEST_NAME_WITH_DIGEST}\`" | tee -a "${GITHUB_STEP_SUMMARY}"
if [[ "${MANIFEST_CREATE_SKIPPED}" == "true" ]]; then
echo "Manifest create: skipped (existing tag already points at expected platform digests)" | tee -a "${GITHUB_STEP_SUMMARY}"
fi
if [[ "${DOCKER_MANIFEST_SIGNED}" == 'true' ]]; then
echo >> "${GITHUB_STEP_SUMMARY}"
echo "#### Docker Manifest signed 📝" | tee -a "${GITHUB_STEP_SUMMARY}"
echo "Manifest signed with cosign. To verify, run:" | tee -a "${GITHUB_STEP_SUMMARY}"
if [[ "${MANIFEST_SIGN_SKIPPED}" == "true" ]]; then
echo "Cosign sign: skipped (valid signature already present)" | tee -a "${GITHUB_STEP_SUMMARY}"
fi
echo "\`\`\`shell" >> "${GITHUB_STEP_SUMMARY}"
echo "cosign verify ${MANIFEST_NAME_WITH_DIGEST} --certificate-oidc-issuer ${OIDC_ISSUER} --certificate-identity-regexp '${OIDC_IDENTITY_REGEXP}' --certificate-github-workflow-repository ${GITHUB_WORKFLOW_REPOSITORY}" | tee -a "${GITHUB_STEP_SUMMARY}"
echo "\`\`\`" >> "${GITHUB_STEP_SUMMARY}"
Expand Down
Loading