mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-10 22:23:35 +00:00
Merge pull request #1068 from kaitranntt/kai/chore/ci-contributor-friendly
chore(ci): contributor-friendly CI remediation
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
|
||||
Use what applies. If you skipped something, add a short note instead of forcing it.
|
||||
|
||||
- [ ] `bun run validate`
|
||||
- [ ] `bun run validate:ci-parity`
|
||||
- [ ] `bun run format && bun run lint:fix && bun run validate`
|
||||
- [ ] `bun run validate:ci-parity` before requesting review
|
||||
- [ ] `bun run test:e2e` if this PR touches command routing, proxy flows, or workflow/release logic
|
||||
- [ ] `cd ui && bun run validate` if UI changed
|
||||
- [ ] Not run
|
||||
|
||||
@@ -20,6 +21,7 @@ Check what applies. Not every item is relevant for every PR.
|
||||
- [ ] Relevant `--help` output updated if CLI behavior changed
|
||||
- [ ] Tests added or updated if behavior changed
|
||||
- [ ] README or local docs updated if user-facing behavior changed
|
||||
- [ ] If a check failed, the PR body explains what failed and what changed to fix it
|
||||
- [ ] No secrets, tokens, or private config data are included
|
||||
|
||||
## Docs Impact
|
||||
|
||||
@@ -50,8 +50,6 @@ jobs:
|
||||
path: |
|
||||
${{ env.BUN_INSTALL_CACHE_DIR }}
|
||||
key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-cache-v2-
|
||||
|
||||
- name: Ensure dependencies
|
||||
run: bash scripts/ensure-deps.sh
|
||||
@@ -84,8 +82,6 @@ jobs:
|
||||
path: |
|
||||
${{ env.BUN_INSTALL_CACHE_DIR }}
|
||||
key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-cache-v2-
|
||||
|
||||
- name: Ensure dependencies
|
||||
run: bash scripts/ensure-deps.sh
|
||||
@@ -127,8 +123,6 @@ jobs:
|
||||
path: |
|
||||
${{ env.BUN_INSTALL_CACHE_DIR }}
|
||||
key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-cache-v2-
|
||||
|
||||
- name: Ensure dependencies
|
||||
run: bash scripts/ensure-deps.sh
|
||||
|
||||
@@ -47,9 +47,12 @@ jobs:
|
||||
- name: Build
|
||||
run: bun run build:all
|
||||
|
||||
- name: Validate (typecheck + lint + format + tests)
|
||||
- name: Validate fast gate
|
||||
run: bun run validate
|
||||
|
||||
- name: Test slow bucket
|
||||
run: bun run test:slow
|
||||
|
||||
- name: Test CLI e2e
|
||||
env:
|
||||
CCS_E2E_SKIP_BUILD: '1'
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
name: Push CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev]
|
||||
|
||||
concurrency:
|
||||
group: push-ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
env:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
@@ -42,9 +42,12 @@ jobs:
|
||||
- name: Build package
|
||||
run: bun run build:all
|
||||
|
||||
- name: Validate (typecheck + lint + format + tests)
|
||||
- name: Validate fast gate
|
||||
run: bun run validate
|
||||
|
||||
- name: Test slow bucket
|
||||
run: bun run test:slow
|
||||
|
||||
- name: Test CLI e2e
|
||||
env:
|
||||
CCS_E2E_SKIP_BUILD: '1'
|
||||
|
||||
+2
-2
@@ -45,9 +45,9 @@ echo " branch: $CURRENT_BRANCH"
|
||||
echo " base: $BASE_BRANCH"
|
||||
|
||||
bun run typecheck
|
||||
bun run lint:fix
|
||||
bun run lint
|
||||
bun run format:check
|
||||
bun run build:all
|
||||
bun run test:fast
|
||||
|
||||
git fetch origin "$BASE_BRANCH" --quiet || true
|
||||
DIFF_RANGE="HEAD"
|
||||
|
||||
@@ -41,6 +41,15 @@ gh pr checks <n>
|
||||
### Absolute rule
|
||||
AI MUST NOT declare a task done, close a session, or move to the next task while CI is red or still running. Leaving a PR red and moving on is the primary failure mode this protocol prevents.
|
||||
|
||||
### Dev Release vs Push CI
|
||||
|
||||
- `CI` is the pull-request quality gate for contributor branches.
|
||||
- `Push CI` is the post-merge quality signal for `dev`.
|
||||
- `Dev Release` publishes the `@dev` package after `dev` changes land.
|
||||
- A red `Dev Release` does **not** automatically mean contributor code failed. Check `Push CI` first.
|
||||
- Verified on `2026-04-22` via `gh api repos/kaitranntt/ccs/branches/dev/protection`: `dev` currently requires `typecheck`, `lint`, `format`, `build`, and `test`, has no branch restrictions, and has no required PR-review gate.
|
||||
- `dev-release.yml` currently pushes with `PAT_TOKEN` because `dev` is protected by those required status checks. Do not switch it back to `github.token` unless branch protection changes with it.
|
||||
|
||||
## Core Function
|
||||
|
||||
Multi-provider profile and runtime manager for Claude Code, Factory Droid,
|
||||
@@ -87,7 +96,7 @@ broader topic.
|
||||
| Mistake | Consequence | Correct Action |
|
||||
|---------|-------------|----------------|
|
||||
| Running `validate` without `format` first | format:check fails | Run `bun run format` BEFORE validate |
|
||||
| Assuming maintainability check is always strict | PR/feature branches run warning mode by default | Use `bun run maintainability:check:strict` before merge when touching debt-sensitive code |
|
||||
| Treating `Dev Release` as the contributor quality signal | Publish failures on `dev` look like broken code | Check PR `CI` on the branch and `Push CI` on `dev` first |
|
||||
| Using `chore:` for dev→main PR | No npm release triggered | Use `feat:` or `fix:` prefix |
|
||||
| Committing directly to `main` or `dev` | Bypasses CI/review | Always use PRs |
|
||||
| Manual version bump or git tag | Conflicts with semantic-release | Let CI handle versioning |
|
||||
@@ -208,8 +217,8 @@ Quality gates MUST pass before pushing. **Both projects have identical workflow.
|
||||
# Main project (from repo root)
|
||||
bun run format # Step 1: Fix formatting
|
||||
bun run lint:fix # Step 2: Fix lint issues
|
||||
bun run validate # Step 3: Full gate (typecheck + lint + format + maintainability + tests)
|
||||
bun run validate:ci-parity # Step 4: full CI parity gate (build + validate + base branch check)
|
||||
bun run validate # Step 3: Fast gate (typecheck + lint + format + test:fast)
|
||||
bun run validate:ci-parity # Step 4: PR-CI parity gate (branch check + build + full tests + e2e)
|
||||
|
||||
# UI project (if UI changed)
|
||||
cd ui
|
||||
@@ -221,17 +230,17 @@ bun run validate # Step 3: Final check (must pass)
|
||||
**WHY THIS ORDER:**
|
||||
- `validate` runs `format:check` which only VERIFIES—won't fix
|
||||
- If format:check fails, you skipped step 1
|
||||
- CI runs `validate` only (no auto-fix)—local must be clean
|
||||
- `validate` now uses read-only `lint`, so autofix still belongs in step 2
|
||||
- PR CI and `validate:ci-parity` both run non-mutating checks only
|
||||
|
||||
### What Validate Runs
|
||||
### What Each Gate Runs
|
||||
|
||||
| Project | Command | Runs |
|
||||
|---------|---------|------|
|
||||
| Main | `bun run validate` | typecheck + lint:fix + format:check + test:all |
|
||||
| Main | `bun run validate` | typecheck + lint + format:check + test:fast |
|
||||
| Main | `bun run validate:ci-parity` | base branch check + typecheck + lint + format:check + build:all + test:all + test:e2e |
|
||||
| UI | `bun run validate` | typecheck + lint:fix + format:check |
|
||||
|
||||
**Note:** `maintainability:check` is a SEPARATE gate — not part of `validate`. Run it explicitly via `bun run maintainability:check[:strict|:warn]` when touching debt-sensitive code or before merging to protected branches.
|
||||
|
||||
### ESLint Rules (ALL errors)
|
||||
|
||||
| Rule | Level | Notes |
|
||||
@@ -255,33 +264,19 @@ bun run validate # Step 3: Final check (must pass)
|
||||
|
||||
### Automatic Enforcement
|
||||
|
||||
- `prepublishOnly` / `prepack` runs `build:all` + `validate` + `sync-version.js`
|
||||
- CI/CD runs `bun run validate` on every PR (maintainability is warning mode on PR events)
|
||||
- `prepack` runs `build:all`
|
||||
- PR `CI` runs `typecheck`, `lint`, `format`, `build`, `test:all`, and `test:e2e`
|
||||
- `Push CI` runs the same quality suite on `dev` after merge, separate from release publishing
|
||||
- `Dev Release` still runs build + fast validation + slow tests + e2e before publishing and still requires `PAT_TOKEN` to push back to protected `dev`
|
||||
- husky `pre-commit` runs quick lint/type/format checks
|
||||
- husky `pre-push` runs the full `bun run validate:ci-parity` gate on `main`/`dev`/hotfix branches
|
||||
- husky `pre-push` runs a faster feature-branch gate (`typecheck` + `lint:fix` + `format:check` + targeted checks based on changed files) before GitHub CI handles the full matrix
|
||||
- husky `pre-push` runs a faster feature-branch gate (`typecheck` + `lint` + `format:check` + `test:fast`) plus targeted checks based on changed files
|
||||
|
||||
### Maintainability Baseline Gate
|
||||
### Maintainability Gate Status
|
||||
|
||||
- Baseline file: `docs/metrics/maintainability-baseline.json`
|
||||
- Metric collector/check script: `scripts/maintainability-baseline.js`
|
||||
- Branch-aware gate wrapper: `scripts/maintainability-check.js`
|
||||
- Enforcement path: `bun run maintainability:check` (run separately — NOT part of `bun run validate`; invoked by `validate:ci-parity` on protected branches)
|
||||
- Gate modes:
|
||||
- `strict`: protected branches (`main`, `dev`, `hotfix/*`, `kai/hotfix-*`) and equivalent CI refs
|
||||
- `warn`: pull request CI and non-protected local branches (non-blocking for parallel PR workflow)
|
||||
- override commands:
|
||||
- `bun run maintainability:check:strict`
|
||||
- `bun run maintainability:check:warn`
|
||||
- Gated metrics (must not increase vs baseline):
|
||||
- `processExitReferenceCount`
|
||||
- `synchronousFsApiReferenceCount`
|
||||
- Informational metrics (collected but not gated):
|
||||
- `largeFileCountOver350Loc`
|
||||
- Baseline update policy:
|
||||
1. Prefer reducing the metric and keeping the baseline unchanged.
|
||||
2. On protected-branch integration (strict mode), if increase is intentional and accepted, run `bun run maintainability:baseline`.
|
||||
3. Commit both the code change and `docs/metrics/maintainability-baseline.json`, and state reason in PR description.
|
||||
- The historical maintainability baseline gate is retired from the active CCS workflow.
|
||||
- `validate`, `validate:ci-parity`, PR `CI`, `Push CI`, and release workflows do **not** invoke `maintainability:check`.
|
||||
- Older roadmap references to `maintainability:baseline` / `maintainability:check` are historical context, not current repo commands.
|
||||
|
||||
## Critical Constraints (NEVER VIOLATE)
|
||||
|
||||
@@ -532,17 +527,16 @@ rm -rf ~/.ccs # Clean environment
|
||||
Optimized for iterative push-then-review workflow. Do NOT run the full gate on every push — CI is the safety net. Run the full gate once before asking for review / merge.
|
||||
|
||||
### Tier 1 — Iterative push (feature branch)
|
||||
Husky `pre-push` auto-runs: `typecheck + lint:fix + format:check + build:all` plus targeted tests based on changed files. AI does **nothing extra** at push time.
|
||||
Husky `pre-push` auto-runs: `typecheck + lint + format:check + test:fast` plus targeted checks based on changed files. AI does **nothing extra** at push time.
|
||||
|
||||
**After push (MANDATORY):** follow the [CI-First Protocol](#ci-first-protocol-mandatory) — watch CI until green. Do not move on while CI is red.
|
||||
|
||||
### Tier 2 — Before requesting review / merge
|
||||
Run ONCE, not per push:
|
||||
- [ ] `bun run validate:ci-parity` — full build + validate matches CI
|
||||
- [ ] `bun run validate:ci-parity` — branch freshness + build + full non-e2e tests + e2e
|
||||
- [ ] `gh pr checks <n>` — all checks green
|
||||
- [ ] If touching debt-sensitive code: `bun run maintainability:check:strict`
|
||||
- [ ] If strict mode fails and increase is intentional: `bun run maintainability:baseline` and commit `docs/metrics/maintainability-baseline.json`
|
||||
- [ ] If UI changed: `cd ui && bun run format && bun run validate`
|
||||
- [ ] If touching command routing, proxy flows, workflows, or release logic: `bun run test:e2e`
|
||||
|
||||
### Code / Docs / Standards (verify before merge)
|
||||
- [ ] Conventional commit format (`feat:`, `fix:`, etc.)
|
||||
|
||||
+49
-5
@@ -76,14 +76,29 @@ Rules:
|
||||
- Treat `hotfix/*` as maintainer-only emergency flow from `main`.
|
||||
- Delete your branch after merge.
|
||||
|
||||
## CI and Release Flow
|
||||
|
||||
CCS now uses three separate automation lanes:
|
||||
|
||||
- `CI` runs on pull requests to `dev` and `main`. This is the review gate for contributor branches.
|
||||
- `Push CI` runs after a merge lands on `dev`. This is the code-quality signal for the shared `dev` branch.
|
||||
- `Dev Release` publishes the `@dev` package after `dev` changes land. It is release automation, not the primary contributor quality signal.
|
||||
|
||||
If `Dev Release` is red but your PR checks were green, check `Push CI` before assuming the merged code is broken.
|
||||
|
||||
If `CI` or `Push CI` stays queued for a long time, it is a maintainer infrastructure issue, not a contributor mistake. Leave a comment on your PR and a maintainer will address it.
|
||||
|
||||
## AI Agent Rules
|
||||
|
||||
`CONTRIBUTING.md` is the human entry point. For AI agents working in this repo, the authoritative automation and workflow rules live in [CLAUDE.md](./CLAUDE.md).
|
||||
|
||||
## AI Review Lane
|
||||
|
||||
CCS PR review no longer depends on `anthropics/claude-code-action`. The repository review lane is self-hosted PR-Agent:
|
||||
|
||||
- The retained `.github/workflows/ai-review.yml` runs PR-Agent in GitHub Actions.
|
||||
- PR-Agent reviews run on the existing self-hosted `cliproxy` runner.
|
||||
- Use `/review` on the PR when you need a fresh pass after follow-up commits.
|
||||
- Only the trusted `/review` comment path is enabled on the privileged self-hosted runner.
|
||||
- Only the trusted `/review` comment path is enabled.
|
||||
- Keep repository-level reviewer instructions in the root `.pr_agent.toml`.
|
||||
- Keep runtime wiring and defaults in `ai-review.yml`, which still maps the existing `AI_REVIEW_BASE_URL`, `AI_REVIEW_MODEL`, and `AI_REVIEW_API_KEY` integrations onto PR-Agent's `OPENAI.*` and `config.*` settings.
|
||||
- If you change review defaults, update the workflow or `.pr_agent.toml` alongside the contributor or architecture docs in the same PR.
|
||||
@@ -129,16 +144,35 @@ Use `bun run dev` from the repo root when working on the local dashboard experie
|
||||
|
||||
## Validation
|
||||
|
||||
If you can, run these before you open or update a PR:
|
||||
Run this fast local gate before you open or update a PR:
|
||||
|
||||
```bash
|
||||
bun run format
|
||||
bun run lint:fix
|
||||
bun run validate
|
||||
```
|
||||
|
||||
`bun run validate` is the day-to-day contributor gate. It runs:
|
||||
|
||||
- `typecheck`
|
||||
- `lint`
|
||||
- `format:check`
|
||||
- `test:fast`
|
||||
|
||||
Before you ask for review, or whenever you want the closest local equivalent to PR CI, run:
|
||||
|
||||
```bash
|
||||
bun run validate:ci-parity
|
||||
```
|
||||
|
||||
If you changed the dashboard:
|
||||
`bun run validate:ci-parity` adds:
|
||||
|
||||
- branch freshness check against `origin/dev` or `origin/main`
|
||||
- `build:all`
|
||||
- full non-e2e test suite via `test:all`
|
||||
- `test:e2e` with `CCS_E2E_SKIP_BUILD=1`
|
||||
|
||||
If you changed the dashboard, run the UI gate too:
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
@@ -155,7 +189,17 @@ bun run test:native
|
||||
bun run test:e2e
|
||||
```
|
||||
|
||||
`bun run validate` is the main gate. It covers typechecking, linting, format checks, maintainability checks, and automated tests for the main project.
|
||||
Use `bun run test:e2e` locally before review if you touch command routing, proxy flows, release automation, or workflow wiring and want to reproduce the same CLI e2e lane that PR CI runs.
|
||||
|
||||
### Why Did CI Fail?
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| `format` fails in PR CI | `bun run format` was skipped locally | Run `bun run format`, recommit, push again |
|
||||
| `lint` fails in PR CI | `validate` now uses read-only `lint` | Run `bun run lint:fix`, then rerun `bun run validate` |
|
||||
| `test` fails in PR CI but `validate` passed | The failure is in `test:slow` or `test:e2e` | Run `bun run validate:ci-parity` locally |
|
||||
| Checks stay queued for >10 min | Self-hosted runner is offline | Wait for maintainer intervention; rerunning usually does not help |
|
||||
| `Dev Release` is red on `dev` after merge | Release-only failure or publish problem | Check `Push CI` first to confirm code quality |
|
||||
|
||||
If you cannot run the full suite, that is still fine for early or docs-only PRs. Just say what you did run, or what blocked you, in the PR.
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ ccs ollama "summarize these logs"
|
||||
## Contribute And Report Safely
|
||||
|
||||
- Contributing guide: [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
- Daily local gate: `bun run format && bun run lint:fix && bun run validate` (`validate` is the fast path only)
|
||||
- Before review or merge confidence: `bun run validate:ci-parity`
|
||||
- If PR checks stay queued for more than 10 minutes, assume the self-hosted runner is offline and notify a maintainer instead of retrying blindly
|
||||
- Starter work:
|
||||
[good first issue](https://github.com/kaitranntt/ccs/labels/good%20first%20issue),
|
||||
[help wanted](https://github.com/kaitranntt/ccs/labels/help%20wanted)
|
||||
|
||||
@@ -599,6 +599,7 @@ return (
|
||||
bun run format
|
||||
bun run lint:fix
|
||||
bun run validate
|
||||
bun run validate:ci-parity
|
||||
|
||||
# UI project (if changed)
|
||||
cd ui
|
||||
@@ -611,7 +612,7 @@ bun run validate
|
||||
|
||||
| Project | Command | Checks |
|
||||
|---------|---------|--------|
|
||||
| Main | `bun run validate` | typecheck + lint + format:check + test |
|
||||
| Main | `bun run validate` | typecheck + lint + format:check + test:fast |
|
||||
| UI | `bun run validate` | typecheck + lint + format:check |
|
||||
|
||||
---
|
||||
|
||||
+5
-20
@@ -242,28 +242,13 @@ All criteria achieved:
|
||||
- [x] Clear domain boundaries
|
||||
- [x] Consistent naming conventions
|
||||
|
||||
## Maintainability Gate (Issue #539 Foundation)
|
||||
## Historical Maintainability Gate (Retired)
|
||||
|
||||
- Baseline metrics artifact: `docs/metrics/maintainability-baseline.json`
|
||||
- Branch-aware gate wrapper: `scripts/maintainability-check.js`
|
||||
- Generate or refresh baseline:
|
||||
- `bun run maintainability:baseline`
|
||||
- `npm run maintainability:baseline`
|
||||
- Run regression check gate:
|
||||
- `bun run maintainability:check`
|
||||
- `npm run maintainability:check`
|
||||
- `bun run maintainability:check:strict` (force strict locally)
|
||||
This section is preserved as historical context from the original Issue `#539` work.
|
||||
|
||||
The baseline/check scripts enumerate git-tracked files under `src` for deterministic results and fail fast if git file listing is unavailable.
|
||||
|
||||
Default gate behavior:
|
||||
- strict mode on protected branches (`main`, `dev`, `hotfix/*`, `kai/hotfix-*`)
|
||||
- warning mode on PR CI and non-protected branches (parallel PR friendly)
|
||||
|
||||
The check mode supports a maintainability regression gate that blocks increases in:
|
||||
- `process.exit` references
|
||||
- synchronous fs API references
|
||||
- TypeScript files over 350 LOC
|
||||
- The maintainability baseline gate is no longer part of the active CCS workflow.
|
||||
- Current contributor and CI gates are documented in `CLAUDE.md`, `CONTRIBUTING.md`, and the GitHub workflow files.
|
||||
- Do not assume `maintainability:baseline` or `maintainability:check` exist unless they are reintroduced in a future follow-up.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+4
-2
@@ -70,12 +70,14 @@
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"format": "prettier --write src/",
|
||||
"format:check": "prettier --check src/",
|
||||
"validate": "bun run typecheck && bun run lint:fix && bun run format:check && bun run test:all",
|
||||
"validate": "bun run typecheck && bun run lint && bun run format:check && bun run test:fast",
|
||||
"validate:ci-parity": "bash scripts/ci-parity-gate.sh",
|
||||
"verify:bundle": "node scripts/verify-bundle.js",
|
||||
"test": "bun run build && bun run test:all",
|
||||
"test:ci": "bun run test:all",
|
||||
"test:all": "bun test --max-concurrency=1 tests/unit tests/integration tests/npm",
|
||||
"test:fast": "node scripts/run-test-bucket.js fast",
|
||||
"test:slow": "node scripts/run-test-bucket.js slow",
|
||||
"test:all": "node scripts/run-test-bucket.js all",
|
||||
"test:unit": "bun test tests/unit",
|
||||
"test:npm": "bun test tests/npm/",
|
||||
"test:native": "bash tests/native/unix/edge-cases.sh",
|
||||
|
||||
@@ -55,8 +55,14 @@ if git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[i] Running CI-equivalent local checks..."
|
||||
echo "[i] Running CI-parity local checks..."
|
||||
# `set -euo pipefail` above makes every step fail fast. Keep these commands
|
||||
# explicit so parity drift is visible when CI changes.
|
||||
bun run typecheck
|
||||
bun run lint
|
||||
bun run format:check
|
||||
bun run build:all
|
||||
bun run validate
|
||||
bun run test:all
|
||||
CCS_E2E_SKIP_BUILD=1 bun run test:e2e
|
||||
|
||||
echo "[OK] CI parity gate passed."
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const candidateRoots = ['tests/unit', 'tests/integration', 'tests/npm'];
|
||||
// Add a `.ts` test to `slowTests` when ANY of these apply:
|
||||
// 1. It spawns a child process (CLI, bun test, node, gh, etc.).
|
||||
// 2. It binds a port, starts a server, or talks to localhost.
|
||||
// 3. It reads a real file from `dist/` or the repo root at runtime.
|
||||
// 4. It waits on a timer > 500ms or a filesystem watcher.
|
||||
// 5. A single run consistently takes > 1500ms on reference hardware.
|
||||
// Tests that literally reference `dist/` in source are auto-forced slow by
|
||||
// `readsBuiltDist`. This list is the manual catch-all for `.ts` tests that
|
||||
// meet the criteria above without the literal `dist/` string.
|
||||
// `tests/unit/scripts/run-test-bucket.test.js` verifies every path here exists
|
||||
// (catches deletion drift) but CANNOT detect new undeclared slow tests.
|
||||
// Automated perf-budget enforcement tracked in issue #1071.
|
||||
const slowTests = [
|
||||
'tests/integration/cursor-daemon-lifecycle.test.ts',
|
||||
'tests/integration/proxy/daemon-lifecycle.test.ts',
|
||||
'tests/unit/commands/persist-command-handler.test.ts',
|
||||
'tests/unit/hooks/ccs-browser-mcp-server.test.ts',
|
||||
'tests/unit/targets/codex-runtime-integration.test.ts',
|
||||
'tests/unit/targets/codex-settings-bridge-launch.test.ts',
|
||||
'tests/unit/targets/droid-command-routing-integration.test.ts',
|
||||
'tests/unit/targets/droid-config-manager.test.ts',
|
||||
'tests/unit/targets/settings-profile-browser-launch.test.ts',
|
||||
'tests/unit/targets/settings-profile-image-analysis-launch.test.ts',
|
||||
'tests/unit/targets/settings-profile-websearch-launch.test.ts',
|
||||
'tests/unit/web-server/cursor-routes.test.ts',
|
||||
'tests/unit/web-server/websearch-routes.test.ts',
|
||||
];
|
||||
// CommonJS-heavy JS suites stay slow by default because many of them mutate
|
||||
// module cache or process state. Opt them into `test:fast` only after they are
|
||||
// proven stable in the mixed fast bucket.
|
||||
const fastJsTests = new Set([
|
||||
'tests/unit/flag-parsing-simple.test.js',
|
||||
]);
|
||||
|
||||
const filePattern = /(\.test\.(c|m)?[jt]s|\.spec\.(c|m)?[jt]s|-test\.(c|m)?[jt]s)$/;
|
||||
|
||||
function collectFiles(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
collectFiles(fullPath, files);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filePattern.test(entry.name)) {
|
||||
files.push(path.relative(rootDir, fullPath).split(path.sep).join('/'));
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function readsBuiltDist(relativePath) {
|
||||
const source = fs.readFileSync(path.join(rootDir, relativePath), 'utf8');
|
||||
return source.includes('dist/');
|
||||
}
|
||||
|
||||
function getDiscoveredTests() {
|
||||
return candidateRoots
|
||||
.flatMap((relativeDir) => collectFiles(path.join(rootDir, relativeDir)))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function shouldForceSlow(file) {
|
||||
if (file.startsWith('tests/npm/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (/\.(c|m)?js$/.test(file) && !fastJsTests.has(file)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return readsBuiltDist(file);
|
||||
}
|
||||
|
||||
function getSlowSet() {
|
||||
const discovered = getDiscoveredTests();
|
||||
const forceSlow = discovered.filter((file) => shouldForceSlow(file));
|
||||
return new Set([...slowTests, ...forceSlow]);
|
||||
}
|
||||
|
||||
function selectBucket(name) {
|
||||
const discovered = getDiscoveredTests();
|
||||
const slowSet = getSlowSet();
|
||||
|
||||
return name === 'slow'
|
||||
? [...slowSet].sort()
|
||||
: discovered.filter((file) => !slowSet.has(file));
|
||||
}
|
||||
|
||||
function ensureBuildForSlowBucket() {
|
||||
if (fs.existsSync(path.join(rootDir, 'dist', 'ccs.js'))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const build = spawnSync('bun', ['run', 'build'], {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
return build.status ?? 1;
|
||||
}
|
||||
|
||||
function runBucket(name) {
|
||||
const selected = selectBucket(name);
|
||||
|
||||
if (selected.length === 0) {
|
||||
console.error(`[X] No tests matched the '${name}' bucket.`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (name === 'slow') {
|
||||
const buildStatus = ensureBuildForSlowBucket();
|
||||
if (buildStatus !== 0) {
|
||||
return buildStatus;
|
||||
}
|
||||
}
|
||||
|
||||
// Slow bucket forces sequential execution because it spawns subprocesses,
|
||||
// binds ports, and touches shared state — parallelism causes flakes.
|
||||
// Fast bucket keeps bun's default parallelism for speed.
|
||||
const bunArgs = name === 'slow'
|
||||
? ['test', '--max-concurrency=1', ...selected]
|
||||
: ['test', ...selected];
|
||||
|
||||
const result = spawnSync('bun', bunArgs, {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
return result.status ?? 1;
|
||||
}
|
||||
|
||||
function main(args = process.argv.slice(2)) {
|
||||
const bucket = args[0];
|
||||
|
||||
if (!['fast', 'slow', 'all'].includes(bucket)) {
|
||||
console.error('[X] Usage: node scripts/run-test-bucket.js <fast|slow|all>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (bucket === 'all') {
|
||||
let exitCode = 0;
|
||||
|
||||
for (const name of ['fast', 'slow']) {
|
||||
const status = runBucket(name);
|
||||
if (status !== 0) {
|
||||
exitCode = status;
|
||||
}
|
||||
}
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
return runBucket(bucket);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.exit(main());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
slowTests,
|
||||
fastJsTests,
|
||||
readsBuiltDist,
|
||||
shouldForceSlow,
|
||||
getDiscoveredTests,
|
||||
getSlowSet,
|
||||
selectBucket,
|
||||
main,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
function resolvePath(relativePath: string) {
|
||||
return path.resolve(import.meta.dir, relativePath);
|
||||
}
|
||||
|
||||
describe('pr ci workflow', () => {
|
||||
test('keeps full coverage on pull requests', () => {
|
||||
const workflowPath = resolvePath('../../../../.github/workflows/ci.yml');
|
||||
|
||||
expect(fs.existsSync(workflowPath)).toBe(true);
|
||||
|
||||
const workflow = fs.readFileSync(workflowPath, 'utf8');
|
||||
|
||||
expect(workflow).toContain('name: CI');
|
||||
expect(workflow).toContain('pull_request:');
|
||||
expect(workflow).toContain('branches: [main, dev]');
|
||||
expect(workflow).toContain('group: ci-${{ github.ref }}');
|
||||
expect(workflow).toContain('cancel-in-progress: true');
|
||||
expect(workflow).toContain('fail-fast: false');
|
||||
expect(workflow).toContain('runs-on: [self-hosted, linux, x64]');
|
||||
expect(workflow).toContain("cmd: 'bun run typecheck'");
|
||||
expect(workflow).toContain("cmd: 'bun run lint'");
|
||||
expect(workflow).toContain("cmd: 'bun run format:check'");
|
||||
expect(workflow).toContain("key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }}");
|
||||
expect(workflow).not.toContain('restore-keys:');
|
||||
expect(workflow).toContain('name: dist');
|
||||
expect(workflow).toContain('path: dist/');
|
||||
expect(workflow).toContain('needs: [build]');
|
||||
expect(workflow).toContain('run: bun run test:all');
|
||||
expect(workflow).toContain("CCS_E2E_SKIP_BUILD: '1'");
|
||||
expect(workflow).toContain('run: bun run test:e2e');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
function resolvePath(relativePath: string) {
|
||||
return path.resolve(import.meta.dir, relativePath);
|
||||
}
|
||||
|
||||
describe('push ci workflow', () => {
|
||||
test('keeps dev push quality checks separate from release automation', () => {
|
||||
const workflowPath = resolvePath('../../../../.github/workflows/push-ci.yml');
|
||||
|
||||
expect(fs.existsSync(workflowPath)).toBe(true);
|
||||
|
||||
const workflow = fs.readFileSync(workflowPath, 'utf8');
|
||||
|
||||
expect(workflow).toContain('name: Push CI');
|
||||
expect(workflow).toContain('push:');
|
||||
expect(workflow).toContain('branches: [dev]');
|
||||
expect(workflow).toContain('group: push-ci-${{ github.ref }}');
|
||||
expect(workflow).toContain('cancel-in-progress: true');
|
||||
expect(workflow).toContain('runs-on: [self-hosted, linux, x64]');
|
||||
expect(workflow).toContain("key: ${{ runner.os }}-bun-cache-v2-${{ hashFiles('bun.lock', 'ui/bun.lock') }}");
|
||||
expect(workflow).not.toContain('restore-keys:');
|
||||
expect(workflow).toContain("name: ${{ matrix.check.name }}");
|
||||
expect(workflow).toContain("cmd: 'bun run typecheck'");
|
||||
expect(workflow).toContain("cmd: 'bun run lint'");
|
||||
expect(workflow).toContain("cmd: 'bun run format:check'");
|
||||
expect(workflow).toContain('run: bun run build:all');
|
||||
expect(workflow).toContain('run: bun run test:all');
|
||||
expect(workflow).toContain("CCS_E2E_SKIP_BUILD: '1'");
|
||||
expect(workflow).toContain('run: bun run test:e2e');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
const { describe, expect, test } = require('bun:test');
|
||||
const path = require('node:path');
|
||||
const bucket = require('../../../scripts/run-test-bucket.js');
|
||||
|
||||
describe('run-test-bucket', () => {
|
||||
test('all declared slow tests still exist on disk', () => {
|
||||
for (const relativePath of bucket.slowTests) {
|
||||
const absolutePath = path.resolve(__dirname, '../../../', relativePath);
|
||||
expect(Bun.file(absolutePath).exists()).resolves.toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('forces npm tests into the slow bucket', () => {
|
||||
expect(bucket.shouldForceSlow('tests/npm/cli.test.js')).toBe(true);
|
||||
});
|
||||
|
||||
test('keeps dist-independent javascript tests in the fast bucket', () => {
|
||||
expect(bucket.shouldForceSlow('tests/unit/flag-parsing-simple.test.js')).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps non-allowlisted javascript tests in the slow bucket', () => {
|
||||
expect(bucket.shouldForceSlow('tests/unit/commands/persist-command.test.js')).toBe(true);
|
||||
});
|
||||
|
||||
test('still forces dist-dependent tests into the slow bucket', () => {
|
||||
expect(bucket.shouldForceSlow('tests/unit/config-dir-override.test.js')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user