diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000..06f62b03 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -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 --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 }} diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml index 29f39281..e780b57a 100644 --- a/.github/workflows/release-linux.yml +++ b/.github/workflows/release-linux.yml @@ -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 }} @@ -640,8 +577,6 @@ jobs: permissions: contents: write - id-token: write - attestations: write steps: - name: Download All Artifacts @@ -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: | @@ -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 --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: diff --git a/.github/workflows/release-macos.yml b/.github/workflows/release-macos.yml index 754fd788..ca692752 100644 --- a/.github/workflows/release-macos.yml +++ b/.github/workflows/release-macos.yml @@ -52,74 +52,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 }} @@ -503,8 +440,6 @@ jobs: permissions: contents: write - id-token: write - attestations: write steps: - name: Download All Artifacts @@ -552,7 +487,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: | @@ -561,43 +496,10 @@ jobs: release-assets/*.sbom.cdx.json release-assets/CHECKSUMS-SHA256.txt fail_on_unmatched_files: false - 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 macOS 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." - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # =========================================================================== - # Test Bundles on macOS Runners - # =========================================================================== - test-bundles: - needs: [prepare, build] - runs-on: ${{ matrix.runner }} - 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: - - runner: macos-14 - arch: arm64 - php_brew: php - - steps: - name: Install PHP via Homebrew run: | brew install ${{ matrix.php_brew }} 2>/dev/null || true @@ -606,7 +508,7 @@ jobs: - name: Download Bundle uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 with: - pattern: "*macos-${{ matrix.arch }}*nts*" + pattern: "*macos*${{ matrix.arch }}*" path: bundle merge-multiple: true diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index c26eed4d..35cfe70e 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -33,75 +33,12 @@ env: FIREBIRD_SDK_CACHE_KEY: firebird-sdk-v2 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 }} - # =========================================================================== # Generate build matrix using php-windows-builder/extension-matrix # Reads PHP constraints from composer.json # =========================================================================== get-extension-matrix: - # 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.extension-matrix.outputs.matrix }} @@ -199,8 +136,6 @@ jobs: runs-on: ubuntu-24.04 permissions: contents: write - id-token: write - attestations: write if: | github.event_name == 'release' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) || @@ -223,28 +158,10 @@ jobs: path: artifacts - name: Upload Windows DLLs to GitHub Release - run: | - TAG="${{ steps.tag.outputs.tag }}" - echo "Uploading Windows artifacts to release: $TAG" - find artifacts -name "*.zip" -type f | while read -r zip_file; do - echo "Uploading: $(basename "$zip_file")" - gh release upload "$TAG" "$zip_file" --clobber - done - echo "Upload complete." - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - - - name: Generate SLSA Provenance Attestations - run: | - echo "Generating SLSA provenance attestations for Windows artifacts..." - find artifacts -name "*.zip" -type f | while read -r artifact; 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 --repo ${{ github.repository }}" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3 + with: + tag_name: ${{ steps.tag.outputs.tag }} + files: artifacts/**/*.zip + fail_on_unmatched_files: true + draft: true + diff --git a/.github/workflows/split-stubs.yml b/.github/workflows/split-stubs.yml index bf05b1bd..67c0771f 100644 --- a/.github/workflows/split-stubs.yml +++ b/.github/workflows/split-stubs.yml @@ -26,7 +26,16 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 # Full history required for subtree split - + + - name: Delete existing tag in stubs repo (if re-tagging) + if: github.ref_type == 'tag' + run: | + # If the tag already exists in the stubs repo (e.g. from a previous split), + # delete it so the split action can recreate it. + gh api -X DELETE "repos/satwareAG/php-firebird-stubs/git/refs/tags/${{ github.ref_name }}" 2>/dev/null || true + env: + GH_TOKEN: ${{ secrets.STUBS_REPO_TOKEN }} + - name: Split stubs directory uses: danharrin/monorepo-split-github-action@19b2e0cbe3fe3de17309fd0fe95d4e05454f3ab3 # v2.4.0 env: diff --git a/AGENTS.md b/AGENTS.md index e85a92f4..7d8947dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,3 +42,46 @@ This project targets **L3 conformance** (Discovery). - **PDO Driver**: `pdo_fbird/`. Agents MUST maintain consistency between the three layers when introducing new features. + +--- + +## Release Procedure + +### Pre-Tag Checklist + +1. **Verify CI green**: `bash scripts/verify-ci-green.sh` — all 4 workflows must pass +2. **Run local test matrix**: `bash scripts/test_matrix.sh` — all 12 containers must pass +3. **Verify version stamps**: `bash scripts/check-version-stamps.sh` +4. **Update CHANGELOG.md** with release notes +5. **Bump version**: Update `VERSION.txt` and `stubs/*.php` `@version` tags + +### Tagging Rules + +- **NEVER force-push a tag.** Once `git push origin v*` succeeds, the tag is immutable. +- If a release is broken: delete tag + release entirely, fix, re-tag with a NEW tag (e.g., `v11.0.2`). +- Tag format: `vMAJOR.MINOR.PATCH` (e.g., `v11.0.1`) + +### Release Pipeline Architecture + +``` +Tag push v* + | + +-- CI, Code Quality, Sanitizers, Coverage (validation, already passed) + | + +-- Release (Linux) -> prepare -> build -> test-bundles -> upload (draft:true) + +-- Release (macOS) -> prepare -> build -> test-bundles -> upload (draft:true) + +-- Release (Windows) -> get-matrix -> build -> upload (draft:true) + | + +-- Split Stubs -> split stubs/ to satwareAG/php-firebird-stubs + | + +-- Publish Release -> waits for all 3 platform workflows + -> generates SLSA attestations + -> generates checksums + -> publishes release (draft:false) +``` + +**Key rules**: +- Platform workflows upload with `draft: true` via `softprops/action-gh-release@v3` +- The `publish-release.yml` workflow is the ONLY one that flips `draft: false` +- SLSA attestations are generated centrally in `publish-release.yml` +- The split-stubs workflow deletes any pre-existing tag in the stubs repo before re-creating diff --git a/scripts/verify-ci-green.sh b/scripts/verify-ci-green.sh new file mode 100755 index 00000000..1260ea45 --- /dev/null +++ b/scripts/verify-ci-green.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# verify-ci-green.sh — Pre-tag CI verification +# +# Verifies that all required CI workflows have passed on the current +# HEAD of satware-main before tagging a release. Run BEFORE git tag. +# +# Usage: bash scripts/verify-ci-green.sh [branch] +# Default branch: satware-main +# +# Exit codes: +# 0 = all workflows green +# 1 = one or more workflows failed or not found +set -euo pipefail + +BRANCH="${1:-satware-main}" +REQUIRED_WORKFLOWS=("ci.yml" "code-quality.yml" "sanitizers.yml" "coverage.yml") + +# Colors +if [[ -t 1 ]]; then + GREEN='\033[0;32m' + RED='\033[0;31m' + YELLOW='\033[1;33m' + NC='\033[0m' +else + GREEN='' RED='' YELLOW='' NC='' +fi + +echo -e "Checking CI status on ${YELLOW}${BRANCH}${NC}..." +echo "" + +ALL_PASSED=true + +for wf in "${REQUIRED_WORKFLOWS[@]}"; do + # Get the latest run for this workflow on the specified branch + RESULT=$(gh run list --workflow "$wf" --branch "$BRANCH" --limit 1 \ + --json status,conclusion,headSha,createdAt 2>/dev/null | jq '.[0] // empty' || true) + + if [ -z "$RESULT" ]; then + echo -e " ${RED}FAIL${NC} ${wf}: no run found on ${BRANCH}" + ALL_PASSED=false + continue + fi + + STATUS=$(echo "$RESULT" | jq -r '.status') + CONCLUSION=$(echo "$RESULT" | jq -r '.conclusion // "running"') + SHA=$(echo "$RESULT" | jq -r '.headSha[0:8]') + CREATED=$(echo "$RESULT" | jq -r '.createdAt[0:16]') + + if [ "$STATUS" != "completed" ]; then + echo -e " ${YELLOW}WAIT${NC} ${wf}: ${STATUS} (started ${CREATED})" + ALL_PASSED=false + elif [ "$CONCLUSION" != "success" ]; then + echo -e " ${RED}FAIL${NC} ${wf}: ${CONCLUSION} (${SHA}, ${CREATED})" + ALL_PASSED=false + else + echo -e " ${GREEN}OK${NC} ${wf}: success (${SHA}, ${CREATED})" + fi +done + +echo "" +if [ "$ALL_PASSED" = "true" ]; then + echo -e "${GREEN}All CI workflows green on ${BRANCH}. Safe to tag.${NC}" + exit 0 +else + echo -e "${RED}CI not green on ${BRANCH}. Fix failures before tagging.${NC}" + exit 1 +fi