Files
pulse/.github/workflows/validate-release-assets.yml
pulse-triage[bot] 6998908d2a fix(release): limit asset validation readiness claims
Downstream release-note syndication repeats the asset check banner even when installed health or release convergence is not qualified. Report asset checks only and state the remaining evidence boundaries for both draft and post-publication banners.

Change-source: pulse-maintainer
2026-09-05 17:30:49 +01:00

662 lines
29 KiB
YAML

name: Validate Release Assets
on:
workflow_call:
inputs:
tag:
description: 'Release tag (e.g., v4.29.0)'
required: true
type: string
version:
description: 'Version number without v prefix (e.g., 4.29.0)'
required: true
type: string
release_id:
description: 'GitHub release ID'
required: true
type: string
draft:
description: 'Whether the release is still a draft'
required: true
type: boolean
target_commitish:
description: 'Commit SHA associated with the release'
required: true
type: string
candidate_manifest_artifact:
description: 'Same-run immutable candidate manifest artifact for fast digest verification'
required: false
default: ''
type: string
release:
types: [edited]
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., v4.29.0)'
required: true
type: string
version:
description: 'Version number without v prefix (e.g., 4.29.0)'
required: true
type: string
release_id:
description: 'GitHub release ID'
required: true
type: string
draft:
description: 'Set to true to run against a draft release'
required: true
type: boolean
target_commitish:
description: 'Commit SHA associated with the release'
required: true
type: string
candidate_manifest_artifact:
description: 'Optional immutable candidate manifest artifact name'
required: false
default: ''
type: string
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: write
issues: write
statuses: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Determine release context
id: context
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TAG: ${{ inputs.tag }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_RELEASE_ID: ${{ inputs.release_id }}
INPUT_DRAFT: ${{ inputs.draft }}
INPUT_COMMIT: ${{ inputs.target_commitish }}
run: |
python3 <<'EOF' > context.env
import json, os, re, sys
event_name = os.environ.get("EVENT_NAME", "")
result = {}
if event_name == "release":
with open(os.environ["GITHUB_EVENT_PATH"], "r", encoding="utf-8") as handle:
data = json.load(handle)
release = data.get("release") or {}
result["tag"] = release.get("tag_name", "")
tag = result["tag"]
result["version"] = tag[1:] if tag.startswith("v") else tag
result["release_id"] = str(release.get("id", ""))
result["target_commitish"] = release.get("target_commitish", "")
result["draft"] = str(release.get("draft", False)).lower()
else:
result["tag"] = os.environ.get("INPUT_TAG", "")
result["version"] = os.environ.get("INPUT_VERSION", "")
result["release_id"] = os.environ.get("INPUT_RELEASE_ID", "")
result["target_commitish"] = os.environ.get("INPUT_COMMIT", "")
draft_value = os.environ.get("INPUT_DRAFT", "false")
result["draft"] = str(draft_value).lower()
version_pattern = r"[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|alpha|beta)\.[0-9]+)?"
valid = (
isinstance(result["tag"], str)
and isinstance(result["version"], str)
and isinstance(result["target_commitish"], str)
and re.fullmatch(f"v{version_pattern}", result["tag"])
and re.fullmatch(version_pattern, result["version"])
and result["tag"] == f"v{result['version']}"
and re.fullmatch(r"[0-9]+", result["release_id"])
and re.fullmatch(r"[0-9a-f]{40}", result["target_commitish"])
and result["draft"] in {"true", "false"}
)
if not valid:
sys.stderr.write("::error::Release metadata must contain one exact tag, version, release ID, source commit, and draft state.\n")
sys.exit(1)
result["should_run"] = "true"
for key, value in result.items():
print(f"{key}={value}")
EOF
cat context.env >> "$GITHUB_OUTPUT"
cat context.env
- name: Validate release body integrity
id: body_integrity
if: steps.context.outputs.should_run == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.version }}
run: |
set -euo pipefail
RELEASE_JSON_FILE=$(mktemp)
RELEASE_BODY_FILE=$(mktemp)
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
> "$RELEASE_JSON_FILE"
jq -r '.body // ""' "$RELEASE_JSON_FILE" > "$RELEASE_BODY_FILE"
python3 scripts/release_control/render_release_body.py \
--version "${WORKFLOW_OUTPUT_2}" \
--validate-body-file "$RELEASE_BODY_FILE"
- name: Quarantine malformed release body
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'failure'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.tag }}
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.target_commitish }}
WORKFLOW_OUTPUT_3: ${{ steps.context.outputs.release_id }}
run: |
set -euo pipefail
PATCH_PAYLOAD=$(mktemp)
RELEASE_JSON_FILE=$(mktemp)
jq -n \
--arg tag "${WORKFLOW_OUTPUT_1}" \
--arg target_commitish "${WORKFLOW_OUTPUT_2}" \
'{draft: true, tag_name: $tag, target_commitish: $target_commitish}' \
> "$PATCH_PAYLOAD"
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_3}" \
-X PATCH \
--input "$PATCH_PAYLOAD" > "$RELEASE_JSON_FILE"
ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$RELEASE_JSON_FILE")
ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$RELEASE_JSON_FILE")
ACTUAL_DRAFT=$(jq -r '.draft // false' "$RELEASE_JSON_FILE")
if [ "$ACTUAL_RELEASE_TAG" != "${WORKFLOW_OUTPUT_1}" ]; then
echo "::error::Body-integrity quarantine detached release tag ${ACTUAL_RELEASE_TAG}; expected ${WORKFLOW_OUTPUT_1}."
exit 1
fi
if [ "$ACTUAL_TARGET_COMMITISH" != "${WORKFLOW_OUTPUT_2}" ]; then
echo "::error::Body-integrity quarantine changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${WORKFLOW_OUTPUT_2}."
exit 1
fi
if [ "$ACTUAL_DRAFT" != "true" ]; then
echo "::error::Malformed release body was not quarantined as a draft."
exit 1
fi
curl --fail-with-body --silent --show-error -X POST \
-H "Authorization: token $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${WORKFLOW_OUTPUT_2}" \
-d '{
"state": "failure",
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"description": "Release body integrity failed",
"context": "Release Asset Validation"
}'
- name: Download immutable candidate manifest
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && inputs.candidate_manifest_artifact != ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.candidate_manifest_artifact }}
path: release-candidate-manifest
- name: Fetch release asset metadata
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
run: |
set -euo pipefail
gh api --paginate \
"repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}/assets?per_page=100" \
--slurp > "$RUNNER_TEMP/release-assets.json"
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
--jq '.body // ""' > "$RUNNER_TEMP/release-body.md"
count="$(jq '[.[][]] | length' "$RUNNER_TEMP/release-assets.json")"
if [ "$count" -eq 0 ]; then
echo "::error::No assets found in release"
exit 1
fi
echo "Found ${count} published release assets."
- name: Download all release assets for legacy validation
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && inputs.candidate_manifest_artifact == ''
id: download
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.tag }}
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.release_id }}
run: |
echo "Downloading all assets from release ${WORKFLOW_OUTPUT_1}..."
mkdir -p release
cd release
# Get asset info (id and name) - API works with GITHUB_TOKEN for draft releases
# Use --paginate to handle releases with >30 assets
ASSETS_JSON=$(gh api --paginate "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_2}/assets")
if [ "$(echo "$ASSETS_JSON" | jq '. | length')" -eq 0 ]; then
echo "::error::No assets found in release"
echo "has_assets=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "has_assets=true" >> $GITHUB_OUTPUT
echo "Found $(echo "$ASSETS_JSON" | jq '. | length') assets"
# Download each asset using the API (works with draft releases)
echo "$ASSETS_JSON" | jq -r '.[] | "\(.id) \(.name)"' | while read -r asset_id filename; do
echo "Downloading $filename (ID: $asset_id)..."
# Use GitHub API to download with authentication
curl -L -H "Authorization: token $GH_TOKEN" \
-H "Accept: application/octet-stream" \
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id" \
-o "$filename"
if [ $? -eq 0 ] && [ -f "$filename" ]; then
echo "✓ Downloaded $filename ($(du -h "$filename" | cut -f1))"
else
echo "::error::Failed to download $filename"
exit 1
fi
done
echo ""
echo "All assets downloaded:"
ls -lh
- name: Install Docker
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && inputs.candidate_manifest_artifact == ''
run: docker --version
- name: Pull Docker image (with retry logic)
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && inputs.candidate_manifest_artifact == ''
id: docker
continue-on-error: true
env:
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.tag }}
run: |
IMAGE="rcourtman/pulse:${WORKFLOW_OUTPUT_1}"
echo "Attempting to pull Docker image: $IMAGE"
echo "Docker Hub CDN propagation can take 2-5 minutes after push..."
echo ""
# Retry logic: 10 attempts with exponential backoff
# Total time: ~10 minutes max
MAX_ATTEMPTS=10
ATTEMPT=1
WAIT_TIME=30
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
echo "Attempt $ATTEMPT/$MAX_ATTEMPTS: Pulling image..."
if docker pull "$IMAGE" 2>/dev/null; then
echo "✓ Docker image available: $IMAGE"
echo "image_available=true" >> $GITHUB_OUTPUT
python3 scripts/write_github_output.py image "$IMAGE"
exit 0
fi
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
echo "⚠️ Image not yet available, waiting ${WAIT_TIME}s before retry..."
sleep $WAIT_TIME
WAIT_TIME=$((WAIT_TIME * 2)) # Exponential backoff
if [ $WAIT_TIME -gt 120 ]; then
WAIT_TIME=120 # Cap at 2 minutes
fi
fi
ATTEMPT=$((ATTEMPT + 1))
done
echo "⚠️ Docker image not available after $MAX_ATTEMPTS attempts"
echo "⚠️ Will skip Docker image validation"
echo "image_available=false" >> $GITHUB_OUTPUT
- name: Run validation script
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success'
id: validate
env:
CANDIDATE_MANIFEST_ARTIFACT: ${{ inputs.candidate_manifest_artifact }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.version }}
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.target_commitish }}
WORKFLOW_OUTPUT_3: ${{ steps.docker.outputs.image_available }}
WORKFLOW_OUTPUT_4: ${{ steps.docker.outputs.image }}
run: |
set +e
set -o pipefail
echo "Running validation script..."
OUTPUT_FILE=$(mktemp)
if [ -n "${CANDIDATE_MANIFEST_ARTIFACT}" ]; then
echo "Validating GitHub's stored SHA-256 digests against the immutable candidate manifest..."
python3 scripts/release_candidate_manifest.py verify-release \
--manifest release-candidate-manifest/release-candidate.json \
--assets-json "$RUNNER_TEMP/release-assets.json" \
--version "${WORKFLOW_OUTPUT_1}" \
--source-sha "${WORKFLOW_OUTPUT_2}" \
--release-body-file "$RUNNER_TEMP/release-body.md" 2>&1 | tee "$OUTPUT_FILE"
elif [ "${WORKFLOW_OUTPUT_3}" = "true" ]; then
echo "Running full validation (Docker + assets)..."
scripts/validate-release.sh \
"${WORKFLOW_OUTPUT_1}" \
"${WORKFLOW_OUTPUT_4}" \
"release" 2>&1 | tee "$OUTPUT_FILE"
else
echo "Running assets-only validation (Docker image not available)..."
scripts/validate-release.sh \
"${WORKFLOW_OUTPUT_1}" \
--skip-docker \
"release" 2>&1 | tee "$OUTPUT_FILE"
fi
VALIDATION_EXIT_CODE=${PIPESTATUS[0]}
echo "VALIDATION_OUTPUT<<EOF" >> $GITHUB_OUTPUT
cat "$OUTPUT_FILE" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
if [ $VALIDATION_EXIT_CODE -eq 0 ]; then
echo "validation_passed=true" >> $GITHUB_OUTPUT
echo "✅ Validation PASSED"
else
echo "validation_passed=false" >> $GITHUB_OUTPUT
echo "❌ Validation FAILED"
fi
exit $VALIDATION_EXIT_CODE
- name: Set commit status - Success
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && steps.validate.outputs.validation_passed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.target_commitish }}
run: |
curl --fail-with-body --silent --show-error -X POST \
-H "Authorization: token $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${WORKFLOW_OUTPUT_1}" \
-d '{
"state": "success",
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"description": "All release assets validated successfully",
"context": "Release Asset Validation"
}'
- name: Update release body - Success
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'success' && steps.validate.outputs.validation_passed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.draft }}
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.release_id }}
WORKFLOW_OUTPUT_3: ${{ steps.context.outputs.version }}
WORKFLOW_OUTPUT_4: ${{ steps.context.outputs.tag }}
WORKFLOW_OUTPUT_5: ${{ steps.context.outputs.target_commitish }}
run: |
set -euo pipefail
echo "✅ Validation passed - updating release description"
INITIAL_STATE="${WORKFLOW_OUTPUT_1}"
if [ "$INITIAL_STATE" = "true" ]; then
HEADER_LINE="## ✅ Release Asset Validation: PASSED"
STATUS_LINE="**Status**: Release asset checks passed ✅"
INTRO_LINE="The required release asset checks passed."
else
HEADER_LINE="## ✅ Release Asset Validation (Post-Publish): PASSED"
STATUS_LINE="**Status**: Live release assets re-validated ✅"
INTRO_LINE="Assets were revalidated after publication due to a release edit."
fi
CURRENT_RELEASE_JSON=$(mktemp)
CURRENT_BODY_FILE=$(mktemp)
CLEAN_BODY_FILE=$(mktemp)
NEW_BODY_FILE=$(mktemp)
PATCH_PAYLOAD=$(mktemp)
PATCH_RESPONSE=$(mktemp)
PATCHED_BODY_FILE=$(mktemp)
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_2}" \
> "$CURRENT_RELEASE_JSON"
jq -r '.body // ""' "$CURRENT_RELEASE_JSON" > "$CURRENT_BODY_FILE"
python3 scripts/release_control/render_release_body.py \
--version "${WORKFLOW_OUTPUT_3}" \
--validate-body-file "$CURRENT_BODY_FILE" \
--output "$CLEAN_BODY_FILE"
read -r -d '' VALIDATION_BLOCK <<'EOF' || true
<!-- VALIDATION_STATUS_START -->
HEADER_PLACEHOLDER
INTRO_PLACEHOLDER
STATUS_PLACEHOLDER
**Validated**: TIMESTAMP_PLACEHOLDER
**Workflow**: WORKFLOW_LINK_PLACEHOLDER
### Validation Summary
- All required assets present ✓
- Checksums verified ✓
- Version strings correct ✓
- Binary architectures validated ✓
Asset validation alone does not establish installed-service health,
release convergence, clean soak or approval for stable publication.
<!-- VALIDATION_STATUS_END -->
EOF
VALIDATION_BLOCK="${VALIDATION_BLOCK//HEADER_PLACEHOLDER/$HEADER_LINE}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//STATUS_PLACEHOLDER/$STATUS_LINE}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//INTRO_PLACEHOLDER/$INTRO_LINE}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//TIMESTAMP_PLACEHOLDER/$(date -u +"%Y-%m-%d %H:%M:%S UTC")}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//WORKFLOW_LINK_PLACEHOLDER/[${{ github.workflow }} #${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})}"
printf '%s\n\n' "$VALIDATION_BLOCK" > "$NEW_BODY_FILE"
cat "$CLEAN_BODY_FILE" >> "$NEW_BODY_FILE"
jq -n \
--rawfile body "$NEW_BODY_FILE" \
--arg tag "${WORKFLOW_OUTPUT_4}" \
--arg target_commitish "${WORKFLOW_OUTPUT_5}" \
'{body: $body, tag_name: $tag, target_commitish: $target_commitish}' \
> "$PATCH_PAYLOAD"
curl --fail-with-body --silent --show-error -X PATCH \
-H "Authorization: token $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_2}" \
--data-binary "@$PATCH_PAYLOAD" > "$PATCH_RESPONSE"
ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$PATCH_RESPONSE")
ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$PATCH_RESPONSE")
if [ "$ACTUAL_RELEASE_TAG" != "${WORKFLOW_OUTPUT_4}" ]; then
echo "::error::Validation release body update detached release tag ${ACTUAL_RELEASE_TAG}; expected ${WORKFLOW_OUTPUT_4}."
exit 1
fi
if [ "$ACTUAL_TARGET_COMMITISH" != "${WORKFLOW_OUTPUT_5}" ]; then
echo "::error::Validation release body update changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${WORKFLOW_OUTPUT_5}."
exit 1
fi
jq -r '.body // ""' "$PATCH_RESPONSE" > "$PATCHED_BODY_FILE"
python3 scripts/release_control/render_release_body.py \
--version "${WORKFLOW_OUTPUT_3}" \
--validate-body-file "$PATCHED_BODY_FILE" \
--expected-body-file "$CLEAN_BODY_FILE"
- name: Set commit status - Failure
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.target_commitish }}
run: |
curl --fail-with-body --silent --show-error -X POST \
-H "Authorization: token $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/statuses/${WORKFLOW_OUTPUT_1}" \
-d '{
"state": "failure",
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
"description": "Release asset validation failed",
"context": "Release Asset Validation"
}'
- name: Delete all release assets on failure
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
run: |
echo "❌ Validation failed - deleting all release assets"
ASSET_IDS=$(curl -s -H "Authorization: token $GH_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}/assets" \
| jq -r '.[].id')
if [ -n "$ASSET_IDS" ]; then
echo "$ASSET_IDS" | while read -r asset_id; do
if [ -n "$asset_id" ] && [ "$asset_id" != "null" ]; then
echo "Deleting asset ID: $asset_id"
curl -X DELETE \
-H "Authorization: token $GH_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/releases/assets/$asset_id"
fi
done
echo "✓ Asset deletion process completed"
else
echo "No assets to delete"
fi
- name: Update release body - Failure
if: steps.context.outputs.should_run == 'true' && steps.context.outputs.draft == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WORKFLOW_OUTPUT_1: ${{ steps.context.outputs.release_id }}
WORKFLOW_OUTPUT_2: ${{ steps.context.outputs.version }}
WORKFLOW_OUTPUT_3: ${{ steps.validate.outputs.VALIDATION_OUTPUT }}
WORKFLOW_OUTPUT_4: ${{ steps.context.outputs.tag }}
WORKFLOW_OUTPUT_5: ${{ steps.context.outputs.target_commitish }}
run: |
set -euo pipefail
echo "❌ Validation failed - updating release description"
HEADER_LINE="## ❌ Release Asset Validation: FAILED"
STATUS_LINE="**Status**: ⛔ Draft release blocked until validation passes"
INTRO_LINE="Release assets failed validation checks. All assets have been deleted to prevent publishing an invalid release."
CURRENT_RELEASE_JSON=$(mktemp)
CURRENT_BODY_FILE=$(mktemp)
CLEAN_BODY_FILE=$(mktemp)
NEW_BODY_FILE=$(mktemp)
PATCH_PAYLOAD=$(mktemp)
PATCH_RESPONSE=$(mktemp)
PATCHED_BODY_FILE=$(mktemp)
gh api "repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
> "$CURRENT_RELEASE_JSON"
jq -r '.body // ""' "$CURRENT_RELEASE_JSON" > "$CURRENT_BODY_FILE"
python3 scripts/release_control/render_release_body.py \
--version "${WORKFLOW_OUTPUT_2}" \
--validate-body-file "$CURRENT_BODY_FILE" \
--output "$CLEAN_BODY_FILE"
VALIDATION_OUTPUT="${WORKFLOW_OUTPUT_3}"
if [ -n "$VALIDATION_OUTPUT" ]; then
ERRORS=$(echo "$VALIDATION_OUTPUT" | grep -i '\[ERROR\]' | head -20 || echo "See workflow logs for details")
else
ERRORS="Validation script failed to run. Check workflow logs."
fi
read -r -d '' VALIDATION_BLOCK <<'EOF' || true
<!-- VALIDATION_STATUS_START -->
HEADER_PLACEHOLDER
INTRO_PLACEHOLDER
STATUS_PLACEHOLDER
**Failed**: TIMESTAMP_PLACEHOLDER
**Workflow**: WORKFLOW_LINK_PLACEHOLDER
### What Happened
The automated validation process detected issues with the uploaded release assets. This could be due to:
- Missing required files
- Checksum mismatches
- Incorrect version strings in binaries
- Corrupted or incomplete uploads
### Action Required
1. Review the validation errors in the workflow logs
2. Fix the underlying issues in the release build process
3. Re-upload the corrected assets
4. Validation will run automatically when assets are edited
### Validation Errors (first 20 lines)
```
ERRORS_PLACEHOLDER
```
[View full workflow logs for complete details](WORKFLOW_URL_PLACEHOLDER)
<!-- VALIDATION_STATUS_END -->
EOF
VALIDATION_BLOCK="${VALIDATION_BLOCK//HEADER_PLACEHOLDER/$HEADER_LINE}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//STATUS_PLACEHOLDER/$STATUS_LINE}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//INTRO_PLACEHOLDER/$INTRO_LINE}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//TIMESTAMP_PLACEHOLDER/$(date -u +"%Y-%m-%d %H:%M:%S UTC")}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//WORKFLOW_LINK_PLACEHOLDER/[${{ github.workflow }} #${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//WORKFLOW_URL_PLACEHOLDER/${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}}"
VALIDATION_BLOCK="${VALIDATION_BLOCK//ERRORS_PLACEHOLDER/$ERRORS}"
printf '%s\n\n' "$VALIDATION_BLOCK" > "$NEW_BODY_FILE"
cat "$CLEAN_BODY_FILE" >> "$NEW_BODY_FILE"
jq -n \
--rawfile body "$NEW_BODY_FILE" \
--arg tag "${WORKFLOW_OUTPUT_4}" \
--arg target_commitish "${WORKFLOW_OUTPUT_5}" \
'{body: $body, tag_name: $tag, target_commitish: $target_commitish}' \
> "$PATCH_PAYLOAD"
curl --fail-with-body --silent --show-error -X PATCH \
-H "Authorization: token $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/${WORKFLOW_OUTPUT_1}" \
--data-binary "@$PATCH_PAYLOAD" > "$PATCH_RESPONSE"
ACTUAL_RELEASE_TAG=$(jq -r '.tag_name // empty' "$PATCH_RESPONSE")
ACTUAL_TARGET_COMMITISH=$(jq -r '.target_commitish // empty' "$PATCH_RESPONSE")
if [ "$ACTUAL_RELEASE_TAG" != "${WORKFLOW_OUTPUT_4}" ]; then
echo "::error::Validation failure body update detached release tag ${ACTUAL_RELEASE_TAG}; expected ${WORKFLOW_OUTPUT_4}."
exit 1
fi
if [ "$ACTUAL_TARGET_COMMITISH" != "${WORKFLOW_OUTPUT_5}" ]; then
echo "::error::Validation failure body update changed target_commitish ${ACTUAL_TARGET_COMMITISH}; expected ${WORKFLOW_OUTPUT_5}."
exit 1
fi
jq -r '.body // ""' "$PATCH_RESPONSE" > "$PATCHED_BODY_FILE"
python3 scripts/release_control/render_release_body.py \
--version "${WORKFLOW_OUTPUT_2}" \
--validate-body-file "$PATCHED_BODY_FILE" \
--expected-body-file "$CLEAN_BODY_FILE"
- name: Fail the workflow
if: steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'success' && (failure() || steps.validate.outputs.validation_passed == 'false')
run: |
echo "::error::Release asset validation failed. Draft assets are deleted; published releases remain immutable for explicit remediation."
exit 1
- name: Fail malformed release body
if: always() && steps.context.outputs.should_run == 'true' && steps.body_integrity.outcome == 'failure'
run: |
echo "::error::Release body integrity failed. Draft releases are quarantined; published releases remain immutable for explicit remediation."
exit 1