Dev #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Checks | |
| on: | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Phase 1: Lightweight release detection (no builds needed) | |
| # Runs semantic-release --dry-run for each package to determine which | |
| # ones would get a new version if dev were merged to main. | |
| # This avoids running expensive build+test pipelines for packages | |
| # that have no pending release. | |
| # ────────────────────────────────────────────────────────────────────── | |
| release-check: | |
| name: Detect Pending Releases | |
| runs-on: ubuntu-latest | |
| outputs: | |
| ducrs: ${{ steps.check.outputs.ducrs }} | |
| ducrs_version: ${{ steps.check.outputs.ducrs_version }} | |
| ducrs_current: ${{ steps.check.outputs.ducrs_current }} | |
| ducjs: ${{ steps.check.outputs.ducjs }} | |
| ducjs_version: ${{ steps.check.outputs.ducjs_version }} | |
| ducjs_current: ${{ steps.check.outputs.ducjs_current }} | |
| ducpy: ${{ steps.check.outputs.ducpy }} | |
| ducpy_version: ${{ steps.check.outputs.ducpy_version }} | |
| ducpy_current: ${{ steps.check.outputs.ducpy_current }} | |
| ducpdf: ${{ steps.check.outputs.ducpdf }} | |
| ducpdf_version: ${{ steps.check.outputs.ducpdf_version }} | |
| ducpdf_current: ${{ steps.check.outputs.ducpdf_current }} | |
| ducsvg: ${{ steps.check.outputs.ducsvg }} | |
| ducsvg_version: ${{ steps.check.outputs.ducsvg_version }} | |
| ducsvg_current: ${{ steps.check.outputs.ducsvg_current }} | |
| duc2pdf: ${{ steps.check.outputs.duc2pdf }} | |
| duc2pdf_version: ${{ steps.check.outputs.duc2pdf_version }} | |
| duc2pdf_current: ${{ steps.check.outputs.duc2pdf_current }} | |
| any_release: ${{ steps.check.outputs.any_release }} | |
| test_filters: ${{ steps.check.outputs.test_filters }} | |
| needs_wasm: ${{ steps.check.outputs.needs_wasm }} | |
| needs_python: ${{ steps.check.outputs.needs_python }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: true | |
| - name: Setup Git | |
| run: | | |
| git config user.name "GitHub Action" | |
| git config user.email "action@github.com" | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "lts/*" | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "latest" | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Simulate merge to main | |
| run: | | |
| git stash || true | |
| git fetch origin main --tags | |
| git fetch origin ${{ github.head_ref }} | |
| git checkout main | |
| git merge origin/${{ github.head_ref }} --no-edit | |
| - name: Check releases for all packages | |
| id: check | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| FILTERS="" | |
| ANY="false" | |
| NEEDS_WASM="false" | |
| NEEDS_PYTHON="false" | |
| check_release() { | |
| local name="$1" | |
| local dir="$2" | |
| pushd "$dir" > /dev/null | |
| local output | |
| # Override CI env vars so semantic-release sees a push to main | |
| # instead of a PR context (which causes it to skip entirely). | |
| output=$( | |
| GITHUB_EVENT_NAME=push \ | |
| GITHUB_REF=refs/heads/main \ | |
| GITHUB_REF_NAME=main \ | |
| GITHUB_HEAD_REF= \ | |
| GITHUB_BASE_REF= \ | |
| npx semantic-release --dry-run 2>&1 | |
| ) || true | |
| popd > /dev/null | |
| local version current | |
| version=$(echo "$output" | sed -n 's/.*[Tt]he next release version is \([^ ]*\).*/\1/p' | head -1) | |
| current=$(echo "$output" | sed -n 's/.*associated with version \([^ ]*\) on branch.*/\1/p' | head -1) | |
| if [ -n "$version" ]; then | |
| echo "${name}=true" >> "$GITHUB_OUTPUT" | |
| echo "${name}_version=${version}" >> "$GITHUB_OUTPUT" | |
| echo "${name}_current=${current}" >> "$GITHUB_OUTPUT" | |
| echo "📦 ${name}: ${current:-new} → ${version}" | |
| return 0 | |
| else | |
| echo "${name}=false" >> "$GITHUB_OUTPUT" | |
| echo "${name}_version=" >> "$GITHUB_OUTPUT" | |
| echo "${name}_current=${current}" >> "$GITHUB_OUTPUT" | |
| echo "⏭️ ${name}: no release (current: ${current:-none})" | |
| echo " ↳ $(echo "$output" | grep -iE 'release|version|skip|branch|error' | head -3)" | |
| return 1 | |
| fi | |
| } | |
| if check_release "ducrs" "./packages/ducrs"; then | |
| FILTERS="${FILTERS:+$FILTERS }--filter=ducrs" | |
| ANY="true" | |
| fi | |
| if check_release "ducjs" "./packages/ducjs"; then | |
| FILTERS="${FILTERS:+$FILTERS }--filter=ducjs" | |
| ANY="true" | |
| NEEDS_WASM="true" | |
| fi | |
| if check_release "ducpy" "./packages/ducpy"; then | |
| FILTERS="${FILTERS:+$FILTERS }--filter=ducpy" | |
| ANY="true" | |
| NEEDS_PYTHON="true" | |
| fi | |
| if check_release "ducpdf" "./packages/ducpdf"; then | |
| FILTERS="${FILTERS:+$FILTERS }--filter=ducpdf" | |
| ANY="true" | |
| NEEDS_WASM="true" | |
| fi | |
| if check_release "ducsvg" "./packages/ducsvg"; then | |
| FILTERS="${FILTERS:+$FILTERS }--filter=ducsvg" | |
| ANY="true" | |
| NEEDS_WASM="true" | |
| fi | |
| if check_release "duc2pdf" "./packages/ducpdf/src/duc2pdf"; then | |
| FILTERS="${FILTERS:+$FILTERS }--filter=duc2pdf" | |
| ANY="true" | |
| fi | |
| echo "test_filters=${FILTERS}" >> "$GITHUB_OUTPUT" | |
| echo "any_release=${ANY}" >> "$GITHUB_OUTPUT" | |
| echo "needs_wasm=${NEEDS_WASM}" >> "$GITHUB_OUTPUT" | |
| echo "needs_python=${NEEDS_PYTHON}" >> "$GITHUB_OUTPUT" | |
| echo "" | |
| echo "═══════════════════════════════" | |
| if [ "$ANY" = "true" ]; then | |
| echo "📋 Test plan: turbo test ${FILTERS}" | |
| else | |
| echo "📋 No packages need testing" | |
| fi | |
| echo "═══════════════════════════════" | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Phase 2: Build & test only packages with pending releases | |
| # Uses turbo to orchestrate the dependency graph — if ducsvg needs | |
| # testing, turbo automatically builds ducpdf → ducjs → ducrs first. | |
| # Toolchains are installed conditionally to save time. | |
| # ────────────────────────────────────────────────────────────────────── | |
| test: | |
| name: Test Affected Packages | |
| needs: release-check | |
| if: needs.release-check.outputs.any_release == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20.x | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "latest" | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Install wasm-pack | |
| if: needs.release-check.outputs.needs_wasm == 'true' | |
| run: cargo install wasm-pack --version 0.14.0 --locked --force | |
| - name: Setup Python | |
| if: needs.release-check.outputs.needs_python == 'true' | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Install uv | |
| if: needs.release-check.outputs.needs_python == 'true' | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Run package tests | |
| run: bun turbo test ${{ needs.release-check.outputs.test_filters }} | |
| - name: Validate Pyodide build tooling | |
| if: needs.release-check.outputs.ducpy == 'true' | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "pyodide-build==0.29.3" "wheel<0.44" | |
| # Verify pyodide CLI works (catches import errors like the wheel.cli removal) | |
| pyodide --help > /dev/null | |
| # Install cross-build environment | |
| pyodide xbuildenv install 0.29.3 | |
| # Derive PYODIDE_ROOT directly — the xbuildenv install prints its path, | |
| # and pyodide config requires a Makefile that only exists inside a full | |
| # Pyodide checkout, not in the standalone xbuildenv. | |
| XBUILD_DIR="$(pwd)/.pyodide-xbuildenv-0.29.3/0.29.3/xbuildenv" | |
| if [ ! -d "$XBUILD_DIR/pyodide-root" ]; then | |
| # Fall back to home-directory location (used outside CI workdirs) | |
| XBUILD_DIR="$HOME/.pyodide-xbuildenv-0.29.3/0.29.3/xbuildenv" | |
| fi | |
| export PYODIDE_ROOT="$XBUILD_DIR/pyodide-root" | |
| echo "PYODIDE_ROOT=$PYODIDE_ROOT" | |
| # Check wheel platform tag — must contain pyodide_* for Pyodide 0.29.3 compat | |
| TAG=$(python3 -c " | |
| import os | |
| os.environ['PYODIDE_ROOT'] = '$PYODIDE_ROOT' | |
| from pyodide_build.build_env import pyodide_tags | |
| print(next(iter(pyodide_tags()))) | |
| ") | |
| echo "Pyodide primary wheel tag: $TAG" | |
| if echo "$TAG" | grep -q "pyodide_"; then | |
| echo "Wheel tag includes pyodide_* — OK" | |
| else | |
| echo "ERROR: Wheel tag does not include pyodide_*. Got: $TAG" | |
| echo "This means the built wheel will NOT be compatible with the Pyodide 0.29.3 runtime." | |
| exit 1 | |
| fi | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Phase 3: Summary & gate check | |
| # Acts as a single required status check for branch protection. | |
| # Produces a step summary visible in the Actions UI. | |
| # ────────────────────────────────────────────────────────────────────── | |
| results: | |
| name: PR Check Results | |
| needs: [release-check, test] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Evaluate results | |
| run: | | |
| format_status() { | |
| local has_release="$1" | |
| local current="$2" | |
| local next="$3" | |
| local cur_display="${current:-–}" | |
| if [ -n "$current" ]; then cur_display="\`${current}\`"; fi | |
| if [ "$has_release" = "true" ] && [ -n "$next" ]; then | |
| echo "${cur_display} | \`${next}\` ✅" | |
| elif [ "$has_release" = "true" ]; then | |
| echo "${cur_display} | ✅ Yes" | |
| else | |
| echo "${cur_display} | ➖ No" | |
| fi | |
| } | |
| { | |
| echo "## PR Check Results" | |
| echo "" | |
| echo "### Pending Releases" | |
| echo "| Package | Current | Next |" | |
| echo "|---------|---------|------|" | |
| echo "| ducrs | $(format_status "${{ needs.release-check.outputs.ducrs }}" "${{ needs.release-check.outputs.ducrs_current }}" "${{ needs.release-check.outputs.ducrs_version }}") |" | |
| echo "| ducjs | $(format_status "${{ needs.release-check.outputs.ducjs }}" "${{ needs.release-check.outputs.ducjs_current }}" "${{ needs.release-check.outputs.ducjs_version }}") |" | |
| echo "| ducpy | $(format_status "${{ needs.release-check.outputs.ducpy }}" "${{ needs.release-check.outputs.ducpy_current }}" "${{ needs.release-check.outputs.ducpy_version }}") |" | |
| echo "| ducpdf | $(format_status "${{ needs.release-check.outputs.ducpdf }}" "${{ needs.release-check.outputs.ducpdf_current }}" "${{ needs.release-check.outputs.ducpdf_version }}") |" | |
| echo "| ducsvg | $(format_status "${{ needs.release-check.outputs.ducsvg }}" "${{ needs.release-check.outputs.ducsvg_current }}" "${{ needs.release-check.outputs.ducsvg_version }}") |" | |
| echo "| duc2pdf | $(format_status "${{ needs.release-check.outputs.duc2pdf }}" "${{ needs.release-check.outputs.duc2pdf_current }}" "${{ needs.release-check.outputs.duc2pdf_version }}") |" | |
| echo "" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| RELEASE_CHECK="${{ needs.release-check.result }}" | |
| TEST_RESULT="${{ needs.test.result }}" | |
| ANY_RELEASE="${{ needs.release-check.outputs.any_release }}" | |
| if [ "$RELEASE_CHECK" != "success" ]; then | |
| echo "### ❌ Release detection failed" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| if [ "$ANY_RELEASE" != "true" ]; then | |
| echo "### ✅ No pending releases — tests skipped" >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| if [ "$TEST_RESULT" = "success" ]; then | |
| echo "### ✅ All tests passed for affected packages" >> "$GITHUB_STEP_SUMMARY" | |
| elif [ "$TEST_RESULT" = "skipped" ]; then | |
| echo "### ⏭️ Tests were skipped" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "### ❌ Tests failed" >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| - name: Post PR comment | |
| if: always() | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUMBER="${{ github.event.pull_request.number }}" | |
| REPO="${{ github.repository }}" | |
| TEST_RESULT="${{ needs.test.result }}" | |
| DETECTION="${{ needs.release-check.result }}" | |
| format_row() { | |
| local name="$1" | |
| local has_release="$2" | |
| local current="$3" | |
| local next="$4" | |
| local cur_display="–" | |
| if [ -n "$current" ]; then cur_display="\`${current}\`"; fi | |
| if [ "$has_release" = "true" ]; then | |
| local test_icon | |
| if [ "$TEST_RESULT" = "success" ]; then | |
| test_icon="✅ Passed" | |
| elif [ "$TEST_RESULT" = "failure" ]; then | |
| test_icon="❌ Failed" | |
| else | |
| test_icon="⏳ Pending" | |
| fi | |
| echo "| ${name} | ${cur_display} | \`${next}\` | ${test_icon} |" | |
| else | |
| echo "| ${name} | ${cur_display} | – | ⏭️ No release |" | |
| fi | |
| } | |
| { | |
| echo "<!-- duc-release-preview -->" | |
| echo "## 📋 Release Preview" | |
| echo "" | |
| if [ "$DETECTION" != "success" ]; then | |
| echo "> ⚠️ Release detection failed — results may be incomplete." | |
| echo "" | |
| fi | |
| echo "| Package | Current | Next | Tests |" | |
| echo "|---------|---------|------|-------|" | |
| format_row "ducrs" "${{ needs.release-check.outputs.ducrs }}" "${{ needs.release-check.outputs.ducrs_current }}" "${{ needs.release-check.outputs.ducrs_version }}" | |
| format_row "ducjs" "${{ needs.release-check.outputs.ducjs }}" "${{ needs.release-check.outputs.ducjs_current }}" "${{ needs.release-check.outputs.ducjs_version }}" | |
| format_row "ducpy" "${{ needs.release-check.outputs.ducpy }}" "${{ needs.release-check.outputs.ducpy_current }}" "${{ needs.release-check.outputs.ducpy_version }}" | |
| format_row "ducpdf" "${{ needs.release-check.outputs.ducpdf }}" "${{ needs.release-check.outputs.ducpdf_current }}" "${{ needs.release-check.outputs.ducpdf_version }}" | |
| format_row "ducsvg" "${{ needs.release-check.outputs.ducsvg }}" "${{ needs.release-check.outputs.ducsvg_current }}" "${{ needs.release-check.outputs.ducsvg_version }}" | |
| format_row "duc2pdf" "${{ needs.release-check.outputs.duc2pdf }}" "${{ needs.release-check.outputs.duc2pdf_current }}" "${{ needs.release-check.outputs.duc2pdf_version }}" | |
| } > /tmp/pr-comment.md | |
| # Upsert PR comment (find existing by marker, update or create) | |
| COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ | |
| --paginate --jq '.[] | select(.body | contains("<!-- duc-release-preview -->")) | .id' | tail -1 || true) | |
| if [ -n "$COMMENT_ID" ]; then | |
| jq -n --arg body "$(cat /tmp/pr-comment.md)" '{body: $body}' | \ | |
| gh api "repos/${REPO}/issues/comments/${COMMENT_ID}" -X PATCH --input - | |
| else | |
| jq -n --arg body "$(cat /tmp/pr-comment.md)" '{body: $body}' | \ | |
| gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" -X POST --input - | |
| fi |