name: CI on: pull_request: branches: [main, dev] # Design notes: # - Matrix parallelism cuts wall time from ~3-4min to ~60-90s (cache warm). # - Concurrency group cancels superseded runs on the same ref (saves runner time on rapid pushes). # - Build leg produces dist/ artifact; test leg downloads it instead of rebuilding (DRY). # - fail-fast: false so every failure is visible in one run (no re-pushing to see the next failure). concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: validate: if: >- contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association) runs-on: [self-hosted, linux, x64] env: # Keep Bun cache isolated per job workspace so parallel self-hosted runs # do not race on ~/.bun/install/cache and corrupt restore/install state. BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache strategy: fail-fast: false matrix: check: - { name: typecheck, cmd: 'bun run typecheck' } - { name: lint, cmd: 'bun run lint' } - { name: format, cmd: 'bun run format:check' } name: ${{ matrix.check.name }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: '1.3.9' - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Restore bun package cache uses: actions/cache@v4 with: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - name: Ensure dependencies run: bash scripts/ensure-deps.sh - name: Run ${{ matrix.check.name }} run: ${{ matrix.check.cmd }} build: if: >- contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association) runs-on: [self-hosted, linux, x64] name: build env: BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: '1.3.9' - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Restore bun package cache uses: actions/cache@v4 with: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - name: Ensure dependencies run: bash scripts/ensure-deps.sh - name: Build run: bun run build:all - name: Upload dist artifact uses: actions/upload-artifact@v4 with: name: dist path: dist/ retention-days: 1 if-no-files-found: error test: if: >- contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association) runs-on: [self-hosted, linux, x64] name: test needs: [build] env: BUN_INSTALL_CACHE_DIR: ${{ github.workspace }}/.bun/install/cache steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: '1.3.9' - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Restore bun package cache uses: actions/cache@v4 with: path: | ${{ env.BUN_INSTALL_CACHE_DIR }} key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }} - name: Ensure dependencies run: bash scripts/ensure-deps.sh - name: Download dist artifact uses: actions/download-artifact@v4 with: name: dist path: dist/ - name: Test run: bun run test:all - name: Test CLI e2e env: CCS_E2E_SKIP_BUILD: '1' run: bun run test:e2e compose-parity: if: >- contains(fromJSON('["COLLABORATOR","MEMBER","OWNER"]'), github.event.pull_request.author_association) runs-on: [self-hosted, linux, x64, cliproxy] name: compose-parity steps: - name: Checkout code uses: actions/checkout@v4 with: persist-credentials: false - name: Assert compose parity (compose.yaml vs docker-compose.integrated.yml) run: bash tests/docker/compose-parity.sh # Single always-reporting status that branch protection requires. # # The validate/build/test jobs are gated to trusted author associations so # untrusted fork code never executes on the self-hosted runners. A job skipped # by a job-level `if` reports no status, so requiring those job names directly # leaves fork PRs stuck on "Expected - waiting for status to be reported" # forever. Requiring this gate instead keeps the contract satisfiable: # - trusted PR: gate is green only if every gated job actually succeeded # - fork PR: gated jobs skip, gate reports green so the PR is mergeable # (fork code is still reviewed by a maintainer before merge) # # No checkout here: the gate only inspects upstream job results, so it runs no # third-party code and is safe on the self-hosted runner. ci-gate: if: always() needs: [validate, build, test] runs-on: [self-hosted, linux, x64] name: CI Gate steps: - name: Verify gated jobs did not fail env: VALIDATE: ${{ needs.validate.result }} BUILD: ${{ needs.build.result }} TEST: ${{ needs.test.result }} run: | echo "validate=$VALIDATE build=$BUILD test=$TEST" for result in "$VALIDATE" "$BUILD" "$TEST"; do if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then echo "[X] A required CI job did not pass (result: $result)" exit 1 fi done echo "[OK] CI gate satisfied (success or skipped-for-fork)"