Dev #17
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 native toolchain for wasm build | |
| if: needs.release-check.outputs.needs_wasm == 'true' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y clang | |
| - 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 }} | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 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 |