Files
ccs/.github/workflows/ci.yml
T
Tam Nhu Tran 7dba36fb03 fix(ci): add always-reporting CI Gate so fork PRs stop deadlocking
The validate/build/test/compose-parity jobs are gated to trusted author
associations so untrusted fork code never runs on the self-hosted runners.
But a job skipped via a job-level if reports no status, and branch protection
requires the job names typecheck/lint/format/build/test directly, so fork PRs
hang on 'Expected - waiting for status to be reported' and can never merge.

Add a single CI Gate job (if: always(), needs the gated jobs, no checkout so it
runs no third-party code) that fails only when a gated job actually
failed/cancelled and passes when they succeed or skip-for-fork. Branch
protection should require CI Gate instead of the individual job names.

Closes #1461
2026-06-02 06:38:54 -04:00

197 lines
6.1 KiB
YAML

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)"