Skip to content

Dev

Dev #18

Workflow file for this run

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