Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
165 changes: 165 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# =============================================================================
# Publish Release — finalizes a multi-platform release
# =============================================================================
# Waits for Release (Linux), Release (macOS), and Release (Windows) to all
# complete successfully on the same tag, then:
# 1. Generates SLSA provenance attestations for all artifacts
# 2. Publishes the draft release (draft: false)
#
# Prerequisites: All 3 platform workflows must upload with draft: true.
# This workflow is the ONLY one that flips draft to false.
# =============================================================================

name: Publish Release

on:
push:
tags:
- 'v[0-9]*.*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to publish (must have draft release with assets)'
required: true

permissions:
contents: write
id-token: write
attestations: write

jobs:
wait-and-publish:
runs-on: ubuntu-24.04
timeout-minutes: 45

steps:
- name: Determine tag
id: tag
run: |
if [ "${{ github.event_name }}" = "push" ]; then
TAG="${GITHUB_REF#refs/tags/}"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
echo "Unsupported event: ${{ github.event_name }}"
exit 1
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Publishing release: ${TAG}"

- name: Wait for all platform workflows
run: |
set +e
TAG="${{ steps.tag.outputs.tag }}"
MAX_WAIT=2400 # 40 minutes (releases can take 20-30 min)
INTERVAL=30
ELAPSED=0

# Required release workflows (by filename)
REQUIRED=("release-linux.yml" "release-macos.yml" "release-windows.yml")

while [ $ELAPSED -lt $MAX_WAIT ]; do
ALL_DONE=true
ALL_PASSED=true

for wf in "${REQUIRED[@]}"; do
RESULT=$(gh run list --workflow "$wf" --branch "$TAG" --limit 1 \
--json status,conclusion 2>/dev/null | jq '.[0] // empty' || true)
STATUS=$(echo "$RESULT" | jq -r '.status // "not_found"' || true)
CONCLUSION=$(echo "$RESULT" | jq -r '.conclusion // ""' || true)

if [ "$STATUS" = "not_found" ] || [ -z "$STATUS" ]; then
echo " $wf: not found (waiting)"
ALL_DONE=false
elif [ "$STATUS" != "completed" ]; then
echo " $wf: $STATUS (waiting)"
ALL_DONE=false
elif [ "$CONCLUSION" != "success" ]; then
echo " $wf: $CONCLUSION (FAILED)"
ALL_PASSED=false
else
echo " $wf: success"
fi
done

if [ "$ALL_DONE" = "true" ]; then
if [ "$ALL_PASSED" = "true" ]; then
echo "All platform release workflows passed"
break
else
echo "One or more platform release workflows failed"
exit 1
fi
fi

echo "Waiting... (${ELAPSED}s / ${MAX_WAIT}s)"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done

if [ $ELAPSED -ge $MAX_WAIT ]; then
echo "Timeout waiting for platform release workflows"
exit 1
fi
env:
GH_TOKEN: ${{ github.token }}

- name: Download all release assets
run: |
TAG="${{ steps.tag.outputs.tag }}"
mkdir -p release-assets
cd release-assets

# Download all assets from the draft release
gh release download "$TAG" --clobber --repo "${{ github.repository }}" \
--pattern "*.tar.gz" --pattern "*.zip" --pattern "*.sbom.cdx.json" \
--pattern "CHECKSUMS*" || true

echo "Downloaded assets:"
ls -la

- name: Generate SLSA Provenance Attestations
run: |
TAG="${{ steps.tag.outputs.tag }}"
echo "Generating SLSA provenance attestations for release: $TAG"

cd release-assets
for artifact in *.tar.gz *.zip; do
if [ -f "$artifact" ]; then
echo "Attesting: $artifact"
gh attestation generate "$artifact" \
--repo "${{ github.repository }}" \
--signer github 2>&1 || echo "WARNING: Attestation failed for $artifact"
fi
done

echo "Attestation generation complete."
echo "Verify with: gh attestation verify <artifact> --repo ${{ github.repository }}"
env:
GH_TOKEN: ${{ github.token }}

- name: Generate checksums index
run: |
cd release-assets
if ls *.tar.gz *.zip 1> /dev/null 2>&1; then
sha256sum *.tar.gz *.zip > CHECKSUMS-SHA256.txt 2>/dev/null || true
md5sum *.tar.gz *.zip > CHECKSUMS-MD5.txt 2>/dev/null || true
echo "Checksums generated:"
cat CHECKSUMS-SHA256.txt
fi

- name: Upload checksums to release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
with:
tag_name: ${{ steps.tag.outputs.tag }}
files: release-assets/CHECKSUMS-*.txt
draft: true

- name: Publish release
run: |
TAG="${{ steps.tag.outputs.tag }}"
echo "Publishing release: $TAG"
gh release edit "$TAG" --draft=false --make-latest
echo "Release published: https://github.com/${{ github.repository }}/releases/tag/$TAG"
env:
GH_TOKEN: ${{ github.token }}
130 changes: 3 additions & 127 deletions .github/workflows/release-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,74 +54,11 @@ env:
permissions: read-all

jobs:
# ===========================================================================
# CI Gate: Wait for CI/Sanitizers/Code Quality to pass before building
# ===========================================================================
ci-gate:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Check required workflows passed on this commit
run: |
set +e
set -u
SHA="${{ github.sha }}"
MAX_WAIT=1080 # 18 minutes
INTERVAL=30
ELAPSED=0

# Workflow filenames that must pass (CI, Sanitizers, Code Quality)
REQUIRED=("ci.yml" "sanitizers.yml" "code-quality.yml")

while [ $ELAPSED -lt $MAX_WAIT ]; do
ALL_DONE=true
ALL_PASSED=true

for wf in "${REQUIRED[@]}"; do
RESULT=$(gh run list --workflow "$wf" --branch "$GITHUB_REF_NAME" --limit 1 \
--json status,conclusion 2>/dev/null | jq '.[0] // empty' || true)
STATUS=$(echo "$RESULT" | jq -r '.status // "not_found"' || true)
CONCLUSION=$(echo "$RESULT" | jq -r '.conclusion // ""' || true)

if [ "$STATUS" = "not_found" ] || [ -z "$STATUS" ]; then
echo " $wf: not found (waiting)"
ALL_DONE=false
elif [ "$STATUS" != "completed" ]; then
echo " $wf: $STATUS (waiting)"
ALL_DONE=false
elif [ "$CONCLUSION" != "success" ]; then
echo " $wf: $CONCLUSION (FAILED)"
ALL_PASSED=false
else
echo " $wf: success"
fi
done

if [ "$ALL_DONE" = "true" ]; then
if [ "$ALL_PASSED" = "true" ]; then
echo "All required CI workflows passed"
exit 0
else
echo "One or more required CI workflows failed"
exit 1
fi
fi

echo "Waiting for CI workflows... (${ELAPSED}s / ${MAX_WAIT}s)"
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done

echo "Timeout waiting for CI workflows to complete"
exit 1
env:
GH_TOKEN: ${{ github.token }}

# ===========================================================================
# Prepare Build Matrix
# ===========================================================================
prepare:
# needs: ci-gate # TODO fix for tag runs
# CI gate removed — relies on pre-tag verification (scripts/verify-ci-green.sh)
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
Expand Down Expand Up @@ -640,8 +577,6 @@ jobs:

permissions:
contents: write
id-token: write
attestations: write

steps:
- name: Download All Artifacts
Expand Down Expand Up @@ -691,7 +626,7 @@ jobs:
echo "Release tag: ${TAG}"

- name: Upload to Release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
with:
tag_name: ${{ steps.release-tag.outputs.tag }}
files: |
Expand All @@ -701,69 +636,10 @@ jobs:
release-assets/CHECKSUMS-SHA256.txt
fail_on_unmatched_files: false
# Create draft release for tag push (immutable releases)
draft: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Generate SLSA Provenance Attestations
run: |
echo "Generating SLSA provenance attestations for release artifacts..."
for artifact in release-assets/*.tar.gz; do
echo "Attesting: $(basename "$artifact")"
gh attestation generate "$artifact" \
--repo "${{ github.repository }}" \
--signer github 2>&1 || echo "WARNING: Attestation failed for $(basename "$artifact")"
done
echo "Attestation generation complete."
echo "Verify with: gh attestation verify <artifact> --repo ${{ github.repository }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# ===========================================================================
# Test Bundles on Multiple Distributions
# ===========================================================================
# Each distro tests with the correct PHP version bundle matching its system PHP.
# Extension requires PHP 8.2+ (PHP 7.x/8.1 not supported).
#
# Current matrix:
# - Ubuntu 24.04: PHP 8.3 -> tests php83-nts bundle
# - Ubuntu 25.04: PHP 8.4 -> tests php84-nts bundle
# - Debian 12: PHP 8.2 -> tests php82-nts bundle
# - Fedora 41: PHP 8.3 -> tests php83-nts bundle
#
# Removed: Ubuntu 24.10 (EOL July 2025, repos no longer available)
# Removed: AlmaLinux 9 (default PHP 8.0 not supported, requires 8.1+)
# Removed: Ubuntu 20.04 (PHP 7.4), Debian 11 (PHP 7.4), AlmaLinux 8 (PHP 7.2)
test-bundles:
needs: [prepare, build]
runs-on: ubuntu-24.04
if: |
github.event_name == 'release' ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||
github.event_name == 'workflow_dispatch'

strategy:
fail-fast: false
matrix:
include:
- distro: ubuntu:24.04
php: "83"
bundle_pattern: "*php83-nts-linux-x86_64*"
- distro: ubuntu:25.04
php: "84"
bundle_pattern: "*php84-nts-linux-x86_64*"
- distro: debian:12
php: "82"
bundle_pattern: "*php82-nts-linux-x86_64*"
- distro: fedora:41
php: "83"
bundle_pattern: "*php83-nts-linux-x86_64*"
- distro: alpine:3.21
php: "83"
bundle_suffix: "musl"
bundle_pattern: "*php83-nts-linux-musl-x86_64*"

steps:
- name: Download PHP ${{ matrix.php }} NTS Bundle
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
Expand Down
Loading
Loading