diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..69cc80c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.githooks/* text eol=lf diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..70f2a64 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,2 @@ +#!/bin/sh +exec just pre-commit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a9d32c..b711091 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,29 +32,6 @@ concurrency: cancel-in-progress: true jobs: - quality: - name: Quality - if: ${{ (inputs.profile || 'full') == 'full' }} - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ inputs.source_sha || github.sha }} - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: .node-version - package-manager-cache: false - - run: npm install --global npm@12.0.2 - - run: npm ci - - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 - with: - version: "0.13.0" - - run: npm run format:check - - run: npm run lint - - run: uv run --no-project --python 3.15 python scripts/check_loc.py - correctness: name: Correctness / Package and browser runs-on: ubuntu-latest @@ -82,7 +59,15 @@ jobs: - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 with: just-version: "1.58.0" + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + if: ${{ (inputs.profile || 'full') == 'full' }} + with: + version: "0.13.0" + - name: Quality + if: ${{ (inputs.profile || 'full') == 'full' }} + run: just quality - name: Correctness lint + if: ${{ inputs.profile == 'deploy' }} run: just lint-correctness - run: npm run typecheck - name: Install Chromium @@ -105,7 +90,7 @@ jobs: result: name: Qualification result if: ${{ always() }} - needs: [quality, correctness] + needs: correctness runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -113,14 +98,12 @@ jobs: env: PROFILE: ${{ inputs.profile || 'full' }} SOURCE_SHA: ${{ needs.correctness.outputs.sha || inputs.source_sha || github.sha }} - QUALITY: ${{ needs.quality.result }} CORRECTNESS: ${{ needs.correctness.result }} run: | node <<'NODE' - const { PROFILE, SOURCE_SHA, QUALITY, CORRECTNESS } = process.env; - console.log(`Qualification ${PROFILE}: ${SOURCE_SHA}; Quality=${QUALITY}; Correctness=${CORRECTNESS}`); - const expectedQuality = { full: "success", deploy: "skipped" }[PROFILE]; - if (!expectedQuality || QUALITY !== expectedQuality || CORRECTNESS !== "success") { + const { PROFILE, SOURCE_SHA, CORRECTNESS } = process.env; + console.log(`Qualification ${PROFILE}: ${SOURCE_SHA}; Correctness=${CORRECTNESS}`); + if (!["full", "deploy"].includes(PROFILE) || CORRECTNESS !== "success") { process.exit(1); } NODE diff --git a/AGENTS.md b/AGENTS.md index f35f1e1..45349d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,16 @@ Use `just setup` and `just check` for the shared development baseline; see READM `test:`, `chore:`, `ci:`). - Do not merge with failing or missing required checks. +## Application secrets + +- Use Nextide's Infisical-backed secret service for application-secret editing; + follow the [secrets policy](https://github.com/nextidemedia/meta/blob/main/docs/development-baseline.md#application-secrets) + and the service owner's startup/release contract. Existing services migrate + per owner and environment; `nextide-web` is exempt. Keep environment variables, + nonsecret config, schemas/examples, scoped bootstrap/verifier registries and + independent recovery; retire independent application-secret `.env` copies + after verified migration and the owning rollback window has passed. + ## Runtime checks - Add a validation, guard or limit only when both are true: you can name what @@ -85,6 +95,13 @@ Use `just setup` and `just check` for the shared development baseline; see READM ## Validation +- Expose `just pre-commit` for measured-fast, check-only local feedback and run + it through the tracked Git hook after setup. Reuse prepared installed tools; + never install, fetch, format, fix, or alter source or staging in the hook. + Include type checks or existing tests only when measurements justify their + latency. Consolidate compatible tiny CI checks; CI qualifies committed + contents and retains the complete Quality and Correctness contract. See the + [local feedback and hook setup guide](https://github.com/nextidemedia/meta/blob/main/docs/development-baseline.md#local-feedback-and-git-hooks). - Run targeted local checks as needed; run larger validation remotely on the working branch. Required checks must pass before merge. - If remote validation is blocked, proceed with a reasonable local fallback. diff --git a/README.md b/README.md index 9f7c4c3..0e8e2ad 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,20 @@ Run `npm run dev`, then open [http://127.0.0.1:5174](http://127.0.0.1:5174) to v ## Development checks Install Node 26.9.0 (matching `.node-version` and CI), npm 12.0.2, `just`, and uv. On Windows, install PowerShell 7.5+ -with `pwsh` available on PATH. - -- `just setup` installs the frozen workspace lockfile. +with `pwsh` available on PATH. Commit checks also need Python 3.15 on PATH. + +- `just setup` installs the frozen workspace lockfile and activates the tracked + commit hook. For an already prepared checkout, run `just hooks-install`. +- `just pre-commit` checks the whole working tree: LOC limits, Prettier, full lint, + the supply-chain watchlist, and TypeScript for the package, playground, and E2E. + It uses installed tools without downloading, formatting, staging, or stashing. + Type checks read source directly; they do not rebuild package declarations. + Packed-package and browser qualification remain separate required CI checks. +- Hook setup preserves custom `core.hooksPath` and existing Git hooks. Integrate + `just pre-commit` into those hooks explicitly. The relative `.githooks` path + applies to linked worktrees too; each worktree needs its own prepared tools. + Whole-tree checks include unstaged edits; use `git -c core.hooksPath=/dev/null commit` + for a deliberate one-off bypass, with CI still required before merge. - `just check` runs Prettier, Vite+ Oxlint, the supply-chain watchlist, LOC budgets, strict TypeScript checks, and packed-consumer qualification. - `just fmt`, `just fmt-check`, `just lint`, and `just typecheck` run separately. @@ -53,8 +64,10 @@ the complete rule sets still run in Quality. Type safety restrictions and the supply-chain watchlist remain required in both profiles. Pull requests, main pushes, and the weekly **Qualification** workflow run full qualification. Manual runs select -`full` or `deploy`; deploy deliberately skips **Quality**. **Qualification result** -requires every selected group to succeed and reports the validated commit. The +`full` or `deploy`; deploy deliberately skips **Quality**. Both profiles share one +Node installation in **Correctness / Package and browser**. Full runs execute +Quality first; its full lint also covers correctness lint and the watchlist. +**Qualification result** requires the selected checks to succeed and reports the validated commit. The package has no service deployment or database integration lane; its integration surface is the packed consumer and playground browser suite. diff --git a/justfile b/justfile index c1812dd..c870352 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,20 @@ _forward_args := if os() == "windows" { "@forwarded" } else { '"$@"' } setup: npm ci + python scripts/install_hooks.py + +hooks-install: + python scripts/install_hooks.py + +pre-commit: + python scripts/check_loc.py + node node_modules/prettier/bin/prettier.cjs --check . + node node_modules/vite-plus/bin/vp lint --deny-warnings + npm run security:watchlist + node node_modules/typescript/bin/tsc --noEmit -p packages/ui/tsconfig.json + node node_modules/typescript/bin/tsc --noEmit -p apps/playground/tsconfig.app.json + node node_modules/typescript/bin/tsc --noEmit -p apps/playground/tsconfig.node.json + node node_modules/typescript/bin/tsc --noEmit -p tsconfig.e2e.json lint: npm run lint diff --git a/scripts/install_hooks.py b/scripts/install_hooks.py new file mode 100644 index 0000000..72bcead --- /dev/null +++ b/scripts/install_hooks.py @@ -0,0 +1,79 @@ +"""Activate the tracked hooks without replacing existing local hooks.""" + +import subprocess +from pathlib import Path + + +def git(*args: str) -> str: + return subprocess.check_output(["git", *args], text=True).strip() + + +def main() -> None: + root = Path(git("rev-parse", "--show-toplevel")) + project = Path(__file__).resolve().parents[1] + if project != root.resolve() or Path.cwd().resolve() != project: + raise SystemExit( + "Run git init in this project, then run hook setup from its root." + ) + configured = subprocess.run( + ["git", "config", "--get", "core.hooksPath"], + capture_output=True, + text=True, + check=False, + ) + if configured.returncode not in (0, 1): + raise SystemExit(configured.stderr) + if configured.returncode == 0: + if configured.stdout.strip() == ".githooks": + return + raise SystemExit( + "Existing core.hooksPath preserved; integrate just pre-commit there explicitly." + ) + default_hooks = Path(git("rev-parse", "--git-path", "hooks")) + hook_names = { + "applypatch-msg", + "pre-applypatch", + "post-applypatch", + "pre-commit", + "pre-merge-commit", + "prepare-commit-msg", + "commit-msg", + "post-commit", + "pre-rebase", + "post-checkout", + "post-merge", + "pre-push", + "pre-receive", + "update", + "proc-receive", + "post-receive", + "post-update", + "reference-transaction", + "push-to-checkout", + "pre-auto-gc", + "post-rewrite", + "sendemail-validate", + "fsmonitor-watchman", + "p4-changelist", + "p4-prepare-changelist", + "p4-post-changelist", + "p4-pre-submit", + "post-index-change", + } + if default_hooks.exists() and any( + hook.name in hook_names for hook in default_hooks.iterdir() + ): + raise SystemExit( + "Existing Git hooks preserved; integrate just pre-commit there explicitly." + ) + hook = root / ".githooks" / "pre-commit" + if not hook.is_file(): + raise SystemExit("Missing tracked .githooks/pre-commit.") + hook.chmod(hook.stat().st_mode | 0o111) + subprocess.run( + ["git", "config", "--local", "core.hooksPath", ".githooks"], check=True + ) + + +if __name__ == "__main__": + main()