diff --git a/.github/workflows/validate-release-assets.yml b/.github/workflows/validate-release-assets.yml new file mode 100644 index 0000000..3dd9d4a --- /dev/null +++ b/.github/workflows/validate-release-assets.yml @@ -0,0 +1,332 @@ +name: Validate Release Assets + +on: + release: + types: [created, edited] + +jobs: + validate: + # Only run for draft releases to act as a safety gate before publishing + if: github.event.release.draft == true + runs-on: ubuntu-latest + + permissions: + contents: write # Needed to delete assets and update release + issues: write # Needed to add labels and comments + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: | + TAG="${{ github.event.release.tag_name }}" + # Remove 'v' prefix if present + VERSION="${TAG#v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Validating release: $TAG (version: $VERSION)" + + - name: Download all release assets + id: download + run: | + echo "Downloading all assets from release ${{ steps.version.outputs.tag }}..." + + # Create release directory + mkdir -p release + cd release + + # Get list of all assets for this release + ASSETS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets" \ + | jq -r '.[].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" + + # Download each asset + echo "$ASSETS" | while read -r url; do + if [ -n "$url" ]; then + filename=$(basename "$url") + echo "Downloading $filename..." + curl -L -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/octet-stream" \ + -o "$filename" "$url" + + 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 + run: | + # Docker is pre-installed on ubuntu-latest runners + docker --version + + - name: Pull Docker image (if available) + id: docker + continue-on-error: true + run: | + IMAGE="rcourtman/pulse:${{ steps.version.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 + id: validate + run: | + set +e # Don't exit on error, we want to capture the output + + echo "Running validation script..." + chmod +x scripts/validate-release.sh + + # Create output file for validation results + OUTPUT_FILE=$(mktemp) + + if [ "${{ steps.docker.outputs.image_available }}" = "true" ]; then + # Full validation with Docker image + echo "Running full validation (Docker + assets)..." + scripts/validate-release.sh \ + "${{ steps.version.outputs.version }}" \ + "${{ steps.docker.outputs.image }}" \ + "release" 2>&1 | tee "$OUTPUT_FILE" + else + # Modified validation script that skips Docker checks + echo "Running assets-only validation (Docker image not available)..." + + # We'll need to skip Docker validation - create a temporary modified script + # For now, we'll just run the script and let Docker validation fail + # A more robust solution would be to modify the script or create a separate assets-only version + scripts/validate-release.sh \ + "${{ steps.version.outputs.version }}" \ + "rcourtman/pulse:${{ steps.version.outputs.tag }}" \ + "release" 2>&1 | tee "$OUTPUT_FILE" || true + fi + + VALIDATION_EXIT_CODE=$? + + # Save output for later use in comments + echo "VALIDATION_OUTPUT<> $GITHUB_OUTPUT + cat "$OUTPUT_FILE" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Check validation result + 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.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/${{ github.event.release.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.validate.outputs.validation_passed == 'true' + run: | + echo "✅ Validation passed - updating release description" + + # Get current release body + CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \ + | jq -r '.body // ""') + + # Remove any existing validation status block + CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '//,//d') + + # Create new validation status block using heredoc + read -r -d '' VALIDATION_BLOCK <<'EOF' || true + + ## ✅ 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 ✓ + + + EOF + + # Replace placeholders + 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 }})}" + + # Combine validation block with original body + NEW_BODY="$VALIDATION_BLOCK + + $CURRENT_BODY" + + # Update release + curl -X PATCH \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \ + -d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')" + + - name: Set commit status - Failure + if: 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/${{ github.event.release.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: failure() || steps.validate.outputs.validation_passed == 'false' + run: | + echo "❌ Validation failed - deleting all release assets" + + # Delete all assets from the release + ASSET_IDS=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.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" + + if [ $? -eq 0 ]; then + echo "✓ Deleted asset $asset_id" + else + echo "⚠️ Failed to delete asset $asset_id" + fi + fi + done + echo "✓ Asset deletion process completed" + else + echo "No assets to delete" + fi + + - name: Update release body - Failure + if: failure() || steps.validate.outputs.validation_passed == 'false' + run: | + echo "❌ Validation failed - updating release description" + + # Get current release body + CURRENT_BODY=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \ + | jq -r '.body // ""') + + # Remove any existing validation status block + CURRENT_BODY=$(echo "$CURRENT_BODY" | sed '//,//d') + + # Extract validation errors if available + 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 + + # Create new validation status block using heredoc + read -r -d '' VALIDATION_BLOCK <<'EOF' || true + + ## ❌ 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) + + + EOF + + # Replace placeholders + 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}" + + # Combine validation block with original body + NEW_BODY="$VALIDATION_BLOCK + + $CURRENT_BODY" + + # Update release + curl -X PATCH \ + -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}" \ + -d "$(jq -n --arg body "$NEW_BODY" '{body: $body}')" + + - name: Fail the workflow + if: failure() || steps.validate.outputs.validation_passed == 'false' + run: | + echo "::error::Release asset validation failed. All assets have been deleted." + exit 1