diff --git a/.github/actions/detect-docs-only/action.yml b/.github/actions/detect-docs-changes/action.yml similarity index 76% rename from .github/actions/detect-docs-only/action.yml rename to .github/actions/detect-docs-changes/action.yml index 5bdc5d7d89b..853442a7783 100644 --- a/.github/actions/detect-docs-only/action.yml +++ b/.github/actions/detect-docs-changes/action.yml @@ -8,6 +8,9 @@ outputs: docs_only: description: "'true' if all changes are docs/markdown, 'false' otherwise" value: ${{ steps.check.outputs.docs_only }} + docs_changed: + description: "'true' if any changed file is under docs/ or is markdown" + value: ${{ steps.check.outputs.docs_changed }} runs: using: composite @@ -28,9 +31,18 @@ runs: CHANGED=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "UNKNOWN") if [ "$CHANGED" = "UNKNOWN" ] || [ -z "$CHANGED" ]; then echo "docs_only=false" >> "$GITHUB_OUTPUT" + echo "docs_changed=false" >> "$GITHUB_OUTPUT" exit 0 fi + # Check if any changed file is a doc + DOCS=$(echo "$CHANGED" | grep -E '^docs/|\.md$|\.mdx$' || true) + if [ -n "$DOCS" ]; then + echo "docs_changed=true" >> "$GITHUB_OUTPUT" + else + echo "docs_changed=false" >> "$GITHUB_OUTPUT" + fi + # Check if all changed files are docs or markdown NON_DOCS=$(echo "$CHANGED" | grep -vE '^docs/|\.md$|\.mdx$' || true) if [ -z "$NON_DOCS" ]; then diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a98f435cdf5..a29955c81d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: runs-on: ubuntu-latest outputs: docs_only: ${{ steps.check.outputs.docs_only }} + docs_changed: ${{ steps.check.outputs.docs_changed }} steps: - name: Checkout uses: actions/checkout@v4 @@ -25,7 +26,7 @@ jobs: - name: Detect docs-only changes id: check - uses: ./.github/actions/detect-docs-only + uses: ./.github/actions/detect-docs-changes # Detect which heavy areas are touched so PRs can skip unrelated expensive jobs. # Push to main keeps broad coverage. @@ -120,7 +121,7 @@ jobs: # Build dist once for Node-relevant changes and share it with downstream jobs. build-artifacts: - needs: [docs-scope, changed-scope, code-size, check-lint] + needs: [docs-scope, changed-scope, code-analysis, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') runs-on: blacksmith-4vcpu-ubuntu-2404 steps: @@ -144,9 +145,10 @@ jobs: path: dist/ retention-days: 1 - install-check: - needs: [docs-scope, changed-scope, code-size, check-lint] - if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') + # Validate npm pack contents after build. + release-check: + needs: [docs-scope, build-artifacts] + if: needs.docs-scope.outputs.docs_only != 'true' runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout @@ -159,8 +161,17 @@ jobs: with: install-bun: "false" + - name: Download dist artifact + uses: actions/download-artifact@v4 + with: + name: dist-build + path: dist/ + + - name: Check release contents + run: pnpm release:check + checks: - needs: [docs-scope, changed-scope, code-size, check-lint] + needs: [docs-scope, changed-scope, code-analysis, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') runs-on: blacksmith-4vcpu-ubuntu-2404 strategy: @@ -188,9 +199,9 @@ jobs: - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) run: ${{ matrix.command }} - # Format check — cheapest gate (~43s). Always runs, even on docs-only changes. - check-format: - name: "check: format" + # Types, lint, and format check. + check: + name: "check" runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout @@ -201,31 +212,30 @@ jobs: - name: Setup Node environment uses: ./.github/actions/setup-node-env - - name: Check formatting - run: pnpm format:check - - # Lint check — runs after format passes for cleaner output. - check-lint: - name: "check: lint" - needs: [check-format] - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: false - - - name: Setup Node environment - uses: ./.github/actions/setup-node-env - - - name: Check types and lint + - name: Check types and lint and oxfmt run: pnpm check + # Validate docs (format, lint, broken links) only when docs files changed. + check-docs: + needs: [docs-scope] + if: needs.docs-scope.outputs.docs_changed == 'true' + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: false + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + + - name: Check docs + run: pnpm check:docs + # Check for files that grew past LOC threshold in this PR (delta-only). # On push events, all steps are skipped and the job passes (no-op). # Heavy downstream jobs depend on this to fail fast on violations. - code-size: - needs: [check-format] + code-analysis: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Checkout @@ -279,7 +289,7 @@ jobs: fi checks-windows: - needs: [docs-scope, changed-scope, build-artifacts, code-size, check-lint] + needs: [docs-scope, changed-scope, build-artifacts, code-analysis, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_node == 'true') runs-on: blacksmith-4vcpu-windows-2025 env: @@ -328,19 +338,6 @@ jobs: Write-Warning "Failed to apply Defender exclusions, continuing. $($_.Exception.Message)" } - - name: Checkout submodules (retry) - run: | - set -euo pipefail - git submodule sync --recursive - for attempt in 1 2 3 4 5; do - if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then - exit 0 - fi - echo "Submodule update failed (attempt $attempt/5). Retrying…" - sleep $((attempt * 10)) - done - exit 1 - - name: Download dist artifact (lint lane) if: matrix.task == 'lint' uses: actions/download-artifact@v4 @@ -400,7 +397,7 @@ jobs: # running 4 separate jobs per PR (as before) starved the queue. One job # per PR allows 5 PRs to run macOS checks simultaneously. macos: - needs: [docs-scope, changed-scope, code-size, check-lint] + needs: [docs-scope, changed-scope, code-analysis, check] if: github.event_name == 'pull_request' && needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_macos == 'true' runs-on: macos-latest steps: @@ -481,19 +478,6 @@ jobs: with: submodules: false - - name: Checkout submodules (retry) - run: | - set -euo pipefail - git submodule sync --recursive - for attempt in 1 2 3 4 5; do - if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then - exit 0 - fi - echo "Submodule update failed (attempt $attempt/5). Retrying…" - sleep $((attempt * 10)) - done - exit 1 - - name: Select Xcode 26.1 run: | sudo xcode-select -s /Applications/Xcode_26.1.app @@ -646,7 +630,7 @@ jobs: PY android: - needs: [docs-scope, changed-scope, code-size, check-lint] + needs: [docs-scope, changed-scope, code-analysis, check] if: needs.docs-scope.outputs.docs_only != 'true' && (github.event_name == 'push' || needs.changed-scope.outputs.run_android == 'true') runs-on: blacksmith-4vcpu-ubuntu-2404 strategy: @@ -663,19 +647,6 @@ jobs: with: submodules: false - - name: Checkout submodules (retry) - run: | - set -euo pipefail - git submodule sync --recursive - for attempt in 1 2 3 4 5; do - if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then - exit 0 - fi - echo "Submodule update failed (attempt $attempt/5). Retrying…" - sleep $((attempt * 10)) - done - exit 1 - - name: Setup Java uses: actions/setup-java@v4 with: diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 1f42d8f4039..e6c0914f018 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -23,7 +23,7 @@ jobs: - name: Detect docs-only changes id: check - uses: ./.github/actions/detect-docs-only + uses: ./.github/actions/detect-docs-changes install-smoke: needs: [docs-scope] diff --git a/docs/ci.md b/docs/ci.md index 4fa9579c91d..5bec6922f7c 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -19,10 +19,10 @@ Tier 1 — Cheapest gates (parallel, ~43 s) check-format secrets Tier 2 — After format (parallel, ~2 min) - check-lint code-size + check-lint code-analysis Tier 3 — Build (~3 min) - build-artifacts install-check + build-artifacts release-check Tier 4 — Tests (~5 min) checks (node tsgo / test / protocol, bun test) @@ -39,8 +39,8 @@ Tier 5 — Platform (most expensive) ``` docs-scope ──► changed-scope ──┐ │ -check-format ──► check-lint ──►├──► build-artifacts ──► checks-windows - ├─► code-size ──►├──► install-check +check-format ──► check-lint ──►├──► build-artifacts ──► release-check + ├─► code-analysis ►│ └──► checks-windows ├──► checks ├──► macos └──► android @@ -65,37 +65,37 @@ secrets (independent) ### Tier 2 — After Format -| Job | Runner | Depends on | Purpose | -| ------------ | ----------------- | -------------- | ----------------------------------------------------------- | -| `check-lint` | Blacksmith 4 vCPU | `check-format` | Runs `pnpm lint` — cleaner output after format passes | -| `code-size` | Blacksmith 4 vCPU | `check-format` | Checks LOC thresholds — accurate counts need formatted code | +| Job | Runner | Depends on | Purpose | +| --------------- | ----------------- | -------------- | ----------------------------------------------------------- | +| `check-lint` | Blacksmith 4 vCPU | `check-format` | Runs `pnpm lint` — cleaner output after format passes | +| `code-analysis` | Blacksmith 4 vCPU | `check-format` | Checks LOC thresholds — accurate counts need formatted code | ### Tier 3 — Build -| Job | Runner | Depends on | Purpose | -| ----------------- | ----------------- | ------------------------- | ------------------------------------- | -| `build-artifacts` | Blacksmith 4 vCPU | `check-lint`, `code-size` | Builds dist and uploads artifact | -| `install-check` | Blacksmith 4 vCPU | `check-lint`, `code-size` | Verifies `pnpm install` works cleanly | +| Job | Runner | Depends on | Purpose | +| ----------------- | ----------------- | ----------------------------- | -------------------------------- | +| `build-artifacts` | Blacksmith 4 vCPU | `check-lint`, `code-analysis` | Builds dist and uploads artifact | +| `release-check` | Blacksmith 4 vCPU | `build-artifacts` | Validates npm pack contents | ### Tier 4+ — Tests and Platform -| Job | Runner | Depends on | Purpose | -| ---------------- | ------------------ | -------------------------------------------- | ------------------------------------------------------ | -| `checks` | Blacksmith 4 vCPU | `check-lint`, `code-size` | TypeScript checks, tests (Node + Bun), protocol checks | -| `checks-windows` | Blacksmith Windows | `build-artifacts`, `check-lint`, `code-size` | Windows-specific lint, tests, protocol checks | -| `macos` | `macos-latest` | `check-lint`, `code-size` | TS tests + Swift lint/build/test (PR only) | -| `android` | Blacksmith 4 vCPU | `check-lint`, `code-size` | Gradle test + build | +| Job | Runner | Depends on | Purpose | +| ---------------- | ------------------ | ------------------------------------------------ | ------------------------------------------------------ | +| `checks` | Blacksmith 4 vCPU | `check-lint`, `code-analysis` | TypeScript checks, tests (Node + Bun), protocol checks | +| `checks-windows` | Blacksmith Windows | `build-artifacts`, `check-lint`, `code-analysis` | Windows-specific lint, tests, protocol checks | +| `macos` | `macos-latest` | `check-lint`, `code-analysis` | TS tests + Swift lint/build/test (PR only) | +| `android` | Blacksmith 4 vCPU | `check-lint`, `code-analysis` | Gradle test + build | -## Code-Size Gate +## Code-Analysis Gate -The `code-size` job runs `scripts/analyze_code_files.py` on PRs to catch: +The `code-analysis` job runs `scripts/analyze_code_files.py` on PRs to catch: 1. **Threshold crossings** — files that grew past 1000 lines in the PR 2. **Already-large files growing** — files already over 1000 lines that got bigger 3. **Duplicate function regressions** — new duplicate functions introduced by the PR When `--strict` is set, any violation fails the job and blocks all downstream -work. On push to `main`, the code-size steps are skipped (the job passes as a +work. On push to `main`, the code-analysis steps are skipped (the job passes as a no-op) so pushes still run the full test suite. ### Excluded Directories @@ -109,26 +109,25 @@ The analysis skips: `node_modules`, `dist`, `vendor`, `.git`, `coverage`, **Bad PR (formatting violations):** - `check-format` fails at ~43 s -- `check-lint`, `code-size`, and all downstream jobs never start +- `check-lint`, `code-analysis`, and all downstream jobs never start - Total cost: ~1 runner-minute **Bad PR (lint or LOC violations, good format):** -- `check-format` passes → `check-lint` and `code-size` run in parallel +- `check-format` passes → `check-lint` and `code-analysis` run in parallel - One or both fail → all downstream jobs skipped - Total cost: ~3 runner-minutes **Good PR:** - Critical path: `check-format` (43 s) → `check-lint` (1m 46 s) → `build-artifacts` → `checks` -- `code-size` runs in parallel with `check-lint`, adding no latency +- `code-analysis` runs in parallel with `check-lint`, adding no latency ## Composite Action The `setup-node-env` composite action (`.github/actions/setup-node-env/`) handles the shared setup boilerplate: -- Submodule init/update with retry (5 attempts, exponential backoff) - Node.js 22 setup - pnpm via corepack + store cache - Optional Bun install @@ -141,7 +140,7 @@ This eliminates ~40 lines of duplicated YAML per job. ## Push vs PR Behavior -| Trigger | `code-size` | Downstream jobs | +| Trigger | `code-analysis` | Downstream jobs | | -------------- | ----------------------------- | --------------------- | | Push to `main` | Steps skipped (job passes) | Run normally | | Pull request | Full analysis with `--strict` | Blocked on violations | diff --git a/package.json b/package.json index f78d0d2c857..00c119cd79a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "build": "pnpm canvas:a2ui:bundle && tsdown && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-compat.ts", "build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json", "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh", - "check": "pnpm tsgo && pnpm lint && pnpm format:check", + "check": "pnpm format:check && pnpm tsgo && pnpm lint", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:build", "check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500", "dev": "node scripts/run-node.mjs", diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts index 726b9a9c6bf..ed23f93d772 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts @@ -17,7 +17,6 @@ describe("isBillingErrorMessage", () => { "Payment Required", "HTTP 402 Payment Required", "plans & billing", - "billing: please upgrade your plan", ]; for (const sample of samples) { expect(isBillingErrorMessage(sample)).toBe(true);