feat: service layering and platform hardening #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: SpecCursor Quality Gates | |
| # Honest qualification suite aligned to repository reality. | |
| # What exists today: Node apps/packages (pnpm), optional Rust worker, | |
| # optional Lean workspace. There is no root go.mod / requirements.txt. | |
| # | |
| # Stages that need external secrets (Snyk, live Claude, Morph) are skipped | |
| # with an explicit notice unless the secret is present — never fake-green theater. | |
| # | |
| # Optional repository secret: SNYK_TOKEN (Snyk step in this workflow). | |
| # Deploy-time secrets (ANTHROPIC_API_KEY, GitHub App, JWT_SECRET) are NOT | |
| # injected here — see docs/environment.md and docs/deployment.md. | |
| on: | |
| push: | |
| branches: [main, develop, release/*] | |
| paths: | |
| - 'apps/**' | |
| - 'packages/**' | |
| - 'workers/**' | |
| - 'scripts/**' | |
| - 'tests/**' | |
| - 'load/**' | |
| - 'chaos/**' | |
| - 'security/**' | |
| - '.github/workflows/**' | |
| - 'package.json' | |
| - 'pnpm-workspace.yaml' | |
| - 'pnpm-lock.yaml' | |
| - 'verify-implementation.js' | |
| pull_request: | |
| branches: [main, develop, release/*] | |
| paths: | |
| - 'apps/**' | |
| - 'packages/**' | |
| - 'workers/**' | |
| - 'scripts/**' | |
| - 'tests/**' | |
| - 'load/**' | |
| - 'chaos/**' | |
| - 'security/**' | |
| - '.github/workflows/**' | |
| - 'package.json' | |
| - 'pnpm-workspace.yaml' | |
| - 'pnpm-lock.yaml' | |
| - 'verify-implementation.js' | |
| workflow_dispatch: | |
| inputs: | |
| stage: | |
| description: 'Stage to run' | |
| required: false | |
| default: all | |
| type: choice | |
| options: | |
| - all | |
| - verify | |
| - static-analysis | |
| - unit-tests | |
| - workers | |
| - security | |
| - load-smoke | |
| concurrency: | |
| group: qualify-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| NODE_ENV: test | |
| PNPM_VERSION: '8.15.0' | |
| jobs: | |
| verify: | |
| name: Architecture verify | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.stage == 'all' || github.event.inputs.stage == 'verify' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: pnpm | |
| - run: node verify-implementation.js | |
| static-analysis: | |
| name: Static analysis (Node) | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.stage == 'all' || github.event.inputs.stage == 'static-analysis' }} | |
| needs: verify | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node: [18, 20] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Format check | |
| run: pnpm format:check | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Type check | |
| run: pnpm type-check | |
| unit-tests: | |
| name: Unit and integration tests | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.stage == 'all' || github.event.inputs.stage == 'unit-tests' }} | |
| needs: static-analysis | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| node: [18, 20] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node }} | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: Run unit and integration tests | |
| run: pnpm test:unit | |
| - name: Run property tests | |
| run: pnpm test:property | |
| workers: | |
| name: Optional workers (Rust / Lean) | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.stage == 'all' || github.event.inputs.stage == 'workers' }} | |
| needs: verify | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Rust toolchain | |
| if: hashFiles('workers/rust-worker/Cargo.toml') != '' | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| components: clippy | |
| - name: Cargo test + clippy | |
| if: hashFiles('workers/rust-worker/Cargo.toml') != '' | |
| working-directory: workers/rust-worker | |
| run: | | |
| cargo test --verbose | |
| cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Skip Rust (manifest absent) | |
| if: hashFiles('workers/rust-worker/Cargo.toml') == '' | |
| run: echo "workers/rust-worker/Cargo.toml not present; skipping Rust." | |
| - name: Install elan (Lean) | |
| if: hashFiles('workers/lean-engine/lakefile.lean') != '' | |
| run: | | |
| curl -sSf https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s -- -y --default-toolchain none | |
| echo "$HOME/.elan/bin" >> "$GITHUB_PATH" | |
| - name: Lean build / check | |
| if: hashFiles('workers/lean-engine/lakefile.lean') != '' | |
| working-directory: workers/lean-engine | |
| run: | | |
| lake build | |
| if [ -f lean/test_runner.lean ]; then | |
| lake env lean --run lean/test_runner.lean || true | |
| fi | |
| - name: Skip Lean (lakefile absent) | |
| if: hashFiles('workers/lean-engine/lakefile.lean') == '' | |
| run: echo "workers/lean-engine/lakefile.lean not present; skipping Lean." | |
| security: | |
| name: Dependency security | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.stage == 'all' || github.event.inputs.stage == 'security' }} | |
| needs: verify | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: ${{ env.PNPM_VERSION }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - name: pnpm audit + ecosystem audits | |
| continue-on-error: true | |
| run: | | |
| pnpm audit --audit-level moderate | |
| node scripts/security-audit.mjs | |
| - name: Snyk (optional — requires SNYK_TOKEN secret) | |
| env: | |
| SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} | |
| run: | | |
| if [ -z "$SNYK_TOKEN" ]; then | |
| echo "SNYK_TOKEN not configured; Snyk scan skipped (optional gate)." | |
| exit 0 | |
| fi | |
| npx snyk test --severity-threshold=high | |
| - name: Trivy filesystem scan | |
| uses: aquasecurity/trivy-action@0.28.0 | |
| with: | |
| scan-type: fs | |
| scan-ref: . | |
| severity: CRITICAL,HIGH | |
| exit-code: '1' | |
| ignore-unfixed: true | |
| load-smoke: | |
| name: Load script smoke (syntax) | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.stage == 'all' || github.event.inputs.stage == 'load-smoke' }} | |
| needs: unit-tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Parse load scripts | |
| run: | | |
| if [ -f load/upgrade-workflow.js ]; then | |
| node --check load/upgrade-workflow.js | |
| else | |
| echo "No load scripts; skipping." | |
| fi | |
| - name: Chaos experiment files present | |
| run: | | |
| if [ -d chaos/experiments ]; then | |
| find chaos/experiments -type f -name '*.yaml' -o -name '*.yml' | tee /tmp/chaos.txt | |
| test -s /tmp/chaos.txt | |
| else | |
| echo "No chaos experiments directory; skipping." | |
| fi | |
| qualify-summary: | |
| name: Qualify summary | |
| if: always() | |
| needs: [verify, static-analysis, unit-tests, workers, security, load-smoke] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Require critical jobs | |
| run: | | |
| echo "verify=${{ needs.verify.result }}" | |
| echo "static=${{ needs.static-analysis.result }}" | |
| echo "tests=${{ needs.unit-tests.result }}" | |
| echo "workers=${{ needs.workers.result }}" | |
| echo "security=${{ needs.security.result }}" | |
| echo "load=${{ needs.load-smoke.result }}" | |
| for r in "${{ needs.verify.result }}" "${{ needs.static-analysis.result }}" "${{ needs.unit-tests.result }}" "${{ needs.security.result }}"; do | |
| if [ "$r" != "success" ] && [ "$r" != "skipped" ]; then | |
| echo "Critical gate failed: $r" | |
| exit 1 | |
| fi | |
| done | |
| # workers/load are best-effort when toolchains flake; fail only on hard failure | |
| if [ "${{ needs.workers.result }}" = "failure" ]; then exit 1; fi | |
| if [ "${{ needs.load-smoke.result }}" = "failure" ]; then exit 1; fi |