The Python heredoc was not indented, causing YAML parsers to interpret the Python code as YAML syntax. This caused workflow_dispatch runs to fail instantly with 'workflow file issue' error before any jobs could start. The fix indents the heredoc content and changes delimiter from 'PY' to 'EOF' to match standard conventions.
381 lines
15 KiB
YAML
381 lines
15 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
|
|
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
|
|
|
|
jobs:
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
issues: write
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- 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, 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()
|
|
|
|
if not result["tag"] or not result["release_id"]:
|
|
sys.stderr.write("::error::Release metadata is missing. Provide tag, version, release_id, and target_commitish.\n")
|
|
sys.exit(1)
|
|
|
|
should_run = "true"
|
|
if event_name == "release" and result["draft"] != "true":
|
|
should_run = "false"
|
|
result["should_run"] = should_run
|
|
|
|
for key, value in result.items():
|
|
print(f"{key}={value}")
|
|
EOF
|
|
cat context.env >> "$GITHUB_OUTPUT"
|
|
cat context.env
|
|
|
|
- name: Skip validation for published releases
|
|
if: steps.context.outputs.should_run != 'true'
|
|
run: echo "Release is already published; skipping validation checks."
|
|
|
|
- name: Download all release assets
|
|
if: steps.context.outputs.should_run == 'true'
|
|
id: download
|
|
run: |
|
|
echo "Downloading all assets from release ${{ steps.context.outputs.tag }}..."
|
|
|
|
mkdir -p release
|
|
cd release
|
|
|
|
ASSETS=$(gh api "repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/assets" --jq '.[].browser_download_url')
|
|
|
|
if [ -z "$ASSETS" ]; 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 assets:"
|
|
echo "$ASSETS"
|
|
|
|
echo "$ASSETS" | while read -r url; do
|
|
if [ -n "$url" ]; then
|
|
filename=$(basename "$url")
|
|
echo "Downloading $filename..."
|
|
gh release download "${{ steps.context.outputs.tag }}" \
|
|
--pattern "$filename" --dir . --clobber
|
|
|
|
if [ $? -eq 0 ]; then
|
|
echo "✓ Downloaded $filename ($(du -h "$filename" | cut -f1))"
|
|
else
|
|
echo "::error::Failed to download $filename"
|
|
exit 1
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "All assets downloaded:"
|
|
ls -lh
|
|
|
|
- name: Install Docker
|
|
if: steps.context.outputs.should_run == 'true'
|
|
run: docker --version
|
|
|
|
- name: Pull Docker image (if available)
|
|
if: steps.context.outputs.should_run == 'true'
|
|
id: docker
|
|
continue-on-error: true
|
|
run: |
|
|
IMAGE="rcourtman/pulse:${{ steps.context.outputs.tag }}"
|
|
echo "Attempting to pull Docker image: $IMAGE"
|
|
|
|
if docker pull "$IMAGE" 2>/dev/null; then
|
|
echo "✓ Docker image available: $IMAGE"
|
|
echo "image_available=true" >> $GITHUB_OUTPUT
|
|
echo "image=$IMAGE" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "⚠️ Docker image not yet available: $IMAGE"
|
|
echo "⚠️ Will skip Docker image validation"
|
|
echo "image_available=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Run validation script
|
|
if: steps.context.outputs.should_run == 'true'
|
|
id: validate
|
|
run: |
|
|
set +e
|
|
echo "Running validation script..."
|
|
chmod +x scripts/validate-release.sh
|
|
OUTPUT_FILE=$(mktemp)
|
|
|
|
if [ "${{ steps.docker.outputs.image_available }}" = "true" ]; then
|
|
echo "Running full validation (Docker + assets)..."
|
|
scripts/validate-release.sh \
|
|
"${{ steps.context.outputs.version }}" \
|
|
"${{ steps.docker.outputs.image }}" \
|
|
"release" 2>&1 | tee "$OUTPUT_FILE"
|
|
else
|
|
echo "Running assets-only validation (Docker image not available)..."
|
|
scripts/validate-release.sh \
|
|
"${{ steps.context.outputs.version }}" \
|
|
"rcourtman/pulse:${{ steps.context.outputs.tag }}" \
|
|
"release" 2>&1 | tee "$OUTPUT_FILE" || true
|
|
fi
|
|
|
|
VALIDATION_EXIT_CODE=$?
|
|
|
|
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.validate.outputs.validation_passed == 'true'
|
|
run: |
|
|
curl -X POST \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ steps.context.outputs.target_commitish }}" \
|
|
-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.validate.outputs.validation_passed == 'true'
|
|
run: |
|
|
echo "✅ Validation passed - updating release description"
|
|
|
|
CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
|
| jq -r '.body // ""')
|
|
|
|
CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '/<!-- VALIDATION_STATUS_START -->/,/<!-- VALIDATION_STATUS_END -->/d')
|
|
|
|
read -r -d '' VALIDATION_BLOCK <<'EOF' || true
|
|
<!-- VALIDATION_STATUS_START -->
|
|
## ✅ Release Asset Validation: PASSED
|
|
|
|
All release assets have been validated successfully!
|
|
|
|
**Status**: Ready for publication ✅
|
|
**Validated**: TIMESTAMP_PLACEHOLDER
|
|
**Workflow**: WORKFLOW_LINK_PLACEHOLDER
|
|
|
|
### Validation Summary
|
|
- All required assets present ✓
|
|
- Checksums verified ✓
|
|
- Version strings correct ✓
|
|
- Binary architectures validated ✓
|
|
|
|
<!-- VALIDATION_STATUS_END -->
|
|
EOF
|
|
|
|
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 }})}"
|
|
|
|
NEW_BODY="$VALIDATION_BLOCK
|
|
|
|
$CURRENT_BODY"
|
|
|
|
curl -X PATCH \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
|
-d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')"
|
|
|
|
- name: Set commit status - Failure
|
|
if: steps.context.outputs.should_run == 'true' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
|
run: |
|
|
curl -X POST \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"https://api.github.com/repos/${{ github.repository }}/statuses/${{ steps.context.outputs.target_commitish }}" \
|
|
-d '{
|
|
"state": "failure",
|
|
"target_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
|
|
"description": "Release asset validation failed - assets deleted",
|
|
"context": "Release Asset Validation"
|
|
}'
|
|
|
|
- name: Delete all release assets on failure
|
|
if: steps.context.outputs.should_run == 'true' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
|
run: |
|
|
echo "❌ Validation failed - deleting all release assets"
|
|
|
|
ASSET_IDS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}/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 ${{ secrets.GITHUB_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' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
|
run: |
|
|
echo "❌ Validation failed - updating release description"
|
|
|
|
CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
|
| jq -r '.body // ""')
|
|
|
|
CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '/<!-- VALIDATION_STATUS_START -->/,/<!-- VALIDATION_STATUS_END -->/d')
|
|
|
|
VALIDATION_OUTPUT="${{ steps.validate.outputs.VALIDATION_OUTPUT }}"
|
|
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 -->
|
|
## ❌ Release Asset Validation: FAILED
|
|
|
|
**Critical**: Release assets failed validation checks. All assets have been deleted to prevent publishing an invalid release.
|
|
|
|
**Status**: ⛔ DO NOT PUBLISH until validation passes
|
|
**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//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}"
|
|
|
|
NEW_BODY="$VALIDATION_BLOCK
|
|
|
|
$CURRENT_BODY"
|
|
|
|
curl -X PATCH \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.context.outputs.release_id }}" \
|
|
-d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')"
|
|
|
|
- name: Fail the workflow
|
|
if: steps.context.outputs.should_run == 'true' && (failure() || steps.validate.outputs.validation_passed == 'false')
|
|
run: |
|
|
echo "::error::Release asset validation failed. All assets have been deleted."
|
|
exit 1
|