From 37440d08b0b0c3a59c21cadd4ff6be804a6924a6 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:29:06 +0000 Subject: [PATCH] chore(governance): seed lint/test toolchain configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four lint/test toolchain files split out of the org auto-seed PR (#233), which the PR-size gate blocked at 1213 reviewable added lines (limit 1000). This sibling carries the toolchain half; #233 keeps the governance, template, and community files. Seeded here, content identical to the seed except the security hardening already applied on the #233 branch: - biome.json + .biomeignore — Biome as the single JS/TS/JSON formatter and linter owner (biome_default workspace class) - .vscode/extensions.json — recommends biome + ruff; unrecommends eslint + prettier so no second formatter owner appears (tracked with an explicit add -f, matching the seed: the repo .gitignore excludes .vscode/ but the org pack intentionally ships this one file) - .github/workflows/l9-lint-test-node.yml — org Node lint/test workflow, hardened: installs require a lockfile and run with --ignore-scripts, and the tsc step uses `npx --no-install` so only the lockfile-resolved compiler can run (Sonar S6505/S8543, Scorecard Pinned-Dependencies). Both Node jobs skip on this Python-only repo via the detect-node guard. --- .biomeignore | 6 + .github/workflows/l9-lint-test-node.yml | 225 ++++++++++++++++++++++++ .vscode/extensions.json | 10 ++ biome.json | 169 ++++++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 .biomeignore create mode 100644 .github/workflows/l9-lint-test-node.yml create mode 100644 .vscode/extensions.json create mode 100644 biome.json diff --git a/.biomeignore b/.biomeignore new file mode 100644 index 0000000..bddc75c --- /dev/null +++ b/.biomeignore @@ -0,0 +1,6 @@ +# Biome path exclusions (gitignore syntax). +# Biome 2.5 reads files.includes in biome.json, not this file — keep generated trees in both. + +coverage/ +dist/ +.l9/ diff --git a/.github/workflows/l9-lint-test-node.yml b/.github/workflows/l9-lint-test-node.yml new file mode 100644 index 0000000..172c311 --- /dev/null +++ b/.github/workflows/l9-lint-test-node.yml @@ -0,0 +1,225 @@ +# L9 consumer lint + test workflow for Node.js / TypeScript +# +# Copy into the CONSUMER repo at `.github/workflows/l9-lint-test-node.yml`. +# Org-distributed copy of Quantum-L9/l9-ci-core presets/typescript +# `.github/workflows/l9-lint-test.yml`. Do not invent a second lint owner. +# +# Formatter/linter ownership: Biome owns JS/TS/JSON (format + lint) via the +# SDK-owned reusable workflow below. ESLint is NOT a second formatter owner +# here. Type checking (tsc) and the repository test suite stay in this repo. +# +# Org-seed safety (Cursor-Governance#276 remediating): +# - Biome always runs. It needs no Node package. +# - `typecheck` / `test` run only when a root `package.json` exists. +# `actions/setup-node` with `cache: npm` hard-fails +# ("Dependencies lock file is not found") before any skip guard, so those +# jobs must not start on Python-only / no-Node consumers. +# - The test job is named `Node Test Suite`, not `Test Suite`, so it cannot +# collide with a required Python pytest context. +# +# Conventions: +# - Immutable event-revision checkout (no floating action ref) +# - SDK reusable workflow pinned to a full 40-char commit SHA +# - Least privilege (contents: read only) +name: L9 Lint and Test (Node) +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +# ── CONFIGURABLE: agent sets these during activation ─────────────────────────── +env: + NODE_VERSION: "20" + PACKAGE_MANAGER: "npm" + SOURCE_DIR: "." + HAS_TYPESCRIPT: "true" + +permissions: + contents: read + +concurrency: + group: l9-lint-test-node-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Biome (format + lint + import organization) is owned by the SDK reusable + # workflow — a single read-only gate with a checksum-verified biome binary + # and zero external actions. Reusable-workflow inputs cannot read `env:`, so + # scan-path and the rollout flag are set literally here. + biome: + name: Biome (format + lint) — SDK-owned + permissions: + contents: read + uses: Quantum-L9/l9-ci-sdk/.github/workflows/l9-biome-scan.yml@f546f122d33601ea5a4b2592e3482c5c39eddd82 + with: + scan-path: "." + # Advisory-to-blocking rollout: false = full scan + annotate + exit 0. + # Flip to true to make Biome findings blocking once the repo is clean. + enforce-biome: false + + detect-node: + name: Detect Node package + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + outputs: + has_package: ${{ steps.detect.outputs.has_package }} + steps: + - name: Checkout immutable event revision + env: + REPOSITORY: ${{ github.repository }} + REVISION: ${{ github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git init . + git remote add origin "https://github.com/${REPOSITORY}.git" + auth_header="Authorization: Basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 | tr -d '\n')" + git -c protocol.version=2 \ + -c "http.https://github.com/.extraheader=${auth_header}" \ + fetch --depth=1 origin "${REVISION}" + git checkout --detach FETCH_HEAD + + - id: detect + name: Detect root package.json + run: | + set -euo pipefail + if [ -f package.json ]; then + echo "has_package=true" >> "$GITHUB_OUTPUT" + else + echo "has_package=false" >> "$GITHUB_OUTPUT" + echo "::notice::no root package.json; skipping Node typecheck and tests" + fi + + typecheck: + name: Type Check (tsc --noEmit) + needs: detect-node + if: needs.detect-node.outputs.has_package == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout immutable event revision + env: + REPOSITORY: ${{ github.repository }} + REVISION: ${{ github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git init . + git remote add origin "https://github.com/${REPOSITORY}.git" + # Authenticate with a per-fetch Authorization header instead of + # embedding the token in the remote URL, so the credential is never + # written to the remote URL or .git/config (mirrors actions/checkout). + auth_header="Authorization: Basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 | tr -d '\n')" + git -c protocol.version=2 \ + -c "http.https://github.com/.extraheader=${auth_header}" \ + fetch --depth=1 origin "${REVISION}" + git checkout --detach FETCH_HEAD + + - name: Set up Node + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: ${{ env.NODE_VERSION }} + # Do not set cache: here. setup-node cache hard-fails when the + # matching lockfile is absent. + + - name: Install dependencies + run: | + set -euo pipefail + # Lockfile required so installs resolve only pinned versions (Sonar S8543, + # Scorecard Pinned-Dependencies). --ignore-scripts blocks package lifecycle + # scripts during install (Sonar S6505); a consumer whose dependencies need + # install-time build scripts opts out here deliberately, per repo. + case "${PACKAGE_MANAGER}" in + npm) + [ -f package-lock.json ] || { echo "::error::package-lock.json is required — commit a lockfile"; exit 1; } + npm ci --ignore-scripts ;; + pnpm) + corepack enable + [ -f pnpm-lock.yaml ] || { echo "::error::pnpm-lock.yaml is required — commit a lockfile"; exit 1; } + pnpm install --frozen-lockfile --ignore-scripts ;; + yarn) + corepack enable + [ -f yarn.lock ] || { echo "::error::yarn.lock is required — commit a lockfile"; exit 1; } + yarn install --frozen-lockfile --ignore-scripts ;; + *) echo "::error::unknown PACKAGE_MANAGER ${PACKAGE_MANAGER}" && exit 1 ;; + esac + + - name: tsc --noEmit + if: env.HAS_TYPESCRIPT == 'true' + run: | + set -euo pipefail + if [ -f tsconfig.json ]; then + # --no-install runs the lockfile-resolved local tsc and never fetches a + # floating release from the registry (Sonar S6505/S8543). A repo with a + # tsconfig but no typescript devDependency fails here — add the dep. + npx --no-install tsc --noEmit -p tsconfig.json + else + echo "::notice::no tsconfig.json found, skipping type check" + fi + + test: + name: Node Test Suite + needs: detect-node + if: needs.detect-node.outputs.has_package == 'true' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + steps: + - name: Checkout immutable event revision + env: + REPOSITORY: ${{ github.repository }} + REVISION: ${{ github.sha }} + TOKEN: ${{ github.token }} + run: | + set -euo pipefail + git init . + git remote add origin "https://github.com/${REPOSITORY}.git" + auth_header="Authorization: Basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 | tr -d '\n')" + git -c protocol.version=2 \ + -c "http.https://github.com/.extraheader=${auth_header}" \ + fetch --depth=1 origin "${REVISION}" + git checkout --detach FETCH_HEAD + + - name: Set up Node + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install dependencies + run: | + set -euo pipefail + # Lockfile required so installs resolve only pinned versions (Sonar S8543, + # Scorecard Pinned-Dependencies). --ignore-scripts blocks package lifecycle + # scripts during install (Sonar S6505); a consumer whose dependencies need + # install-time build scripts opts out here deliberately, per repo. + case "${PACKAGE_MANAGER}" in + npm) + [ -f package-lock.json ] || { echo "::error::package-lock.json is required — commit a lockfile"; exit 1; } + npm ci --ignore-scripts ;; + pnpm) + corepack enable + [ -f pnpm-lock.yaml ] || { echo "::error::pnpm-lock.yaml is required — commit a lockfile"; exit 1; } + pnpm install --frozen-lockfile --ignore-scripts ;; + yarn) + corepack enable + [ -f yarn.lock ] || { echo "::error::yarn.lock is required — commit a lockfile"; exit 1; } + yarn install --frozen-lockfile --ignore-scripts ;; + *) echo "::error::unknown PACKAGE_MANAGER ${PACKAGE_MANAGER}" && exit 1 ;; + esac + + - name: Run test suite + env: + CI: "true" + run: | + set -euo pipefail + if [ -f package.json ] && node -e "process.exit(require('./package.json').scripts && require('./package.json').scripts.test ? 0 : 1)" 2>/dev/null; then + ${PACKAGE_MANAGER} test + else + echo "::notice::no test script defined in package.json, skipping" + fi diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..af34433 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "biomejs.biome", + "charliermarsh.ruff" + ], + "unwantedRecommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..b1fe395 --- /dev/null +++ b/biome.json @@ -0,0 +1,169 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.8/schema.json", + "root": true, + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true, + "defaultBranch": "main" + }, + "files": { + "ignoreUnknown": true, + "maxSize": 1048576, + "includes": [ + "**", + "!**/*.jsonc", + "!**/node_modules", + "!**/.venv", + "!**/venv", + "!**/__pycache__", + "!**/.git", + "!**/.ruff_cache", + "!**/.l9", + "!**/dist", + "!**/coverage", + "!**/package-lock.json", + "!**/npm-shrinkwrap.json" + ] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "useEditorconfig": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "trailingNewline": true, + "bracketSpacing": true, + "bracketSameLine": false, + "attributePosition": "auto", + "expand": "auto", + "delimiterSpacing": false + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, + "assist": { + "enabled": true, + "actions": { + "preset": "recommended", + "source": { + "organizeImports": "on", + "useSortedKeys": "off", + "useSortedAttributes": "off", + "useSortedInterfaceMembers": "off", + "useSortedEnumMembers": "on", + "useSortedPackageJson": "on", + "noDuplicateClasses": "on" + } + } + }, + "json": { + "parser": { + "allowComments": false, + "allowTrailingCommas": false + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "trailingNewline": true, + "trailingCommas": "none", + "bracketSpacing": true, + "expand": "auto", + "delimiterSpacing": false + }, + "linter": { + "enabled": true + }, + "assist": { + "enabled": false + } + }, + "javascript": { + "jsxRuntime": "transparent", + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "trailingNewline": true, + "quoteStyle": "double", + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "attributePosition": "auto", + "operatorLinebreak": "after", + "expand": "auto", + "delimiterSpacing": false + }, + "linter": { + "enabled": true + }, + "assist": { + "enabled": true + } + }, + "css": { + "formatter": { + "enabled": false + }, + "linter": { + "enabled": false + }, + "assist": { + "enabled": false + } + }, + "graphql": { + "formatter": { + "enabled": false + }, + "linter": { + "enabled": false + }, + "assist": { + "enabled": false + } + }, + "html": { + "experimentalFullSupportEnabled": false, + "formatter": { + "enabled": false + }, + "linter": { + "enabled": false + }, + "assist": { + "enabled": false + } + }, + "overrides": [ + { + "includes": ["**/*.jsonc", "**/.vscode"], + "json": { + "parser": { + "allowComments": true, + "allowTrailingCommas": true + }, + "formatter": { + "enabled": false + }, + "linter": { + "enabled": false + } + } + } + ] +}