openpencil/.github/workflows/ci.yml
Danila Poyarkov 5b9533a230
ci: build and check packages in parallel and reuse verified trees in the merge queue (#944)
* ci: build and check packages in parallel

The Vue SDK's declarations used the tsc resolver, which took 21 of the 32 seconds a local package build takes; tsdown's default oxc resolver writes byte-identical output in 4 seconds. Packages now build level by level, each level's packages together, with their output printed whole. Package checks run npm and Bun packing side by side and ATTW on every core instead of two.

* ci: skip the merge queue's suites for a tree its PR already passed

The merge queue reran every check even when master had not moved, so the queued commit had exactly the tree the pull request's CI had just passed. A passing PR run now records that tree as a commit status on the PR head, and the queue's classification compares its own tree with it: a match runs only the always-on checks, anything else the full suites. Fork PRs cannot write the status and keep the full run.

* ci: accept a verified tree only from its pull request's passing CI run

Any writer can post a commit status, and another pull request's CI could post one on this head, so a status alone could skip the queue's suites. The record now links the run that wrote it, and the queue accepts it only when GitHub shows Actions created it and the run is this repository's CI workflow on pull_request, passed, and ran on this exact head. Recording no longer fails the gate when the status cannot be written. Parallel packs and builds now all settle before a failure is reported, so none writes into a directory that is being removed or rebuilt.

* refactor(ci): group the verified-tree lookup and recorder in one folder
2026-10-07 11:04:39 +00:00

297 lines
9.6 KiB
YAML

name: CI
on:
# Every base, so stacked pull requests are checked against the PR below them.
pull_request:
# The merge queue checks each queued change on top of the ones ahead of it.
merge_group:
permissions:
contents: read
packages: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Classify changes
runs-on: ubuntu-latest
timeout-minutes: 3
# In the merge queue, reads the queued PR's verified-tree status and the CI run behind it.
permissions:
contents: read
actions: read
pull-requests: read
statuses: read
outputs:
scope: ${{ steps.classify.outputs.scope }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- name: Fetch comparison base
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
run: git fetch --no-tags --depth=1 origin "$CI_BASE_SHA"
- name: Select validation scope
id: classify
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
# In the merge queue, a tree the pull request's CI already passed skips the suites.
CI_EVENT: ${{ github.event_name }}
CI_HEAD_REF: ${{ github.event.merge_group.head_ref }}
GITHUB_TOKEN: ${{ github.token }}
run: bun tools/ci/policy/src/classify.ts
commit-messages:
name: Commit messages
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Validate PR commit messages
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
run: |
if ! bun run check:commits --from "$BASE_SHA" --to "$HEAD_SHA" --verbose; then
echo '::error title=Commit messages::Check the messages listed above. Use type(scope): description, for example fix: preserve selection. See CONTRIBUTING.md#commit-messages.'
exit 1
fi
documentation:
name: Documentation
needs: changes
if: needs.changes.outputs.scope == 'docs'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Build documentation type dependencies
run: bun run build:packages
- name: Validate documentation and generated references
run: bun run check:docs
- name: Build documentation and check examples
run: bun run docs:build
source-quality:
name: Code quality
needs: changes
if: needs.changes.outputs.scope == 'code'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Generate package declarations
run: bun run build:packages
- name: Verify formatting
run: bun run format:check
- name: Lint TypeScript and Vue
run: bun run lint
- name: Typecheck application and SDKs
run: bun run typecheck
- name: Typecheck test suites
run: bun run check:test-types
- name: Enforce architecture and type-shape boundaries
run: bun run check:arch && bun run check:test-homes && bun run test:type-shapes
package-quality:
name: Package integrity
needs: changes
if: needs.changes.outputs.scope == 'code'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Cache npm consumer downloads
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ~/.npm
key: npm-consumers-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: npm-consumers-${{ runner.os }}-
- name: Build publishable packages
run: bun run build:packages
- name: Validate installed package artifacts with Node and Bun
run: bun run test:packages
- name: Detect unused dependencies and files
run: bun run check:deps
- name: Validate workspace dependency policy
run: bun run check:monorepo
repository-quality:
name: Repository hygiene
needs: changes
if: needs.changes.outputs.scope == 'code'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Validate generated brand assets
run: bun run check:icons
- name: Validate translations
run: bun run check:i18n
- name: Validate documentation links and structure
run: bun run check:docs
- name: Audit critical dependency vulnerabilities
run: bun run check:audit
- name: Scan for committed secrets
run: bun run check:secrets
- name: Test repository tooling
run: bun run test:tools
- name: Detect duplicated product code
run: bun run test:dupes
storybook:
name: Component workshop
needs: changes
if: needs.changes.outputs.scope == 'code'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-bun
- name: Build component dependencies
run: bun run build:packages
- name: Build static Storybook
run: bun run build-storybook
native-test-contracts:
name: Native app contracts
needs: changes
if: needs.changes.outputs.scope == 'code'
timeout-minutes: 8
runs-on: ubuntu-24.04
container:
image: ghcr.io/open-pencil/native-contracts-ci@sha256:64e6b1b50a988c1cefe2b69ac9d58076fd743b18391fa2dbd1d153eba2be9521
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: desktop -> target
key: native-test
- uses: ./.github/actions/setup-bun
- name: Typecheck native interaction tests
run: bun run check:native-test
- name: Generate native brand assets
run: bun run generate:icons --target desktop
- name: Compile native-test Tauri feature
run: cargo check --manifest-path desktop/Cargo.toml --features native-test
unit-tests:
needs: changes
if: needs.changes.outputs.scope == 'code'
timeout-minutes: 10
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
group: [app, cli, core, dom, fig, mcp, render, scene-graph, vue]
name: Unit tests — ${{ matrix.group }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: ./.github/actions/setup-bun
with:
lfs: 'true'
# Quick shards read only the small fixtures; heavy-tests.yml pulls the large .fig corpora.
lfs-include: tests/fixtures/gold-preview.fig,tests/fixtures/circle-text.fig,tests/fixtures/slots.fig,tests/fixtures/variable-override-precedence.fig,tests/fixtures/fonts/*
- name: Build shared Core test dependency
run: bun --filter @open-pencil/core build
- name: Run ${{ matrix.group }} unit tests
run: bun tools/dev/unit-tests/src/run.ts "${{ matrix.group }}"
result:
name: CI result
needs: [changes, commit-messages, documentation, source-quality, package-quality, repository-quality, storybook, native-test-contracts, unit-tests]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 3
# Records the verified tree as a commit status once the gate has passed.
permissions:
contents: read
statuses: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- name: Require successful checks for the selected scope
env:
CI_NEEDS: ${{ toJSON(needs) }}
run: bun tools/ci/policy/src/gate.ts
# Lets the merge queue skip the suites when it tests this exact tree. Forks cannot write it,
# and the queue accepts it only from this run. Recording is an optimization: a failure
# leaves the queue to run every check, so it must not fail the gate.
- name: Record the verified tree
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
env:
CI_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CI_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
GITHUB_TOKEN: ${{ github.token }}
run: bun tools/ci/policy/src/verified-tree/record.ts