From b02b1f082038e4273eedd1d8092d1d4a9e77b35a Mon Sep 17 00:00:00 2001 From: Prasad Pawar Date: Thu, 14 May 2026 13:38:43 +0530 Subject: [PATCH 1/6] ATLAS-5293: ATLAS UI: Add repo-wide Git hooks and dashboard pre-commit/push verification --- .githooks/pre-commit | 7 + .githooks/pre-push | 7 + dashboard/.githooks/pre-commit | 4 + dashboard/.githooks/pre-push | 3 + dashboard/.husky/pre-commit | 4 + dashboard/docs/GIT_HOOKS.md | 127 +++++++++++++++++ dashboard/lint-staged.config.mjs | 14 ++ .../scripts/check-staged-new-file-license.mjs | 106 ++++++++++++++ dashboard/scripts/git-precommit-verify.mjs | 33 +++++ dashboard/scripts/git-prepush-verify.mjs | 105 ++++++++++++++ dashboard/scripts/install-git-hooks.mjs | 41 ++++++ dashboard/scripts/lib/git-changed-files.mjs | 71 ++++++++++ .../scripts/lib/license-header-policy.mjs | 38 +++++ dashboard/scripts/lib/test-path-helpers.mjs | 131 ++++++++++++++++++ dashboard/scripts/run-precommit-local.mjs | 67 +++++++++ .../git-hooks/check-added-license-generic.mjs | 65 +++++++++ scripts/git-hooks/lib/extra-license-skip.mjs | 46 ++++++ scripts/git-hooks/lib/git-helpers.mjs | 104 ++++++++++++++ scripts/git-hooks/run-precommit.mjs | 104 ++++++++++++++ scripts/git-hooks/run-prepush.mjs | 72 ++++++++++ scripts/git-hooks/syntax-check-staged.mjs | 71 ++++++++++ 21 files changed, 1220 insertions(+) create mode 100755 .githooks/pre-commit create mode 100755 .githooks/pre-push create mode 100755 dashboard/.githooks/pre-commit create mode 100755 dashboard/.githooks/pre-push create mode 100644 dashboard/.husky/pre-commit create mode 100644 dashboard/docs/GIT_HOOKS.md create mode 100644 dashboard/lint-staged.config.mjs create mode 100644 dashboard/scripts/check-staged-new-file-license.mjs create mode 100644 dashboard/scripts/git-precommit-verify.mjs create mode 100644 dashboard/scripts/git-prepush-verify.mjs create mode 100644 dashboard/scripts/install-git-hooks.mjs create mode 100644 dashboard/scripts/lib/git-changed-files.mjs create mode 100644 dashboard/scripts/lib/license-header-policy.mjs create mode 100644 dashboard/scripts/lib/test-path-helpers.mjs create mode 100644 dashboard/scripts/run-precommit-local.mjs create mode 100644 scripts/git-hooks/check-added-license-generic.mjs create mode 100644 scripts/git-hooks/lib/extra-license-skip.mjs create mode 100644 scripts/git-hooks/lib/git-helpers.mjs create mode 100644 scripts/git-hooks/run-precommit.mjs create mode 100644 scripts/git-hooks/run-prepush.mjs create mode 100644 scripts/git-hooks/syntax-check-staged.mjs diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000000..b0db143666e --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +# Atlas monorepo: pre-commit (license, lint, typecheck, syntax; dashboard test guard) +# Skip: SKIP_ATLAS_HOOKS=1 | SKIP_ALL_ATLAS_GIT_HOOKS=1 +set -e +if [ "$SKIP_ATLAS_HOOKS" = "1" ] || [ "$SKIP_ALL_ATLAS_GIT_HOOKS" = "1" ]; then exit 0; fi +ROOT="$(git rev-parse --show-toplevel)" +exec node "$ROOT/scripts/git-hooks/run-precommit.mjs" diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 00000000000..0048cf4e69c --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +# Atlas monorepo: pre-push (dashboard Jest/eslint/build; dashboardv2 + docs build) +# Skip: SKIP_ATLAS_HOOKS=1 | SKIP_ALL_ATLAS_GIT_HOOKS=1 +set -e +if [ "$SKIP_ATLAS_HOOKS" = "1" ] || [ "$SKIP_ALL_ATLAS_GIT_HOOKS" = "1" ]; then exit 0; fi +ROOT="$(git rev-parse --show-toplevel)" +exec node "$ROOT/scripts/git-hooks/run-prepush.mjs" diff --git a/dashboard/.githooks/pre-commit b/dashboard/.githooks/pre-commit new file mode 100755 index 00000000000..c715084e4b2 --- /dev/null +++ b/dashboard/.githooks/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +# Deprecated path: use repo root core.hooksPath=.githooks (see dashboard/scripts/install-git-hooks.mjs). +ROOT="$(git rev-parse --show-toplevel)" +exec "$ROOT/.githooks/pre-commit" diff --git a/dashboard/.githooks/pre-push b/dashboard/.githooks/pre-push new file mode 100755 index 00000000000..536610d2ca2 --- /dev/null +++ b/dashboard/.githooks/pre-push @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +ROOT="$(git rev-parse --show-toplevel)" +exec "$ROOT/.githooks/pre-push" diff --git a/dashboard/.husky/pre-commit b/dashboard/.husky/pre-commit new file mode 100644 index 00000000000..95fe05b179f --- /dev/null +++ b/dashboard/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/sh +# Git hooks: core.hooksPath=.githooks at Atlas repo root (see dashboard npm prepare). +printf '%s\n' '[atlas] Hooks: git config core.hooksPath .githooks OR npm install (in dashboard/)' +exit 0 diff --git a/dashboard/docs/GIT_HOOKS.md b/dashboard/docs/GIT_HOOKS.md new file mode 100644 index 00000000000..b5e204e6dfe --- /dev/null +++ b/dashboard/docs/GIT_HOOKS.md @@ -0,0 +1,127 @@ +# Atlas Git hooks (dashboard, dashboardv2, docs) + +Hooks run **locally** before `git commit` and `git push` so common issues are +caught early. **CI** on the server is still required to enforce merges. + +## One-time setup (per clone) + +From the **Atlas repo root** (`atlas/`, where `.git` lives): + +```bash +git config core.hooksPath .githooks +``` + +Or run **`npm install`** inside **`dashboard/`** — the **`prepare`** script runs +`dashboard/scripts/install-git-hooks.mjs`, which sets **`core.hooksPath=.githooks`** +when Git config is writable. + +Verify: + +```bash +git config --get core.hooksPath +# expect: .githooks +``` + +The active hook scripts live in **`.githooks/`** at the **repository root**. +`dashboard/.githooks/*` only forwards to the root hooks (legacy path compat). + +## What runs when + +### `pre-commit` (root: `scripts/git-hooks/run-precommit.mjs`) + +Runs **only for packages that have staged paths** under that prefix. + +| Area | When staged under … | Checks | +|------|---------------------|--------| +| **dashboard** | `dashboard/` | (1) **UI test guard** — `src/views`, `src/components`, `App.tsx` / `Main.tsx` / `ErrorBoundary.tsx` must include a **staged** test file; (2) **ASF license** on **new** files under `dashboard/src/`; (3) **lint-staged** → ESLint on staged TS/TSX; (4) **`npm run typecheck`** (`tsc --noEmit`). | +| **dashboardv2** | `dashboardv2/` | (1) **ASF license** on **new** `.js`/`.jsx`/`.ts`/`.tsx` (skips `node_modules`, `bin/`, `external_lib`, `.min.js`); (2) **`node --check`** on staged plain `.js` under `dashboardv2/public/js/` (syntax). **No** Jest/test guard (legacy Grunt UI). | +| **docs** | `docs/` | (1) **ASF license** on **new** sources (skips `node_modules`, `site/`, `bin/`, `docz-lib/`); (2) **`node --check`** on staged **plain** `docs/**/*.js` outside theme/webapp JSX trees. | + +### `pre-push` (root: `scripts/git-hooks/run-prepush.mjs`) + +Runs for each package **if commits in the push range** touch that prefix. + +| Area | Checks | +|------|--------| +| **dashboard** | Colocated tests on disk, **`jest --findRelatedTests`**, **`eslint src`**, **`npm run build`**. | +| **dashboardv2** | **`npm run build`** (Grunt). | +| **docs** | **`npm run build`** (Docz). | + +## Skip hooks (emergency / slow machines) + +Disable **everything**: + +```bash +SKIP_ATLAS_HOOKS=1 git commit ... +SKIP_ATLAS_HOOKS=1 git push ... +``` + +Per **package**: + +```bash +SKIP_DASHBOARD_HOOKS=1 git commit ... +SKIP_DASHBOARDV2_HOOKS=1 git commit ... +SKIP_DOCS_HOOKS=1 git commit ... +``` + +**dashboard** only (still documented): + +```bash +SKIP_DASHBOARD_TEST_GUARD=1 git commit ... # staged test file rule +SKIP_DASHBOARD_LICENSE_CHECK=1 git commit ... # ASF on new files under dashboard/src +SKIP_DASHBOARD_TYPECHECK=1 git commit ... # tsc on commit +``` + +**dashboardv2 / docs** ASF license on new files: + +```bash +SKIP_ATLAS_LICENSE_CHECK=1 git commit ... +``` + +Skip **long builds** on push: + +```bash +SKIP_DASHBOARDV2_BUILD=1 git push ... +SKIP_DOCS_BUILD=1 git push ... +``` + +## Manual run (no Git hook) + +From **repo root** `atlas/`: + +```bash +node scripts/git-hooks/run-precommit.mjs +node scripts/git-hooks/run-prepush.mjs +``` + +**dashboard**-only local verify (same as before): + +```bash +cd dashboard && npm run verify:precommit +cd dashboard && npm run verify:prepush +``` + +## Limitations + +- **dashboardv2** has no ESLint in-repo; **`node --check`** only catches **syntax** on selected `.js` paths, not style. +- **docs** JSX/theme files are not run through `node --check`. +- Hooks can be bypassed with env vars; **rely on CI** for PR enforcement. + +## Files (reference) + +| Path | Role | +|------|------| +| `.githooks/pre-commit` | Root hook → `run-precommit.mjs` | +| `.githooks/pre-push` | Root hook → `run-prepush.mjs` | +| `scripts/git-hooks/run-precommit.mjs` | Monorepo pre-commit orchestration | +| `scripts/git-hooks/run-prepush.mjs` | Monorepo pre-push orchestration | +| `scripts/git-hooks/check-added-license-generic.mjs` | ASF header for v2/docs new files | +| `scripts/git-hooks/syntax-check-staged.mjs` | `node --check` for v2/docs | +| `scripts/git-hooks/lib/git-helpers.mjs` | `git diff` helpers | +| `scripts/git-hooks/lib/extra-license-skip.mjs` | Path skip rules for v2/docs | +| `dashboard/scripts/install-git-hooks.mjs` | Sets `core.hooksPath=.githooks` | +| `dashboard/scripts/git-precommit-verify.mjs` | Dashboard staged UI ↔ test guard | +| `dashboard/scripts/check-staged-new-file-license.mjs` | Dashboard ASF on new files | +| `dashboard/scripts/git-prepush-verify.mjs` | Dashboard Jest, ESLint, build | +| `dashboard/scripts/run-precommit-local.mjs` | `npm run verify:precommit` (dashboard only) | +| `dashboard/lint-staged.config.mjs` | ESLint on staged dashboard sources | diff --git a/dashboard/lint-staged.config.mjs b/dashboard/lint-staged.config.mjs new file mode 100644 index 00000000000..70527d77049 --- /dev/null +++ b/dashboard/lint-staged.config.mjs @@ -0,0 +1,14 @@ +/** + * lint-staged config: Git reports paths as dashboard/src/... from repo root; + * ESLint runs with cwd = dashboard, so strip the dashboard/ prefix. + */ + +export default { + 'dashboard/src/**/*.{ts,tsx}': (filenames) => { + if (filenames.length === 0) { + return process.platform === 'win32' ? 'node -e "process.exit(0)"' : 'true' + } + const relative = filenames.map((f) => f.replace(/^dashboard\//, '')) + return `eslint --max-warnings 200 ${relative.join(' ')}` + }, +} diff --git a/dashboard/scripts/check-staged-new-file-license.mjs b/dashboard/scripts/check-staged-new-file-license.mjs new file mode 100644 index 00000000000..b3dff7048b2 --- /dev/null +++ b/dashboard/scripts/check-staged-new-file-license.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/** + * Pre-commit: newly added (staged) source files under dashboard/src must carry + * the standard ASF header (same policy as apache-license-header.test.ts). + * + * Skip: SKIP_DASHBOARD_HOOKS=1 | SKIP_DASHBOARD_LICENSE_CHECK=1 + */ + +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { + HEADER_READ_BYTES, + contentHasAsfHeader, + isLicenseCheckSkippedForSrcRel, +} from './lib/license-header-policy.mjs' + +if ( + process.env.SKIP_DASHBOARD_HOOKS === '1' || + process.env.SKIP_DASHBOARD_LICENSE_CHECK === '1' || + process.env.HUSKY === '0' +) { + process.exit(0) +} + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const dashboardDir = join(__dirname, '..') + +const shLines = (args) => { + try { + return String( + execFileSync('git', args, { + encoding: 'utf8', + cwd: dashboardDir, + maxBuffer: 20 * 1024 * 1024, + }), + ) + .trim() + .split('\n') + .map((l) => l.trim()) + .filter(Boolean) + } catch { + return [] + } +} + +let repoRoot +try { + repoRoot = String( + execFileSync('git', ['rev-parse', '--show-toplevel'], { + encoding: 'utf8', + cwd: dashboardDir, + }), + ).trim() +} catch { + console.warn('[license-check] Not in a Git work tree; skipping.') + process.exit(0) +} + +const added = shLines(['-C', repoRoot, 'diff', '--cached', '--name-only', '--diff-filter=A']) + +const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx']) +const missing = [] + +for (const repoPath of added) { + const norm = repoPath.replace(/\\/g, '/') + if (!norm.startsWith('dashboard/src/')) continue + const ext = norm.slice(norm.lastIndexOf('.')) + if (!SOURCE_EXT.has(ext)) continue + + const srcRel = norm.slice('dashboard/src/'.length) + if (isLicenseCheckSkippedForSrcRel(srcRel)) continue + + let content + try { + content = String( + execFileSync('git', ['-C', repoRoot, 'show', `:${norm}`], { + encoding: 'utf8', + maxBuffer: HEADER_READ_BYTES + 64_000, + }), + ) + } catch { + continue + } + + const head = content.slice(0, HEADER_READ_BYTES) + if (!contentHasAsfHeader(head)) { + missing.push(norm) + } +} + +if (missing.length > 0) { + console.error( + '\x1b[31m[dashboard pre-commit]\x1b[0m New file(s) lack the Apache license header:', + ) + for (const m of missing.sort()) { + console.error(` - ${m}`) + } + console.error( + '\nAdd the standard ASF block at the top (see nearby files), or set SKIP_DASHBOARD_LICENSE_CHECK=1 only for rare exceptions.\n', + ) + process.exit(1) +} + +process.exit(0) diff --git a/dashboard/scripts/git-precommit-verify.mjs b/dashboard/scripts/git-precommit-verify.mjs new file mode 100644 index 00000000000..6cdf94386f7 --- /dev/null +++ b/dashboard/scripts/git-precommit-verify.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env node +/** + * Pre-commit: ensure UI changes stage tests; lint-staged runs ESLint after this. + * Skip: SKIP_DASHBOARD_HOOKS=1 or HUSKY=0 or SKIP_DASHBOARD_TEST_GUARD=1 + */ + +import { stagedIncludesTestWhenUiChanges } from './lib/test-path-helpers.mjs' +import { getStagedFiles } from './lib/git-changed-files.mjs' + +if (process.env.SKIP_DASHBOARD_HOOKS === '1' || process.env.HUSKY === '0') { + process.exit(0) +} + +if (process.env.SKIP_DASHBOARD_TEST_GUARD === '1') { + process.exit(0) +} + +const staged = getStagedFiles() +const dashboardPaths = staged.filter( + (p) => p.startsWith('dashboard/') || p.startsWith('src/'), +) + +if (dashboardPaths.length === 0) { + process.exit(0) +} + +const guard = stagedIncludesTestWhenUiChanges(dashboardPaths) +if (!guard.ok) { + console.error('\x1b[31m[dashboard pre-commit]\x1b[0m', guard.message) + process.exit(1) +} + +process.exit(0) diff --git a/dashboard/scripts/git-prepush-verify.mjs b/dashboard/scripts/git-prepush-verify.mjs new file mode 100644 index 00000000000..4880913c418 --- /dev/null +++ b/dashboard/scripts/git-prepush-verify.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + * Pre-push: impact-related Jest tests, ESLint (src), production build. + * Skip: SKIP_DASHBOARD_HOOKS=1 or HUSKY=0 + */ + +import { execSync, spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { join, relative } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { getPushRangeFiles } from './lib/git-changed-files.mjs' +import { + allUiChangesHaveTestHome, + isUiSourcePath, + toDashboardRelative, +} from './lib/test-path-helpers.mjs' + +if (process.env.SKIP_DASHBOARD_HOOKS === '1' || process.env.HUSKY === '0') { + process.exit(0) +} + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const dashboardRoot = join(__dirname, '..') +if (!existsSync(join(dashboardRoot, 'package.json'))) { + console.error('Could not find dashboard root', dashboardRoot) + process.exit(1) +} + +const run = (cmd, opts = {}) => { + console.log(`\x1b[36m▶\x1b[0m ${cmd}`) + execSync(cmd, { stdio: 'inherit', cwd: dashboardRoot, ...opts }) +} + +const repoPaths = getPushRangeFiles() +const dashboardPaths = repoPaths.filter( + (p) => p.startsWith('dashboard/') || p.startsWith('src/'), +) + +const dashRelFiles = dashboardPaths.map(toDashboardRelative).filter((p) => { + if (p.startsWith('..')) return false + return existsSync(join(dashboardRoot, p)) +}) + +/** Source files Jest can map to related tests */ +const jestSourceArgs = dashRelFiles.filter((p) => { + if (!p.startsWith('src/')) return false + if (p.includes('__tests__')) return false + if (/\.(test|spec)\.(tsx?)$/.test(p)) return false + return /\.(ts|tsx)$/.test(p) +}) + +if (process.env.SKIP_DASHBOARD_TEST_GUARD !== '1') { + const hasUi = dashboardPaths.map(toDashboardRelative).some(isUiSourcePath) + if (hasUi) { + const { ok, missing } = allUiChangesHaveTestHome( + dashboardRoot, + dashboardPaths, + ) + if (!ok) { + console.error( + '\x1b[31m[dashboard pre-push]\x1b[0m These UI files have no colocated __tests__ or *.test.ts(x):', + ) + for (const m of missing) { + console.error(` - ${m}`) + } + console.error( + 'Add tests or set SKIP_DASHBOARD_TEST_GUARD=1 only for exceptions.\n', + ) + process.exit(1) + } + } +} + +console.log('\x1b[35m[dashboard pre-push]\x1b[0m Changed paths in range (sample):') +console.log( + dashRelFiles.slice(0, 20).join('\n') + (dashRelFiles.length > 20 ? '\n…' : ''), +) + +if (jestSourceArgs.length > 0) { + console.log( + '\x1b[35m[dashboard pre-push]\x1b[0m Running Jest --findRelatedTests (impact surface):', + ) + const rel = jestSourceArgs.map((f) => + relative(dashboardRoot, join(dashboardRoot, f)).replace(/\\/g, '/'), + ) + const res = spawnSync( + process.platform === 'win32' ? 'npx.cmd' : 'npx', + ['jest', '--bail', '--passWithNoTests', '--findRelatedTests', ...rel], + { cwd: dashboardRoot, stdio: 'inherit', shell: process.platform === 'win32' }, + ) + if (res.status !== 0) process.exit(res.status ?? 1) +} else { + console.log( + '\x1b[33m[dashboard pre-push]\x1b[0m No TS source files in diff for --findRelatedTests; skipping Jest.', + ) +} + +run( + 'npx eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 200', +) +run('npm run build') + +console.log('\x1b[32m[dashboard pre-push]\x1b[0m All checks passed.\n') +process.exit(0) diff --git a/dashboard/scripts/install-git-hooks.mjs b/dashboard/scripts/install-git-hooks.mjs new file mode 100644 index 00000000000..6330c9bd880 --- /dev/null +++ b/dashboard/scripts/install-git-hooks.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +/** + * Point this Git repo at .githooks (repo root) so pre-commit / pre-push run for + * dashboard, dashboardv2, and docs. Runs after `npm install` in dashboard/. + * Safe no-op if not inside a Git work tree. + */ + +import { execSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const dashboardDir = join(__dirname, '..') + +let top +try { + top = execSync('git rev-parse --show-toplevel', { + encoding: 'utf8', + cwd: dashboardDir, + }).trim() +} catch { + process.exit(0) +} + +const hooksPath = '.githooks' +const absHooks = join(top, hooksPath) +if (!existsSync(absHooks)) { + console.warn('[install-git-hooks] Skipping: missing', absHooks) + process.exit(0) +} + +try { + execSync(`git config core.hooksPath "${hooksPath}"`, { + cwd: top, + stdio: 'inherit', + }) + console.log('[install-git-hooks] core.hooksPath =', hooksPath) +} catch (e) { + console.warn('[install-git-hooks] Could not set core.hooksPath (read-only?)') +} diff --git a/dashboard/scripts/lib/git-changed-files.mjs b/dashboard/scripts/lib/git-changed-files.mjs new file mode 100644 index 00000000000..0a32089de8b --- /dev/null +++ b/dashboard/scripts/lib/git-changed-files.mjs @@ -0,0 +1,71 @@ +/** + * Resolve paths changed in git (staged, committed range, or vs base branch). + * SPDX: Apache-2.0 (match dashboard) + */ + +import { execSync } from 'node:child_process' + +/** + * @param {string} cmd + * @returns {string} + */ +const sh = (cmd) => + String(execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 })).trim() + +/** + * @param {string} raw + * @returns {string[]} + */ +export const splitLines = (raw) => + raw + .split('\n') + .map((l) => l.trim()) + .filter(Boolean) + +/** + * Files staged for commit. + * @returns {string[]} + */ +export const getStagedFiles = () => { + try { + return splitLines(sh('git diff --cached --name-only --diff-filter=ACM')) + } catch { + return [] + } +} + +/** + * Files changed between remote tracking branch and HEAD (for pre-push). + * Falls back to merge-base with main/master or single last commit. + * @returns {string[]} + */ +export const getPushRangeFiles = () => { + const tryRange = (range) => { + try { + return splitLines(sh(`git diff --name-only ${range}`)) + } catch { + return null + } + } + + let files = tryRange('@{u}..HEAD') + if (files && files.length > 0) return files + + for (const base of ['origin/main', 'origin/master', 'main', 'master']) { + try { + const mergeBase = sh(`git merge-base HEAD ${base} 2>/dev/null`) + if (mergeBase) { + files = tryRange(`${mergeBase}..HEAD`) + if (files && files.length > 0) return files + } + } catch { + // continue + } + } + + try { + return splitLines(sh('git diff --name-only HEAD~1..HEAD')) + } catch { + return [] + } +} diff --git a/dashboard/scripts/lib/license-header-policy.mjs b/dashboard/scripts/lib/license-header-policy.mjs new file mode 100644 index 00000000000..b5533225957 --- /dev/null +++ b/dashboard/scripts/lib/license-header-policy.mjs @@ -0,0 +1,38 @@ +/** + * ASF license header policy aligned with src/__tests__/apache-license-header.test.ts + * (paths here are relative to dashboard/src/). + */ + +export const LICENSE_MARKERS = [ + 'Licensed to the Apache Software Foundation', + 'Apache License, Version 2.0', +] + +export const HEADER_READ_BYTES = 12_000 + +/** + * @param {string} relativePosix path relative to src/, forward slashes + * @returns {boolean} + */ +export const isLicenseCheckSkippedForSrcRel = (relativePosix) => { + const segments = relativePosix.split('/') + if (segments.includes('__tests__')) return true + if (segments.includes('__mocks__')) return true + if (/\.test\.(ts|tsx|js|jsx)$/.test(relativePosix)) return true + if (relativePosix.endsWith('.d.ts')) return true + if ( + relativePosix === 'setupTests.ts' || + relativePosix === 'setupTests.simple.ts' + ) { + return true + } + if (relativePosix === 'utils/test-utils.tsx') return true + return false +} + +/** + * @param {string} head first bytes of file as string + * @returns {boolean} + */ +export const contentHasAsfHeader = (head) => + LICENSE_MARKERS.every((marker) => head.includes(marker)) diff --git a/dashboard/scripts/lib/test-path-helpers.mjs b/dashboard/scripts/lib/test-path-helpers.mjs new file mode 100644 index 00000000000..7629b282a50 --- /dev/null +++ b/dashboard/scripts/lib/test-path-helpers.mjs @@ -0,0 +1,131 @@ +/** + * Helpers: detect UI source files and colocated / __tests__ coverage. + */ + +import { existsSync, readdirSync } from 'node:fs' +import { basename, dirname, extname, join } from 'node:path' + +/** Root-level React entry files (same risk as views/components). */ +const ROOT_UI_FILES = new Set([ + 'src/App.tsx', + 'src/Main.tsx', + 'src/ErrorBoundary.tsx', +]) + +/** Normalize to path relative to dashboard/ */ +export const toDashboardRelative = (repoPath) => { + const norm = repoPath.replace(/\\/g, '/') + if (norm.startsWith('dashboard/')) return norm.slice('dashboard/'.length) + return norm +} + +/** Production React UI paths worth guarding with tests */ +export const isUiSourcePath = (dashboardRel) => { + if (!dashboardRel.startsWith('src/')) return false + if (dashboardRel.includes('__tests__')) return false + if (dashboardRel.includes('__mocks__')) return false + if (/\.(test|spec)\.(tsx?|jsx?)$/.test(dashboardRel)) return false + if (!/\.(tsx|jsx)$/.test(dashboardRel)) return false + if (ROOT_UI_FILES.has(dashboardRel)) return true + if ( + !dashboardRel.startsWith('src/views/') && + !dashboardRel.startsWith('src/components/') + ) { + return false + } + return true +} + +/** + * @param {string} absFile absolute path to source file + */ +export const hasColocatedOrDirTests = (absFile) => { + const dir = dirname(absFile) + const ext = extname(absFile) + const base = basename(absFile, ext) + + const candidates = [ + join(dir, `${base}.test.tsx`), + join(dir, `${base}.test.ts`), + join(dir, '__tests__', `${base}.test.tsx`), + join(dir, '__tests__', `${base}.test.ts`), + ] + + for (const c of candidates) { + if (existsSync(c)) return true + } + + if (base === 'App') { + const appTest = join(dir, 'components', '__tests__', 'App.test.tsx') + if (existsSync(appTest)) return true + } + + if (base === 'EntityForm') { + const viewsDir = dirname(dir) + const viewTests = join(viewsDir, '__tests__', 'EntityForm.test.tsx') + if (existsSync(viewTests)) return true + const viewTestsTs = join(viewsDir, '__tests__', 'EntityForm.test.ts') + if (existsSync(viewTestsTs)) return true + } + + const testsDir = join(dir, '__tests__') + if (existsSync(testsDir)) { + const entries = readdirSync(testsDir) + if (entries.some((e) => /\.(test|spec)\.(tsx?|jsx?)$/.test(e))) return true + } + + return false +} + +/** + * @param {string} dashboardRoot absolute path to dashboard package + * @param {string} dashboardRel e.g. src/views/Foo/Bar.tsx + */ +export const hasTestsOnDisk = (dashboardRoot, dashboardRel) => { + const abs = join(dashboardRoot, dashboardRel) + if (!existsSync(abs)) return false + return hasColocatedOrDirTests(abs) +} + +/** + * Staged files must include at least one test when UI sources are staged. + * @param {string[]} repoPaths paths from git (dashboard/... or src/...) + * @returns {{ ok: boolean, message?: string }} + */ +export const stagedIncludesTestWhenUiChanges = (repoPaths) => { + const dashPaths = repoPaths + .map(toDashboardRelative) + .filter((p) => !p.startsWith('..')) + + const uiChanged = dashPaths.filter(isUiSourcePath) + if (uiChanged.length === 0) return { ok: true } + + const testTouched = dashPaths.some( + (p) => + p.includes('__tests__') || /\.(test|spec)\.(tsx?|jsx?)$/.test(p), + ) + + if (testTouched) return { ok: true } + + return { + ok: false, + message: + 'Staged changes touch src/views or src/components (.tsx/.jsx) but no test file was staged.\n' + + 'Add or update a matching *.test.ts(x) or __tests__/* file, or set SKIP_DASHBOARD_TEST_GUARD=1 for a rare exception.', + } +} + +/** + * For each changed UI file, require on-disk test companion. + * @param {string} dashboardRoot + * @param {string[]} repoPaths + * @returns {{ ok: boolean, missing: string[] }} + */ +export const allUiChangesHaveTestHome = (dashboardRoot, repoPaths) => { + const dashPaths = repoPaths + .map(toDashboardRelative) + .filter((p) => p.startsWith('src/')) + const ui = [...new Set(dashPaths.filter(isUiSourcePath))] + const missing = ui.filter((p) => !hasTestsOnDisk(dashboardRoot, p)) + return { ok: missing.length === 0, missing } +} diff --git a/dashboard/scripts/run-precommit-local.mjs b/dashboard/scripts/run-precommit-local.mjs new file mode 100644 index 00000000000..0aeadf95aba --- /dev/null +++ b/dashboard/scripts/run-precommit-local.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * Run the same checks as .githooks/pre-commit (for manual verification). + * Execute from dashboard/: npm run verify:precommit + * + * Order: test guard → ASF header on new files → lint-staged → tsc --noEmit + */ + +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const dashboardDir = join(__dirname, '..') + +const run = (title, fn) => { + console.log(`\x1b[36m▶\x1b[0m ${title}`) + fn() +} + +try { + run('UI ↔ staged test guard', () => { + execFileSync(process.execPath, ['scripts/git-precommit-verify.mjs'], { + cwd: dashboardDir, + stdio: 'inherit', + }) + }) + + run('ASF license on newly added staged files', () => { + execFileSync(process.execPath, ['scripts/check-staged-new-file-license.mjs'], { + cwd: dashboardDir, + stdio: 'inherit', + }) + }) + + const repoRoot = String( + execFileSync('git', ['rev-parse', '--show-toplevel'], { + encoding: 'utf8', + cwd: dashboardDir, + }), + ).trim() + + run('lint-staged (ESLint on staged dashboard/src)', () => { + const lintStagedCli = join( + dashboardDir, + 'node_modules/lint-staged/bin/lint-staged.js', + ) + execFileSync(process.execPath, [lintStagedCli, '--config', 'dashboard/lint-staged.config.mjs'], { + cwd: repoRoot, + stdio: 'inherit', + }) + }) + + if (process.env.SKIP_DASHBOARD_TYPECHECK !== '1') { + run('TypeScript project check (tsc --noEmit)', () => { + execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'typecheck'], { + cwd: dashboardDir, + stdio: 'inherit', + shell: process.platform === 'win32', + }) + }) + } + + console.log('\x1b[32m[dashboard verify:precommit]\x1b[0m All steps passed.\n') +} catch (e) { + process.exit(e.status ?? 1) +} diff --git a/scripts/git-hooks/check-added-license-generic.mjs b/scripts/git-hooks/check-added-license-generic.mjs new file mode 100644 index 00000000000..bb0ead3e482 --- /dev/null +++ b/scripts/git-hooks/check-added-license-generic.mjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node +/** + * ASF license on newly staged files for a path prefix (dashboardv2, docs). + * Reuses marker rules from dashboard/scripts/lib/license-header-policy.mjs + */ + +import { execFileSync } from 'node:child_process' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { + HEADER_READ_BYTES, + contentHasAsfHeader, +} from '../../dashboard/scripts/lib/license-header-policy.mjs' +import { getRepoRoot, getStagedAddedFiles } from './lib/git-helpers.mjs' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const scriptDir = join(__dirname, '..') // git-hooks +const repoRootDefault = getRepoRoot(scriptDir) + +/** + * @param {object} opts + * @param {string} opts.label + * @param {(repoRel: string) => boolean} opts.shouldSkip + * @param {Set} opts.extensions + * @param {string} [opts.repoRoot] + */ +export const verifyAddedFilesAspLicense = (opts) => { + const root = opts.repoRoot ?? repoRootDefault + const added = getStagedAddedFiles(root) + const missing = [] + + for (const repoPath of added) { + const norm = repoPath.replace(/\\/g, '/') + if (opts.shouldSkip(norm)) continue + const ext = norm.slice(norm.lastIndexOf('.')) + if (!opts.extensions.has(ext)) continue + + let content + try { + content = String( + execFileSync('git', ['-C', root, 'show', `:${norm}`], { + encoding: 'utf8', + maxBuffer: HEADER_READ_BYTES + 64_000, + }), + ) + } catch { + continue + } + + const head = content.slice(0, HEADER_READ_BYTES) + if (!contentHasAsfHeader(head)) missing.push(norm) + } + + if (missing.length > 0) { + console.error( + `\x1b[31m[${opts.label} pre-commit]\x1b[0m New file(s) lack the Apache license header:`, + ) + for (const m of missing.sort()) console.error(` - ${m}`) + console.error( + '\nAdd the standard ASF block at the top (match sibling files), or set SKIP_ATLAS_LICENSE_CHECK=1 (emergency only).\n', + ) + process.exit(1) + } +} diff --git a/scripts/git-hooks/lib/extra-license-skip.mjs b/scripts/git-hooks/lib/extra-license-skip.mjs new file mode 100644 index 00000000000..c5af3ab7b44 --- /dev/null +++ b/scripts/git-hooks/lib/extra-license-skip.mjs @@ -0,0 +1,46 @@ +/** + * License skip / scan rules for dashboardv2 and docs (dashboard uses dashboard/scripts). + */ + +/** @param {string} repoRel forward slashes */ +export const shouldSkipLicenseDashboardv2 = (repoRel) => { + if (!repoRel.startsWith('dashboardv2/')) return true + const r = repoRel.slice('dashboardv2/'.length) + if ( + r.startsWith('node_modules/') || + r.startsWith('bin/') || + r.includes('/node_modules/') || + r.includes('/external_lib/') + ) { + return true + } + if (r.endsWith('.min.js') || r.endsWith('.map')) return true + if (r.endsWith('package-lock.json') || r === 'package.json') return true + return false +} + +/** @param {string} repoRel */ +export const shouldSkipLicenseDocs = (repoRel) => { + if (!repoRel.startsWith('docs/')) return true + const r = repoRel.slice('docs/'.length) + if ( + r.startsWith('node_modules/') || + r.startsWith('site/') || + r.startsWith('bin/') || + r.startsWith('docz-lib/') || + r.includes('/node_modules/') + ) { + return true + } + if ( + r.endsWith('package-lock.json') || + r === 'package.json' || + r.endsWith('.png') || + r.endsWith('.svg') || + r.endsWith('.woff') || + r.endsWith('.ico') + ) { + return true + } + return false +} diff --git a/scripts/git-hooks/lib/git-helpers.mjs b/scripts/git-hooks/lib/git-helpers.mjs new file mode 100644 index 00000000000..4b3730f2977 --- /dev/null +++ b/scripts/git-hooks/lib/git-helpers.mjs @@ -0,0 +1,104 @@ +/** + * Git helpers for repo-root hook orchestration. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execFileSync } from 'node:child_process' + +/** + * @param {string} cwd + * @returns {string} + */ +export const getRepoRoot = (cwd) => + String( + execFileSync('git', ['rev-parse', '--show-toplevel'], { + encoding: 'utf8', + cwd, + }), + ).trim() + +/** + * @param {string} root + * @param {...string} gitArgs + * @returns {string} + */ +export const git = (root, ...gitArgs) => + String( + execFileSync('git', ['-C', root, ...gitArgs], { + encoding: 'utf8', + maxBuffer: 20 * 1024 * 1024, + }), + ).trim() + +/** + * @param {string} raw + * @returns {string[]} + */ +export const splitLines = (raw) => + raw + .split('\n') + .map((l) => l.trim()) + .filter(Boolean) + +/** + * @param {string} root + * @returns {string[]} + */ +export const getStagedFiles = (root) => { + try { + return splitLines(git(root, 'diff', '--cached', '--name-only', '--diff-filter=ACM')) + } catch { + return [] + } +} + +/** + * @param {string} root + * @returns {string[]} + */ +export const getStagedAddedFiles = (root) => { + try { + return splitLines(git(root, 'diff', '--cached', '--name-only', '--diff-filter=A')) + } catch { + return [] + } +} + +/** + * @param {string} root + * @returns {string[]} + */ +export const getPushRangeFiles = (root) => { + const tryRange = (range) => { + try { + return splitLines(git(root, 'diff', '--name-only', range)) + } catch { + return null + } + } + + let files = tryRange('@{u}..HEAD') + if (files && files.length > 0) return files + + for (const base of ['origin/master', 'origin/main', 'master', 'main']) { + try { + const mergeBase = String( + execFileSync('git', ['-C', root, 'merge-base', 'HEAD', base], { + encoding: 'utf8', + }), + ).trim() + if (mergeBase) { + files = tryRange(`${mergeBase}..HEAD`) + if (files && files.length > 0) return files + } + } catch { + // continue + } + } + + try { + return splitLines(git(root, 'diff', '--name-only', 'HEAD~1..HEAD')) + } catch { + return [] + } +} diff --git a/scripts/git-hooks/run-precommit.mjs b/scripts/git-hooks/run-precommit.mjs new file mode 100644 index 00000000000..b6835b1dc01 --- /dev/null +++ b/scripts/git-hooks/run-precommit.mjs @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/** + * Repo-wide pre-commit: dashboard (full), dashboardv2 + docs (license, syntax). + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execFileSync } from 'node:child_process' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { verifyAddedFilesAspLicense } from './check-added-license-generic.mjs' +import { + shouldSkipLicenseDashboardv2, + shouldSkipLicenseDocs, +} from './lib/extra-license-skip.mjs' +import { getRepoRoot, getStagedFiles } from './lib/git-helpers.mjs' +import { + syntaxCheckDashboardv2Staged, + syntaxCheckDocsStaged, +} from './syntax-check-staged.mjs' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const scriptsDir = join(__dirname, '..') +const repoRoot = getRepoRoot(scriptsDir) + +if ( + process.env.SKIP_ATLAS_HOOKS === '1' || + process.env.SKIP_ALL_ATLAS_GIT_HOOKS === '1' +) { + process.exit(0) +} + +const staged = getStagedFiles(repoRoot) +const touchDashboard = staged.some((p) => p.startsWith('dashboard/')) +const touchV2 = staged.some((p) => p.startsWith('dashboardv2/')) +const touchDocs = staged.some((p) => p.startsWith('docs/')) + +const runDash = (title, file) => { + console.log(`\x1b[36m▶\x1b[0m [dashboard] ${title}`) + execFileSync(process.execPath, [join(repoRoot, 'dashboard', 'scripts', file)], { + cwd: join(repoRoot, 'dashboard'), + stdio: 'inherit', + }) +} + +if (touchDashboard && process.env.SKIP_DASHBOARD_HOOKS !== '1') { + runDash('UI ↔ staged test guard', 'git-precommit-verify.mjs') + runDash('ASF license (new staged files under src/)', 'check-staged-new-file-license.mjs') + + const lintStagedCli = join( + repoRoot, + 'dashboard/node_modules/lint-staged/bin/lint-staged.js', + ) + try { + execFileSync( + process.execPath, + [lintStagedCli, '--config', 'dashboard/lint-staged.config.mjs'], + { + cwd: repoRoot, + stdio: 'inherit', + }, + ) + } catch { + process.exit(1) + } + + if (process.env.SKIP_DASHBOARD_TYPECHECK !== '1') { + execFileSync( + process.platform === 'win32' ? 'npm.cmd' : 'npm', + ['run', 'typecheck'], + { + cwd: join(repoRoot, 'dashboard'), + stdio: 'inherit', + shell: process.platform === 'win32', + }, + ) + } +} + +if (touchV2 && process.env.SKIP_DASHBOARDV2_HOOKS !== '1') { + if (process.env.SKIP_ATLAS_LICENSE_CHECK !== '1') { + verifyAddedFilesAspLicense({ + label: 'dashboardv2', + shouldSkip: (p) => shouldSkipLicenseDashboardv2(p), + extensions: new Set(['.js', '.jsx', '.ts', '.tsx']), + repoRoot, + }) + } + syntaxCheckDashboardv2Staged(repoRoot) +} + +if (touchDocs && process.env.SKIP_DOCS_HOOKS !== '1') { + if (process.env.SKIP_ATLAS_LICENSE_CHECK !== '1') { + verifyAddedFilesAspLicense({ + label: 'docs', + shouldSkip: (p) => shouldSkipLicenseDocs(p), + extensions: new Set(['.js', '.jsx', '.ts', '.tsx']), + repoRoot, + }) + } + syntaxCheckDocsStaged(repoRoot) +} + +process.exit(0) diff --git a/scripts/git-hooks/run-prepush.mjs b/scripts/git-hooks/run-prepush.mjs new file mode 100644 index 00000000000..6be42d8500f --- /dev/null +++ b/scripts/git-hooks/run-prepush.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +/** + * Repo-wide pre-push: dashboard (tests + eslint + build), dashboardv2 build, docs build. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execFileSync } from 'node:child_process' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { getRepoRoot, getPushRangeFiles } from './lib/git-helpers.mjs' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const scriptsDir = join(__dirname, '..') +const repoRoot = getRepoRoot(scriptsDir) + +if ( + process.env.SKIP_ATLAS_HOOKS === '1' || + process.env.SKIP_ALL_ATLAS_GIT_HOOKS === '1' +) { + process.exit(0) +} + +const changed = getPushRangeFiles(repoRoot) +const touchDashboard = changed.some((p) => p.startsWith('dashboard/')) +const touchV2 = changed.some((p) => p.startsWith('dashboardv2/')) +const touchDocs = changed.some((p) => p.startsWith('docs/')) + +if (!touchDashboard && !touchV2 && !touchDocs) { + process.exit(0) +} + +if (touchDashboard && process.env.SKIP_DASHBOARD_HOOKS !== '1') { + console.log('\x1b[35m[atlas pre-push]\x1b[0m dashboard package…') + execFileSync(process.execPath, ['scripts/git-prepush-verify.mjs'], { + cwd: join(repoRoot, 'dashboard'), + stdio: 'inherit', + }) +} + +if (touchV2 && process.env.SKIP_DASHBOARDV2_HOOKS !== '1') { + if (process.env.SKIP_DASHBOARDV2_BUILD === '1') { + console.log( + '\x1b[33m[atlas pre-push]\x1b[0m SKIP_DASHBOARDV2_BUILD=1 — skipping dashboardv2 npm run build.', + ) + } else { + console.log('\x1b[35m[atlas pre-push]\x1b[0m dashboardv2 — npm run build…') + execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { + cwd: join(repoRoot, 'dashboardv2'), + stdio: 'inherit', + shell: process.platform === 'win32', + }) + } +} + +if (touchDocs && process.env.SKIP_DOCS_HOOKS !== '1') { + if (process.env.SKIP_DOCS_BUILD === '1') { + console.log( + '\x1b[33m[atlas pre-push]\x1b[0m SKIP_DOCS_BUILD=1 — skipping docs npm run build.', + ) + } else { + console.log('\x1b[35m[atlas pre-push]\x1b[0m docs — npm run build…') + execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { + cwd: join(repoRoot, 'docs'), + stdio: 'inherit', + shell: process.platform === 'win32', + }) + } +} + +console.log('\x1b[32m[atlas pre-push]\x1b[0m Done.\n') +process.exit(0) diff --git a/scripts/git-hooks/syntax-check-staged.mjs b/scripts/git-hooks/syntax-check-staged.mjs new file mode 100644 index 00000000000..7dce2837eec --- /dev/null +++ b/scripts/git-hooks/syntax-check-staged.mjs @@ -0,0 +1,71 @@ +#!/usr/bin/env node +/** + * node --check on staged plain JS (not .jsx) under dashboardv2/public/js. + */ + +import { execFileSync, spawnSync } from 'node:child_process' +import { join } from 'node:path' + +import { getStagedFiles } from './lib/git-helpers.mjs' + +/** + * @param {string} repoRel + */ +const isV2CheckableJs = (repoRel) => { + const n = repoRel.replace(/\\/g, '/') + if (!n.startsWith('dashboardv2/public/js/')) return false + if (!n.endsWith('.js')) return false + if (n.includes('/external_lib/')) return false + if (n.endsWith('.min.js')) return false + return true +} + +/** + * @param {string} root + */ +export const syntaxCheckDashboardv2Staged = (root) => { + const staged = getStagedFiles(root) + const files = staged.filter(isV2CheckableJs) + for (const f of files) { + const abs = join(root, f) + const r = spawnSync(process.execPath, ['--check', abs], { + encoding: 'utf8', + }) + if (r.status !== 0) { + console.error( + `\x1b[31m[dashboardv2 pre-commit]\x1b[0m Syntax error in ${f}:\n${r.stderr || r.stdout}`, + ) + process.exit(r.status ?? 1) + } + } +} + +/** Docs: only plain scripts (Node parses scripts/*.js, doczrc.js, webapp config). */ +const isDocsCheckableJs = (repoRel) => { + const n = repoRel.replace(/\\/g, '/') + if (!n.startsWith('docs/')) return false + if (!n.endsWith('.js')) return false + if (n.includes('/node_modules/')) return false + if (n.startsWith('docs/site/') || n.startsWith('docs/bin/')) return false + if (n.startsWith('docs/docz-lib/')) return false + // Avoid JSX-heavy paths (node cannot parse) + if (n.startsWith('docs/theme/') || n.startsWith('docs/webapp/')) return false + return true +} + +/** + * @param {string} root + */ +export const syntaxCheckDocsStaged = (root) => { + const staged = getStagedFiles(root) + const files = staged.filter(isDocsCheckableJs) + for (const f of files) { + const abs = join(root, f) + try { + execFileSync(process.execPath, ['--check', abs], { stdio: 'pipe' }) + } catch (e) { + console.error(`\x1b[31m[docs pre-commit]\x1b[0m Syntax error in ${f}`) + process.exit(1) + } + } +} From 8440fd80be2d4d06a6beab59a82477f8a088be54 Mon Sep 17 00:00:00 2001 From: Prasad Pawar Date: Thu, 14 May 2026 15:33:06 +0530 Subject: [PATCH 2/6] ATLAS-5293: ATLAS UI: Add repo-wide Git hooks and dashboard pre-commit/push verification --- .githooks/pre-commit | 17 ++++ .githooks/pre-push | 17 ++++ dashboard/.githooks/pre-commit | 17 ++++ dashboard/.githooks/pre-push | 17 ++++ dashboard/.husky/pre-commit | 17 ++++ dashboard/docs/GIT_HOOKS.md | 6 +- dashboard/lint-staged.config.mjs | 17 ++++ .../scripts/check-push-new-file-license.mjs | 80 +++++++++++++++++ .../scripts/check-staged-new-file-license.mjs | 65 ++++++-------- dashboard/scripts/git-precommit-verify.mjs | 17 ++++ dashboard/scripts/git-prepush-verify.mjs | 29 +++++- dashboard/scripts/install-git-hooks.mjs | 17 ++++ dashboard/scripts/lib/git-changed-files.mjs | 88 ++++++++++++++++++- .../scripts/lib/license-header-policy.mjs | 51 ++++++++++- dashboard/scripts/lib/test-path-helpers.mjs | 17 ++++ .../lib/verify-dashboard-src-license.mjs | 77 ++++++++++++++++ dashboard/scripts/run-precommit-local.mjs | 17 ++++ .../git-hooks/check-added-license-generic.mjs | 17 ++++ scripts/git-hooks/lib/extra-license-skip.mjs | 17 ++++ scripts/git-hooks/lib/git-helpers.mjs | 18 +++- scripts/git-hooks/run-precommit.mjs | 18 +++- scripts/git-hooks/run-prepush.mjs | 18 +++- scripts/git-hooks/syntax-check-staged.mjs | 17 ++++ 23 files changed, 622 insertions(+), 49 deletions(-) create mode 100644 dashboard/scripts/check-push-new-file-license.mjs create mode 100644 dashboard/scripts/lib/verify-dashboard-src-license.mjs diff --git a/.githooks/pre-commit b/.githooks/pre-commit index b0db143666e..63f35f074db 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,4 +1,21 @@ #!/usr/bin/env sh +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# # Atlas monorepo: pre-commit (license, lint, typecheck, syntax; dashboard test guard) # Skip: SKIP_ATLAS_HOOKS=1 | SKIP_ALL_ATLAS_GIT_HOOKS=1 set -e diff --git a/.githooks/pre-push b/.githooks/pre-push index 0048cf4e69c..aef7dc7545f 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,4 +1,21 @@ #!/usr/bin/env sh +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# # Atlas monorepo: pre-push (dashboard Jest/eslint/build; dashboardv2 + docs build) # Skip: SKIP_ATLAS_HOOKS=1 | SKIP_ALL_ATLAS_GIT_HOOKS=1 set -e diff --git a/dashboard/.githooks/pre-commit b/dashboard/.githooks/pre-commit index c715084e4b2..1edfb24ea4f 100755 --- a/dashboard/.githooks/pre-commit +++ b/dashboard/.githooks/pre-commit @@ -1,4 +1,21 @@ #!/usr/bin/env sh +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# # Deprecated path: use repo root core.hooksPath=.githooks (see dashboard/scripts/install-git-hooks.mjs). ROOT="$(git rev-parse --show-toplevel)" exec "$ROOT/.githooks/pre-commit" diff --git a/dashboard/.githooks/pre-push b/dashboard/.githooks/pre-push index 536610d2ca2..899c8031812 100755 --- a/dashboard/.githooks/pre-push +++ b/dashboard/.githooks/pre-push @@ -1,3 +1,20 @@ #!/usr/bin/env sh +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ROOT="$(git rev-parse --show-toplevel)" exec "$ROOT/.githooks/pre-push" diff --git a/dashboard/.husky/pre-commit b/dashboard/.husky/pre-commit index 95fe05b179f..2be87c1c37b 100644 --- a/dashboard/.husky/pre-commit +++ b/dashboard/.husky/pre-commit @@ -1,4 +1,21 @@ #!/usr/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# # Git hooks: core.hooksPath=.githooks at Atlas repo root (see dashboard npm prepare). printf '%s\n' '[atlas] Hooks: git config core.hooksPath .githooks OR npm install (in dashboard/)' exit 0 diff --git a/dashboard/docs/GIT_HOOKS.md b/dashboard/docs/GIT_HOOKS.md index b5e204e6dfe..50b5cb1dd43 100644 --- a/dashboard/docs/GIT_HOOKS.md +++ b/dashboard/docs/GIT_HOOKS.md @@ -33,7 +33,7 @@ Runs **only for packages that have staged paths** under that prefix. | Area | When staged under … | Checks | |------|---------------------|--------| -| **dashboard** | `dashboard/` | (1) **UI test guard** — `src/views`, `src/components`, `App.tsx` / `Main.tsx` / `ErrorBoundary.tsx` must include a **staged** test file; (2) **ASF license** on **new** files under `dashboard/src/`; (3) **lint-staged** → ESLint on staged TS/TSX; (4) **`npm run typecheck`** (`tsc --noEmit`). | +| **dashboard** | `dashboard/` | (1) **UI test guard** — `src/views`, `src/components`, `App.tsx` / `Main.tsx` / `ErrorBoundary.tsx` must include a **staged** test file; (2) **RAT-aligned ASF license** on **new** files under `dashboard/src/` (`license-header-policy.mjs` markers, same bar as CI RAT); (3) **lint-staged** → ESLint on staged TS/TSX; (4) **`npm run typecheck`** (`tsc --noEmit`). | | **dashboardv2** | `dashboardv2/` | (1) **ASF license** on **new** `.js`/`.jsx`/`.ts`/`.tsx` (skips `node_modules`, `bin/`, `external_lib`, `.min.js`); (2) **`node --check`** on staged plain `.js` under `dashboardv2/public/js/` (syntax). **No** Jest/test guard (legacy Grunt UI). | | **docs** | `docs/` | (1) **ASF license** on **new** sources (skips `node_modules`, `site/`, `bin/`, `docz-lib/`); (2) **`node --check`** on staged **plain** `docs/**/*.js` outside theme/webapp JSX trees. | @@ -43,7 +43,7 @@ Runs for each package **if commits in the push range** touch that prefix. | Area | Checks | |------|--------| -| **dashboard** | Colocated tests on disk, **`jest --findRelatedTests`**, **`eslint src`**, **`npm run build`**. | +| **dashboard** | **RAT-aligned ASF header** on **new** `dashboard/src/` files in the push range, colocated tests on disk, **`jest --findRelatedTests`**, **`eslint src`**, **`npm run build`**. | | **dashboardv2** | **`npm run build`** (Grunt). | | **docs** | **`npm run build`** (Docz). | @@ -68,7 +68,7 @@ SKIP_DOCS_HOOKS=1 git commit ... ```bash SKIP_DASHBOARD_TEST_GUARD=1 git commit ... # staged test file rule -SKIP_DASHBOARD_LICENSE_CHECK=1 git commit ... # ASF on new files under dashboard/src +SKIP_DASHBOARD_LICENSE_CHECK=1 git commit ... # RAT-aligned ASF on new dashboard/src (also used by pre-push added-file check) SKIP_DASHBOARD_TYPECHECK=1 git commit ... # tsc on commit ``` diff --git a/dashboard/lint-staged.config.mjs b/dashboard/lint-staged.config.mjs index 70527d77049..2098f952968 100644 --- a/dashboard/lint-staged.config.mjs +++ b/dashboard/lint-staged.config.mjs @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * lint-staged config: Git reports paths as dashboard/src/... from repo root; * ESLint runs with cwd = dashboard, so strip the dashboard/ prefix. diff --git a/dashboard/scripts/check-push-new-file-license.mjs b/dashboard/scripts/check-push-new-file-license.mjs new file mode 100644 index 00000000000..96cf4455870 --- /dev/null +++ b/dashboard/scripts/check-push-new-file-license.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Pre-push: any *new* dashboard/src source files in the push range must carry + * a RAT-aligned Apache license header at HEAD (same policy as pre-commit). + * + * Skip: SKIP_DASHBOARD_HOOKS=1 | SKIP_DASHBOARD_LICENSE_CHECK=1 + */ + +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { getPushAddedRepoPaths } from './lib/git-changed-files.mjs' +import { + gitShowUtf8, + listDashboardSrcAddedMissingLicense, +} from './lib/verify-dashboard-src-license.mjs' + +if ( + process.env.SKIP_DASHBOARD_HOOKS === '1' || + process.env.SKIP_DASHBOARD_LICENSE_CHECK === '1' || + process.env.HUSKY === '0' +) { + process.exit(0) +} + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const dashboardDir = join(__dirname, '..') + +let repoRoot +try { + repoRoot = String( + execFileSync('git', ['rev-parse', '--show-toplevel'], { + encoding: 'utf8', + cwd: dashboardDir, + }), + ).trim() +} catch { + console.warn('[license-check push] Not in a Git work tree; skipping.') + process.exit(0) +} + +const added = getPushAddedRepoPaths(repoRoot) +const missing = listDashboardSrcAddedMissingLicense( + added, + (norm) => gitShowUtf8(repoRoot, `HEAD:${norm}`), +) + +if (missing.length > 0) { + console.error( + '\x1b[31m[dashboard pre-push]\x1b[0m Added file(s) lack a RAT-aligned Apache license header:', + ) + for (const m of missing.sort()) { + console.error(` - ${m}`) + } + console.error( + '\nInclude the full standard ASF block at the top (see license-header-policy.mjs markers).', + 'Or set SKIP_DASHBOARD_LICENSE_CHECK=1 only for rare exceptions.\n', + ) + process.exit(1) +} + +process.exit(0) diff --git a/dashboard/scripts/check-staged-new-file-license.mjs b/dashboard/scripts/check-staged-new-file-license.mjs index b3dff7048b2..a43d9e454cf 100644 --- a/dashboard/scripts/check-staged-new-file-license.mjs +++ b/dashboard/scripts/check-staged-new-file-license.mjs @@ -1,7 +1,24 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Pre-commit: newly added (staged) source files under dashboard/src must carry - * the standard ASF header (same policy as apache-license-header.test.ts). + * a RAT-aligned Apache license header (see license-header-policy.mjs). * * Skip: SKIP_DASHBOARD_HOOKS=1 | SKIP_DASHBOARD_LICENSE_CHECK=1 */ @@ -11,10 +28,9 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { - HEADER_READ_BYTES, - contentHasAsfHeader, - isLicenseCheckSkippedForSrcRel, -} from './lib/license-header-policy.mjs' + gitShowUtf8, + listDashboardSrcAddedMissingLicense, +} from './lib/verify-dashboard-src-license.mjs' if ( process.env.SKIP_DASHBOARD_HOOKS === '1' || @@ -60,45 +76,22 @@ try { const added = shLines(['-C', repoRoot, 'diff', '--cached', '--name-only', '--diff-filter=A']) -const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx']) -const missing = [] - -for (const repoPath of added) { - const norm = repoPath.replace(/\\/g, '/') - if (!norm.startsWith('dashboard/src/')) continue - const ext = norm.slice(norm.lastIndexOf('.')) - if (!SOURCE_EXT.has(ext)) continue - - const srcRel = norm.slice('dashboard/src/'.length) - if (isLicenseCheckSkippedForSrcRel(srcRel)) continue - - let content - try { - content = String( - execFileSync('git', ['-C', repoRoot, 'show', `:${norm}`], { - encoding: 'utf8', - maxBuffer: HEADER_READ_BYTES + 64_000, - }), - ) - } catch { - continue - } - - const head = content.slice(0, HEADER_READ_BYTES) - if (!contentHasAsfHeader(head)) { - missing.push(norm) - } -} +const missing = listDashboardSrcAddedMissingLicense( + added, + (norm) => gitShowUtf8(repoRoot, `:${norm}`), +) if (missing.length > 0) { console.error( - '\x1b[31m[dashboard pre-commit]\x1b[0m New file(s) lack the Apache license header:', + '\x1b[31m[dashboard pre-commit]\x1b[0m New file(s) lack a RAT-aligned Apache license header:', ) for (const m of missing.sort()) { console.error(` - ${m}`) } console.error( - '\nAdd the standard ASF block at the top (see nearby files), or set SKIP_DASHBOARD_LICENSE_CHECK=1 only for rare exceptions.\n', + '\nInclude the full standard ASF block at the top (see CreateDropdown.tsx or audit sibling files).', + 'Required markers match dashboard/scripts/lib/license-header-policy.mjs.', + 'Or set SKIP_DASHBOARD_LICENSE_CHECK=1 only for rare exceptions.\n', ) process.exit(1) } diff --git a/dashboard/scripts/git-precommit-verify.mjs b/dashboard/scripts/git-precommit-verify.mjs index 6cdf94386f7..83db31412d5 100644 --- a/dashboard/scripts/git-precommit-verify.mjs +++ b/dashboard/scripts/git-precommit-verify.mjs @@ -1,4 +1,21 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Pre-commit: ensure UI changes stage tests; lint-staged runs ESLint after this. * Skip: SKIP_DASHBOARD_HOOKS=1 or HUSKY=0 or SKIP_DASHBOARD_TEST_GUARD=1 diff --git a/dashboard/scripts/git-prepush-verify.mjs b/dashboard/scripts/git-prepush-verify.mjs index 4880913c418..fdfd511d28c 100644 --- a/dashboard/scripts/git-prepush-verify.mjs +++ b/dashboard/scripts/git-prepush-verify.mjs @@ -1,10 +1,27 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Pre-push: impact-related Jest tests, ESLint (src), production build. * Skip: SKIP_DASHBOARD_HOOKS=1 or HUSKY=0 */ -import { execSync, spawnSync } from 'node:child_process' +import { execFileSync, execSync, spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { join, relative } from 'node:path' import { fileURLToPath } from 'node:url' @@ -27,6 +44,16 @@ if (!existsSync(join(dashboardRoot, 'package.json'))) { process.exit(1) } +if (process.env.SKIP_DASHBOARD_LICENSE_CHECK !== '1') { + console.log( + '\x1b[35m[dashboard pre-push]\x1b[0m RAT-aligned ASF header on newly added dashboard/src files…', + ) + execFileSync(process.execPath, ['scripts/check-push-new-file-license.mjs'], { + cwd: dashboardRoot, + stdio: 'inherit', + }) +} + const run = (cmd, opts = {}) => { console.log(`\x1b[36m▶\x1b[0m ${cmd}`) execSync(cmd, { stdio: 'inherit', cwd: dashboardRoot, ...opts }) diff --git a/dashboard/scripts/install-git-hooks.mjs b/dashboard/scripts/install-git-hooks.mjs index 6330c9bd880..fef98aaae7b 100644 --- a/dashboard/scripts/install-git-hooks.mjs +++ b/dashboard/scripts/install-git-hooks.mjs @@ -1,4 +1,21 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Point this Git repo at .githooks (repo root) so pre-commit / pre-push run for * dashboard, dashboardv2, and docs. Runs after `npm install` in dashboard/. diff --git a/dashboard/scripts/lib/git-changed-files.mjs b/dashboard/scripts/lib/git-changed-files.mjs index 0a32089de8b..daaa8572dd0 100644 --- a/dashboard/scripts/lib/git-changed-files.mjs +++ b/dashboard/scripts/lib/git-changed-files.mjs @@ -1,9 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Resolve paths changed in git (staged, committed range, or vs base branch). - * SPDX: Apache-2.0 (match dashboard) */ -import { execSync } from 'node:child_process' +import { execFileSync, execSync } from 'node:child_process' /** * @param {string} cmd @@ -51,7 +67,7 @@ export const getPushRangeFiles = () => { let files = tryRange('@{u}..HEAD') if (files && files.length > 0) return files - for (const base of ['origin/main', 'origin/master', 'main', 'master']) { + for (const base of ['origin/master', 'origin/main', 'main', 'master']) { try { const mergeBase = sh(`git merge-base HEAD ${base} 2>/dev/null`) if (mergeBase) { @@ -69,3 +85,69 @@ export const getPushRangeFiles = () => { return [] } } + +/** + * Revision range for `git diff` comparing this branch to its upstream / mainline. + * @param {string} repoRoot absolute repo root + * @returns {string} revRange e.g. `@{u}..HEAD`, `abc..HEAD`, `HEAD~1..HEAD` + */ +export const resolvePushRevRange = (repoRoot) => { + const execGit = (args) => + String( + execFileSync('git', ['-C', repoRoot, ...args], { + encoding: 'utf8', + maxBuffer: 10 * 1024 * 1024, + }), + ).trim() + + const tryRange = (range) => { + try { + execGit(['diff', '--name-only', range]) + return range + } catch { + return null + } + } + + let range = tryRange('@{u}..HEAD') + if (range) return range + + for (const base of ['origin/master', 'origin/main', 'main', 'master']) { + try { + const mergeBase = execGit(['merge-base', 'HEAD', base]) + if (mergeBase) { + const r = `${mergeBase}..HEAD` + if (tryRange(r)) return r + } + } catch { + // continue + } + } + + return 'HEAD~1..HEAD' +} + +/** + * Repo-relative paths added (not present at merge-base / range start) in push range. + * @param {string} repoRoot + * @returns {string[]} + */ +export const getPushAddedRepoPaths = (repoRoot) => { + const range = resolvePushRevRange(repoRoot) + try { + return splitLines( + String( + execFileSync( + 'git', + ['-C', repoRoot, 'diff', '--name-only', '--diff-filter=A', range], + { + encoding: 'utf8', + maxBuffer: 20 * 1024 * 1024, + }, + ), + ).trim(), + ) + } catch { + return [] + } +} diff --git a/dashboard/scripts/lib/license-header-policy.mjs b/dashboard/scripts/lib/license-header-policy.mjs index b5533225957..29826a075f6 100644 --- a/dashboard/scripts/lib/license-header-policy.mjs +++ b/dashboard/scripts/lib/license-header-policy.mjs @@ -1,13 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * ASF license header policy aligned with src/__tests__/apache-license-header.test.ts * (paths here are relative to dashboard/src/). */ -export const LICENSE_MARKERS = [ +/** + * Substrings that must all appear in the first HEADER_READ_BYTES bytes of the file. + * Aligns with the standard Atlas dashboard header and Apache RAT expectations + * (ASL2-style notice), so CI RAT and local hooks enforce the same bar. + */ +export const RAT_ALIGNED_REQUIRED_MARKERS = [ 'Licensed to the Apache Software Foundation', 'Apache License, Version 2.0', + 'http://www.apache.org/licenses/LICENSE-2.0', + 'Unless required by applicable law or agreed to in writing', + 'WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND', + 'limitations under the License', ] +/** @deprecated use {@link RAT_ALIGNED_REQUIRED_MARKERS} — kept for callers that only need the lead-in */ +export const LICENSE_MARKERS = RAT_ALIGNED_REQUIRED_MARKERS.slice(0, 2) + export const HEADER_READ_BYTES = 12_000 /** @@ -30,9 +59,25 @@ export const isLicenseCheckSkippedForSrcRel = (relativePosix) => { return false } +/** + * Collapse whitespace so wrapped ASF headers still match marker substrings. + * @param {string} head + * @returns {string} + */ +const normalizeHeaderWhitespace = (head) => head.replace(/\s+/gu, ' ').trim() + /** * @param {string} head first bytes of file as string * @returns {boolean} */ -export const contentHasAsfHeader = (head) => - LICENSE_MARKERS.every((marker) => head.includes(marker)) +export const contentHasAsfHeader = (head) => { + const n = normalizeHeaderWhitespace(head.slice(0, HEADER_READ_BYTES)) + return RAT_ALIGNED_REQUIRED_MARKERS.every((marker) => n.includes(marker)) +} + +/** + * Alias for {@link contentHasAsfHeader} (explicit RAT-oriented name for tools/tests). + * @param {string} head + * @returns {boolean} + */ +export const contentHasRatApprovedAsfHeader = (head) => contentHasAsfHeader(head) diff --git a/dashboard/scripts/lib/test-path-helpers.mjs b/dashboard/scripts/lib/test-path-helpers.mjs index 7629b282a50..986f6b527f3 100644 --- a/dashboard/scripts/lib/test-path-helpers.mjs +++ b/dashboard/scripts/lib/test-path-helpers.mjs @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Helpers: detect UI source files and colocated / __tests__ coverage. */ diff --git a/dashboard/scripts/lib/verify-dashboard-src-license.mjs b/dashboard/scripts/lib/verify-dashboard-src-license.mjs new file mode 100644 index 00000000000..b36624a52ac --- /dev/null +++ b/dashboard/scripts/lib/verify-dashboard-src-license.mjs @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Shared ASF header verification for dashboard/src (pre-commit + pre-push). + */ + +import { execFileSync } from 'node:child_process' + +import { + HEADER_READ_BYTES, + contentHasAsfHeader, + isLicenseCheckSkippedForSrcRel, +} from './license-header-policy.mjs' + +const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx']) + +/** + * @param {string[]} addedRepoPaths paths from git (e.g. dashboard/src/Foo.tsx) + * @param {(repoNormPath: string) => string} readBlob git show spec resolved to UTF-8 + * @returns {string[]} repo-relative paths missing a RAT-aligned header + */ +export const listDashboardSrcAddedMissingLicense = (addedRepoPaths, readBlob) => { + const missing = [] + + for (const repoPath of addedRepoPaths) { + const norm = repoPath.replace(/\\/g, '/') + if (!norm.startsWith('dashboard/src/')) continue + + const ext = norm.slice(norm.lastIndexOf('.')) + if (!SOURCE_EXT.has(ext)) continue + + const srcRel = norm.slice('dashboard/src/'.length) + if (isLicenseCheckSkippedForSrcRel(srcRel)) continue + + let content + try { + content = readBlob(norm) + } catch { + continue + } + + const head = content.slice(0, HEADER_READ_BYTES) + if (!contentHasAsfHeader(head)) { + missing.push(norm) + } + } + + return missing +} + +/** + * @param {string} repoRoot + * @param {string} gitShowArg e.g. `:${path}` (index) or `HEAD:${path}` + * @returns {string} + */ +export const gitShowUtf8 = (repoRoot, gitShowArg) => + String( + execFileSync('git', ['-C', repoRoot, 'show', gitShowArg], { + encoding: 'utf8', + maxBuffer: HEADER_READ_BYTES + 64_000, + }), + ) diff --git a/dashboard/scripts/run-precommit-local.mjs b/dashboard/scripts/run-precommit-local.mjs index 0aeadf95aba..64a531032a1 100644 --- a/dashboard/scripts/run-precommit-local.mjs +++ b/dashboard/scripts/run-precommit-local.mjs @@ -1,4 +1,21 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Run the same checks as .githooks/pre-commit (for manual verification). * Execute from dashboard/: npm run verify:precommit diff --git a/scripts/git-hooks/check-added-license-generic.mjs b/scripts/git-hooks/check-added-license-generic.mjs index bb0ead3e482..3908d50895e 100644 --- a/scripts/git-hooks/check-added-license-generic.mjs +++ b/scripts/git-hooks/check-added-license-generic.mjs @@ -1,4 +1,21 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * ASF license on newly staged files for a path prefix (dashboardv2, docs). * Reuses marker rules from dashboard/scripts/lib/license-header-policy.mjs diff --git a/scripts/git-hooks/lib/extra-license-skip.mjs b/scripts/git-hooks/lib/extra-license-skip.mjs index c5af3ab7b44..c63f7286611 100644 --- a/scripts/git-hooks/lib/extra-license-skip.mjs +++ b/scripts/git-hooks/lib/extra-license-skip.mjs @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * License skip / scan rules for dashboardv2 and docs (dashboard uses dashboard/scripts). */ diff --git a/scripts/git-hooks/lib/git-helpers.mjs b/scripts/git-hooks/lib/git-helpers.mjs index 4b3730f2977..0167159e80f 100644 --- a/scripts/git-hooks/lib/git-helpers.mjs +++ b/scripts/git-hooks/lib/git-helpers.mjs @@ -1,6 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Git helpers for repo-root hook orchestration. - * SPDX-License-Identifier: Apache-2.0 */ import { execFileSync } from 'node:child_process' diff --git a/scripts/git-hooks/run-precommit.mjs b/scripts/git-hooks/run-precommit.mjs index b6835b1dc01..c2937c5787e 100644 --- a/scripts/git-hooks/run-precommit.mjs +++ b/scripts/git-hooks/run-precommit.mjs @@ -1,7 +1,23 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Repo-wide pre-commit: dashboard (full), dashboardv2 + docs (license, syntax). - * SPDX-License-Identifier: Apache-2.0 */ import { execFileSync } from 'node:child_process' diff --git a/scripts/git-hooks/run-prepush.mjs b/scripts/git-hooks/run-prepush.mjs index 6be42d8500f..88bbb60a9c8 100644 --- a/scripts/git-hooks/run-prepush.mjs +++ b/scripts/git-hooks/run-prepush.mjs @@ -1,7 +1,23 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Repo-wide pre-push: dashboard (tests + eslint + build), dashboardv2 build, docs build. - * SPDX-License-Identifier: Apache-2.0 */ import { execFileSync } from 'node:child_process' diff --git a/scripts/git-hooks/syntax-check-staged.mjs b/scripts/git-hooks/syntax-check-staged.mjs index 7dce2837eec..d927a8c02da 100644 --- a/scripts/git-hooks/syntax-check-staged.mjs +++ b/scripts/git-hooks/syntax-check-staged.mjs @@ -1,4 +1,21 @@ #!/usr/bin/env node +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * node --check on staged plain JS (not .jsx) under dashboardv2/public/js. */ From e792d56ad74cad0576a73e2fb1c846478b70cd1c Mon Sep 17 00:00:00 2001 From: Prasad Pawar Date: Wed, 27 May 2026 12:00:23 +0530 Subject: [PATCH 3/6] ATLAS-5293: ATLAS UI: Add repo-wide Git hooks and dashboard pre-commit/push verification --- dashboard/docs/GIT_HOOKS.md | 18 ++++++++++++++++++ dashboard/lint-staged.config.mjs | 15 ++++++++------- dashboard/package.json | 1 + .../scripts/check-push-new-file-license.mjs | 16 ++++++++-------- .../scripts/check-staged-new-file-license.mjs | 16 ++++++++-------- dashboard/scripts/git-precommit-verify.mjs | 16 ++++++++-------- dashboard/scripts/git-prepush-verify.mjs | 16 ++++++++-------- dashboard/scripts/install-git-hooks.mjs | 16 ++++++++-------- dashboard/scripts/lib/git-changed-files.mjs | 15 ++++++++------- .../scripts/lib/license-header-policy.mjs | 15 ++++++++------- dashboard/scripts/lib/test-path-helpers.mjs | 15 ++++++++------- .../lib/verify-dashboard-src-license.mjs | 15 ++++++++------- dashboard/scripts/run-precommit-local.mjs | 16 ++++++++-------- .../git-hooks/check-added-license-generic.mjs | 16 ++++++++-------- scripts/git-hooks/lib/extra-license-skip.mjs | 15 ++++++++------- scripts/git-hooks/lib/git-helpers.mjs | 15 ++++++++------- scripts/git-hooks/run-precommit.mjs | 16 ++++++++-------- scripts/git-hooks/run-prepush.mjs | 16 ++++++++-------- scripts/git-hooks/syntax-check-staged.mjs | 16 ++++++++-------- 19 files changed, 155 insertions(+), 129 deletions(-) diff --git a/dashboard/docs/GIT_HOOKS.md b/dashboard/docs/GIT_HOOKS.md index 50b5cb1dd43..1265b0a02d4 100644 --- a/dashboard/docs/GIT_HOOKS.md +++ b/dashboard/docs/GIT_HOOKS.md @@ -1,3 +1,21 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + # Atlas Git hooks (dashboard, dashboardv2, docs) Hooks run **locally** before `git commit` and `git push` so common issues are diff --git a/dashboard/lint-staged.config.mjs b/dashboard/lint-staged.config.mjs index 2098f952968..aaf7dbec3ff 100644 --- a/dashboard/lint-staged.config.mjs +++ b/dashboard/lint-staged.config.mjs @@ -1,10 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/package.json b/dashboard/package.json index 6cb8364f188..38c3106ec06 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -8,6 +8,7 @@ "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 200", + "typecheck": "tsc --noEmit", "preview": "vite preview", "prebuild": "cd src/views/Lineage/atlas-lineage && (npm ci 2>/dev/null || npm install) && npm run build" }, diff --git a/dashboard/scripts/check-push-new-file-license.mjs b/dashboard/scripts/check-push-new-file-license.mjs index 96cf4455870..20fe3b953d5 100644 --- a/dashboard/scripts/check-push-new-file-license.mjs +++ b/dashboard/scripts/check-push-new-file-license.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/check-staged-new-file-license.mjs b/dashboard/scripts/check-staged-new-file-license.mjs index a43d9e454cf..93167e0515e 100644 --- a/dashboard/scripts/check-staged-new-file-license.mjs +++ b/dashboard/scripts/check-staged-new-file-license.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/git-precommit-verify.mjs b/dashboard/scripts/git-precommit-verify.mjs index 83db31412d5..15b69c55255 100644 --- a/dashboard/scripts/git-precommit-verify.mjs +++ b/dashboard/scripts/git-precommit-verify.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/git-prepush-verify.mjs b/dashboard/scripts/git-prepush-verify.mjs index fdfd511d28c..9bb86682838 100644 --- a/dashboard/scripts/git-prepush-verify.mjs +++ b/dashboard/scripts/git-prepush-verify.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/install-git-hooks.mjs b/dashboard/scripts/install-git-hooks.mjs index fef98aaae7b..75624867e17 100644 --- a/dashboard/scripts/install-git-hooks.mjs +++ b/dashboard/scripts/install-git-hooks.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/lib/git-changed-files.mjs b/dashboard/scripts/lib/git-changed-files.mjs index daaa8572dd0..87ba710d78d 100644 --- a/dashboard/scripts/lib/git-changed-files.mjs +++ b/dashboard/scripts/lib/git-changed-files.mjs @@ -1,10 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/lib/license-header-policy.mjs b/dashboard/scripts/lib/license-header-policy.mjs index 29826a075f6..cb3f2abbdd7 100644 --- a/dashboard/scripts/lib/license-header-policy.mjs +++ b/dashboard/scripts/lib/license-header-policy.mjs @@ -1,10 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/lib/test-path-helpers.mjs b/dashboard/scripts/lib/test-path-helpers.mjs index 986f6b527f3..1ee3894c438 100644 --- a/dashboard/scripts/lib/test-path-helpers.mjs +++ b/dashboard/scripts/lib/test-path-helpers.mjs @@ -1,10 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/lib/verify-dashboard-src-license.mjs b/dashboard/scripts/lib/verify-dashboard-src-license.mjs index b36624a52ac..53511e49baa 100644 --- a/dashboard/scripts/lib/verify-dashboard-src-license.mjs +++ b/dashboard/scripts/lib/verify-dashboard-src-license.mjs @@ -1,10 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/dashboard/scripts/run-precommit-local.mjs b/dashboard/scripts/run-precommit-local.mjs index 64a531032a1..bd97a62f555 100644 --- a/dashboard/scripts/run-precommit-local.mjs +++ b/dashboard/scripts/run-precommit-local.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/scripts/git-hooks/check-added-license-generic.mjs b/scripts/git-hooks/check-added-license-generic.mjs index 3908d50895e..aa1f42c6379 100644 --- a/scripts/git-hooks/check-added-license-generic.mjs +++ b/scripts/git-hooks/check-added-license-generic.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/scripts/git-hooks/lib/extra-license-skip.mjs b/scripts/git-hooks/lib/extra-license-skip.mjs index c63f7286611..5cf4376fed1 100644 --- a/scripts/git-hooks/lib/extra-license-skip.mjs +++ b/scripts/git-hooks/lib/extra-license-skip.mjs @@ -1,10 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/scripts/git-hooks/lib/git-helpers.mjs b/scripts/git-hooks/lib/git-helpers.mjs index 0167159e80f..1614c615bad 100644 --- a/scripts/git-hooks/lib/git-helpers.mjs +++ b/scripts/git-hooks/lib/git-helpers.mjs @@ -1,10 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/scripts/git-hooks/run-precommit.mjs b/scripts/git-hooks/run-precommit.mjs index c2937c5787e..68b2c63f8a1 100644 --- a/scripts/git-hooks/run-precommit.mjs +++ b/scripts/git-hooks/run-precommit.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/scripts/git-hooks/run-prepush.mjs b/scripts/git-hooks/run-prepush.mjs index 88bbb60a9c8..053b7bd45db 100644 --- a/scripts/git-hooks/run-prepush.mjs +++ b/scripts/git-hooks/run-prepush.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * diff --git a/scripts/git-hooks/syntax-check-staged.mjs b/scripts/git-hooks/syntax-check-staged.mjs index d927a8c02da..6f117efec70 100644 --- a/scripts/git-hooks/syntax-check-staged.mjs +++ b/scripts/git-hooks/syntax-check-staged.mjs @@ -1,11 +1,11 @@ -#!/usr/bin/env node -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * From d4f220e927102cd5094ad0e2e159e7e558c4fef4 Mon Sep 17 00:00:00 2001 From: Prasad Pawar Date: Fri, 29 May 2026 12:29:24 +0530 Subject: [PATCH 4/6] ATLAS-5293: ATLAS UI: Add repo-wide Git hooks and dashboard pre-commit/push verification --- dashboard/.husky/pre-commit | 21 ------------------- dashboard/docs/GIT_HOOKS.md | 4 ++++ .../scripts/check-push-new-file-license.mjs | 3 +-- .../scripts/check-staged-new-file-license.mjs | 3 +-- dashboard/scripts/git-precommit-verify.mjs | 4 ++-- dashboard/scripts/git-prepush-verify.mjs | 4 ++-- 6 files changed, 10 insertions(+), 29 deletions(-) delete mode 100644 dashboard/.husky/pre-commit diff --git a/dashboard/.husky/pre-commit b/dashboard/.husky/pre-commit deleted file mode 100644 index 2be87c1c37b..00000000000 --- a/dashboard/.husky/pre-commit +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/sh -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Git hooks: core.hooksPath=.githooks at Atlas repo root (see dashboard npm prepare). -printf '%s\n' '[atlas] Hooks: git config core.hooksPath .githooks OR npm install (in dashboard/)' -exit 0 diff --git a/dashboard/docs/GIT_HOOKS.md b/dashboard/docs/GIT_HOOKS.md index 1265b0a02d4..96646be5bc2 100644 --- a/dashboard/docs/GIT_HOOKS.md +++ b/dashboard/docs/GIT_HOOKS.md @@ -43,6 +43,10 @@ git config --get core.hooksPath The active hook scripts live in **`.githooks/`** at the **repository root**. `dashboard/.githooks/*` only forwards to the root hooks (legacy path compat). +**This repo does not use Husky.** Git invokes hooks via **`core.hooksPath=.githooks`** +only (`prepare`/`install-git-hooks.mjs` or manual `git config`); there is no +`husky` npm package. + ## What runs when ### `pre-commit` (root: `scripts/git-hooks/run-precommit.mjs`) diff --git a/dashboard/scripts/check-push-new-file-license.mjs b/dashboard/scripts/check-push-new-file-license.mjs index 20fe3b953d5..13cf5e4beb3 100644 --- a/dashboard/scripts/check-push-new-file-license.mjs +++ b/dashboard/scripts/check-push-new-file-license.mjs @@ -35,8 +35,7 @@ import { if ( process.env.SKIP_DASHBOARD_HOOKS === '1' || - process.env.SKIP_DASHBOARD_LICENSE_CHECK === '1' || - process.env.HUSKY === '0' + process.env.SKIP_DASHBOARD_LICENSE_CHECK === '1' ) { process.exit(0) } diff --git a/dashboard/scripts/check-staged-new-file-license.mjs b/dashboard/scripts/check-staged-new-file-license.mjs index 93167e0515e..7e55b758111 100644 --- a/dashboard/scripts/check-staged-new-file-license.mjs +++ b/dashboard/scripts/check-staged-new-file-license.mjs @@ -34,8 +34,7 @@ import { if ( process.env.SKIP_DASHBOARD_HOOKS === '1' || - process.env.SKIP_DASHBOARD_LICENSE_CHECK === '1' || - process.env.HUSKY === '0' + process.env.SKIP_DASHBOARD_LICENSE_CHECK === '1' ) { process.exit(0) } diff --git a/dashboard/scripts/git-precommit-verify.mjs b/dashboard/scripts/git-precommit-verify.mjs index 15b69c55255..0fd292ba519 100644 --- a/dashboard/scripts/git-precommit-verify.mjs +++ b/dashboard/scripts/git-precommit-verify.mjs @@ -18,13 +18,13 @@ /** * Pre-commit: ensure UI changes stage tests; lint-staged runs ESLint after this. - * Skip: SKIP_DASHBOARD_HOOKS=1 or HUSKY=0 or SKIP_DASHBOARD_TEST_GUARD=1 + * Skip: SKIP_DASHBOARD_HOOKS=1 or SKIP_DASHBOARD_TEST_GUARD=1 */ import { stagedIncludesTestWhenUiChanges } from './lib/test-path-helpers.mjs' import { getStagedFiles } from './lib/git-changed-files.mjs' -if (process.env.SKIP_DASHBOARD_HOOKS === '1' || process.env.HUSKY === '0') { +if (process.env.SKIP_DASHBOARD_HOOKS === '1') { process.exit(0) } diff --git a/dashboard/scripts/git-prepush-verify.mjs b/dashboard/scripts/git-prepush-verify.mjs index 9bb86682838..7f7bc6b2661 100644 --- a/dashboard/scripts/git-prepush-verify.mjs +++ b/dashboard/scripts/git-prepush-verify.mjs @@ -18,7 +18,7 @@ /** * Pre-push: impact-related Jest tests, ESLint (src), production build. - * Skip: SKIP_DASHBOARD_HOOKS=1 or HUSKY=0 + * Skip: SKIP_DASHBOARD_HOOKS=1 */ import { execFileSync, execSync, spawnSync } from 'node:child_process' @@ -33,7 +33,7 @@ import { toDashboardRelative, } from './lib/test-path-helpers.mjs' -if (process.env.SKIP_DASHBOARD_HOOKS === '1' || process.env.HUSKY === '0') { +if (process.env.SKIP_DASHBOARD_HOOKS === '1') { process.exit(0) } From 91ff6c75f4e0d1c85b27053bc21a6d5963ab2951 Mon Sep 17 00:00:00 2001 From: Prasad Pawar Date: Mon, 1 Jun 2026 18:17:35 +0530 Subject: [PATCH 5/6] ATLAS-5293: ATLAS UI: Add repo-wide Git hooks and dashboard pre-commit/push verification --- .githooks/pre-push | 2 +- dashboard/docs/GIT_HOOKS.md | 18 ++++------- dashboard/scripts/git-prepush-verify.mjs | 3 +- scripts/git-hooks/run-prepush.mjs | 38 ++---------------------- 4 files changed, 11 insertions(+), 50 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index aef7dc7545f..bae7541aa07 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -16,7 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Atlas monorepo: pre-push (dashboard Jest/eslint/build; dashboardv2 + docs build) +# Atlas monorepo: pre-push (dashboard Jest/eslint) # Skip: SKIP_ATLAS_HOOKS=1 | SKIP_ALL_ATLAS_GIT_HOOKS=1 set -e if [ "$SKIP_ATLAS_HOOKS" = "1" ] || [ "$SKIP_ALL_ATLAS_GIT_HOOKS" = "1" ]; then exit 0; fi diff --git a/dashboard/docs/GIT_HOOKS.md b/dashboard/docs/GIT_HOOKS.md index 96646be5bc2..f78f08b8d03 100644 --- a/dashboard/docs/GIT_HOOKS.md +++ b/dashboard/docs/GIT_HOOKS.md @@ -61,13 +61,14 @@ Runs **only for packages that have staged paths** under that prefix. ### `pre-push` (root: `scripts/git-hooks/run-prepush.mjs`) -Runs for each package **if commits in the push range** touch that prefix. +Runs when commits in the push range touch **`dashboard/`**. | Area | Checks | |------|--------| -| **dashboard** | **RAT-aligned ASF header** on **new** `dashboard/src/` files in the push range, colocated tests on disk, **`jest --findRelatedTests`**, **`eslint src`**, **`npm run build`**. | -| **dashboardv2** | **`npm run build`** (Grunt). | -| **docs** | **`npm run build`** (Docz). | +| **dashboard** | **RAT-aligned ASF header** on **new** `dashboard/src/` files in the push range, colocated tests on disk for UI changes, **`jest --findRelatedTests`**, **`eslint src`**. | + +**No build** runs on pre-push (dashboard, dashboardv2, or docs). Use CI or run +`npm run build` locally when needed. ## Skip hooks (emergency / slow machines) @@ -100,13 +101,6 @@ SKIP_DASHBOARD_TYPECHECK=1 git commit ... # tsc on commit SKIP_ATLAS_LICENSE_CHECK=1 git commit ... ``` -Skip **long builds** on push: - -```bash -SKIP_DASHBOARDV2_BUILD=1 git push ... -SKIP_DOCS_BUILD=1 git push ... -``` - ## Manual run (no Git hook) From **repo root** `atlas/`: @@ -144,6 +138,6 @@ cd dashboard && npm run verify:prepush | `dashboard/scripts/install-git-hooks.mjs` | Sets `core.hooksPath=.githooks` | | `dashboard/scripts/git-precommit-verify.mjs` | Dashboard staged UI ↔ test guard | | `dashboard/scripts/check-staged-new-file-license.mjs` | Dashboard ASF on new files | -| `dashboard/scripts/git-prepush-verify.mjs` | Dashboard Jest, ESLint, build | +| `dashboard/scripts/git-prepush-verify.mjs` | Dashboard Jest, ESLint | | `dashboard/scripts/run-precommit-local.mjs` | `npm run verify:precommit` (dashboard only) | | `dashboard/lint-staged.config.mjs` | ESLint on staged dashboard sources | diff --git a/dashboard/scripts/git-prepush-verify.mjs b/dashboard/scripts/git-prepush-verify.mjs index 7f7bc6b2661..f1559fc198b 100644 --- a/dashboard/scripts/git-prepush-verify.mjs +++ b/dashboard/scripts/git-prepush-verify.mjs @@ -17,7 +17,7 @@ */ /** - * Pre-push: impact-related Jest tests, ESLint (src), production build. + * Pre-push: impact-related Jest tests, ESLint (src). * Skip: SKIP_DASHBOARD_HOOKS=1 */ @@ -126,7 +126,6 @@ if (jestSourceArgs.length > 0) { run( 'npx eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 200', ) -run('npm run build') console.log('\x1b[32m[dashboard pre-push]\x1b[0m All checks passed.\n') process.exit(0) diff --git a/scripts/git-hooks/run-prepush.mjs b/scripts/git-hooks/run-prepush.mjs index 053b7bd45db..c31a0b291b6 100644 --- a/scripts/git-hooks/run-prepush.mjs +++ b/scripts/git-hooks/run-prepush.mjs @@ -17,7 +17,7 @@ */ /** - * Repo-wide pre-push: dashboard (tests + eslint + build), dashboardv2 build, docs build. + * Repo-wide pre-push: dashboard (tests + eslint). */ import { execFileSync } from 'node:child_process' @@ -39,14 +39,12 @@ if ( const changed = getPushRangeFiles(repoRoot) const touchDashboard = changed.some((p) => p.startsWith('dashboard/')) -const touchV2 = changed.some((p) => p.startsWith('dashboardv2/')) -const touchDocs = changed.some((p) => p.startsWith('docs/')) -if (!touchDashboard && !touchV2 && !touchDocs) { +if (!touchDashboard) { process.exit(0) } -if (touchDashboard && process.env.SKIP_DASHBOARD_HOOKS !== '1') { +if (process.env.SKIP_DASHBOARD_HOOKS !== '1') { console.log('\x1b[35m[atlas pre-push]\x1b[0m dashboard package…') execFileSync(process.execPath, ['scripts/git-prepush-verify.mjs'], { cwd: join(repoRoot, 'dashboard'), @@ -54,35 +52,5 @@ if (touchDashboard && process.env.SKIP_DASHBOARD_HOOKS !== '1') { }) } -if (touchV2 && process.env.SKIP_DASHBOARDV2_HOOKS !== '1') { - if (process.env.SKIP_DASHBOARDV2_BUILD === '1') { - console.log( - '\x1b[33m[atlas pre-push]\x1b[0m SKIP_DASHBOARDV2_BUILD=1 — skipping dashboardv2 npm run build.', - ) - } else { - console.log('\x1b[35m[atlas pre-push]\x1b[0m dashboardv2 — npm run build…') - execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { - cwd: join(repoRoot, 'dashboardv2'), - stdio: 'inherit', - shell: process.platform === 'win32', - }) - } -} - -if (touchDocs && process.env.SKIP_DOCS_HOOKS !== '1') { - if (process.env.SKIP_DOCS_BUILD === '1') { - console.log( - '\x1b[33m[atlas pre-push]\x1b[0m SKIP_DOCS_BUILD=1 — skipping docs npm run build.', - ) - } else { - console.log('\x1b[35m[atlas pre-push]\x1b[0m docs — npm run build…') - execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { - cwd: join(repoRoot, 'docs'), - stdio: 'inherit', - shell: process.platform === 'win32', - }) - } -} - console.log('\x1b[32m[atlas pre-push]\x1b[0m Done.\n') process.exit(0) From 51062b7e17b70d131a08cce8ccb93848fe33e626 Mon Sep 17 00:00:00 2001 From: Prasad Pawar Date: Mon, 1 Jun 2026 18:28:14 +0530 Subject: [PATCH 6/6] ATLAS-5293: ATLAS UI: Add repo-wide Git hooks and dashboard pre-commit/push verification --- .githooks/pre-commit | 2 +- .githooks/pre-push | 2 +- dashboard/docs/GIT_HOOKS.md | 44 ++++++------- ...erify.mjs => git-precommit-tests-lint.mjs} | 63 +++++-------------- dashboard/scripts/git-precommit-verify.mjs | 41 ++++++++++-- dashboard/scripts/run-precommit-local.mjs | 17 +---- scripts/git-hooks/run-precommit.mjs | 18 +----- scripts/git-hooks/run-prepush.mjs | 29 +-------- 8 files changed, 78 insertions(+), 138 deletions(-) rename dashboard/scripts/{git-prepush-verify.mjs => git-precommit-tests-lint.mjs} (60%) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 63f35f074db..adb09f75fa0 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -16,7 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Atlas monorepo: pre-commit (license, lint, typecheck, syntax; dashboard test guard) +# Atlas monorepo: pre-commit (license, Jest, ESLint, typecheck, syntax; dashboard test guard) # Skip: SKIP_ATLAS_HOOKS=1 | SKIP_ALL_ATLAS_GIT_HOOKS=1 set -e if [ "$SKIP_ATLAS_HOOKS" = "1" ] || [ "$SKIP_ALL_ATLAS_GIT_HOOKS" = "1" ]; then exit 0; fi diff --git a/.githooks/pre-push b/.githooks/pre-push index bae7541aa07..11b6f0c1f70 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -16,7 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Atlas monorepo: pre-push (dashboard Jest/eslint) +# Atlas monorepo: pre-push (no-op; checks run on pre-commit) # Skip: SKIP_ATLAS_HOOKS=1 | SKIP_ALL_ATLAS_GIT_HOOKS=1 set -e if [ "$SKIP_ATLAS_HOOKS" = "1" ] || [ "$SKIP_ALL_ATLAS_GIT_HOOKS" = "1" ]; then exit 0; fi diff --git a/dashboard/docs/GIT_HOOKS.md b/dashboard/docs/GIT_HOOKS.md index f78f08b8d03..acef4f8b897 100644 --- a/dashboard/docs/GIT_HOOKS.md +++ b/dashboard/docs/GIT_HOOKS.md @@ -18,8 +18,9 @@ # Atlas Git hooks (dashboard, dashboardv2, docs) -Hooks run **locally** before `git commit` and `git push` so common issues are -caught early. **CI** on the server is still required to enforce merges. +Hooks run **locally** before `git commit` so common issues are caught early. +**Pre-push runs no checks** (all dashboard verification is on pre-commit). +**CI** on the server is still required to enforce merges. ## One-time setup (per clone) @@ -55,20 +56,14 @@ Runs **only for packages that have staged paths** under that prefix. | Area | When staged under … | Checks | |------|---------------------|--------| -| **dashboard** | `dashboard/` | (1) **UI test guard** — `src/views`, `src/components`, `App.tsx` / `Main.tsx` / `ErrorBoundary.tsx` must include a **staged** test file; (2) **RAT-aligned ASF license** on **new** files under `dashboard/src/` (`license-header-policy.mjs` markers, same bar as CI RAT); (3) **lint-staged** → ESLint on staged TS/TSX; (4) **`npm run typecheck`** (`tsc --noEmit`). | -| **dashboardv2** | `dashboardv2/` | (1) **ASF license** on **new** `.js`/`.jsx`/`.ts`/`.tsx` (skips `node_modules`, `bin/`, `external_lib`, `.min.js`); (2) **`node --check`** on staged plain `.js` under `dashboardv2/public/js/` (syntax). **No** Jest/test guard (legacy Grunt UI). | +| **dashboard** | `dashboard/` | (1) **UI test guard** — staged UI changes must include a **staged** test file; staged UI files must have colocated `__tests__` or `*.test.ts(x)` on disk; (2) **RAT-aligned ASF license** on **new** files under `dashboard/src/`; (3) **`jest --findRelatedTests`** on staged `.ts`/`.tsx` under `src/`; (4) **`eslint src`** (full `src/` tree); (5) **`npm run typecheck`** (`tsc --noEmit`). | +| **dashboardv2** | `dashboardv2/` | (1) **ASF license** on **new** `.js`/`.jsx`/`.ts`/`.tsx` (skips `node_modules`, `bin/`, `external_lib`, `.min.js`); (2) **`node --check`** on staged plain `.js` under `dashboardv2/public/js/` (syntax). | | **docs** | `docs/` | (1) **ASF license** on **new** sources (skips `node_modules`, `site/`, `bin/`, `docz-lib/`); (2) **`node --check`** on staged **plain** `docs/**/*.js` outside theme/webapp JSX trees. | ### `pre-push` (root: `scripts/git-hooks/run-prepush.mjs`) -Runs when commits in the push range touch **`dashboard/`**. - -| Area | Checks | -|------|--------| -| **dashboard** | **RAT-aligned ASF header** on **new** `dashboard/src/` files in the push range, colocated tests on disk for UI changes, **`jest --findRelatedTests`**, **`eslint src`**. | - -**No build** runs on pre-push (dashboard, dashboardv2, or docs). Use CI or run -`npm run build` locally when needed. +**No checks.** Exits immediately. Use pre-commit before each commit, or CI on +the server for push/merge validation. ## Skip hooks (emergency / slow machines) @@ -76,7 +71,6 @@ Disable **everything**: ```bash SKIP_ATLAS_HOOKS=1 git commit ... -SKIP_ATLAS_HOOKS=1 git push ... ``` Per **package**: @@ -87,11 +81,11 @@ SKIP_DASHBOARDV2_HOOKS=1 git commit ... SKIP_DOCS_HOOKS=1 git commit ... ``` -**dashboard** only (still documented): +**dashboard** only: ```bash -SKIP_DASHBOARD_TEST_GUARD=1 git commit ... # staged test file rule -SKIP_DASHBOARD_LICENSE_CHECK=1 git commit ... # RAT-aligned ASF on new dashboard/src (also used by pre-push added-file check) +SKIP_DASHBOARD_TEST_GUARD=1 git commit ... # UI ↔ test rules +SKIP_DASHBOARD_LICENSE_CHECK=1 git commit ... # RAT-aligned ASF on new dashboard/src SKIP_DASHBOARD_TYPECHECK=1 git commit ... # tsc on commit ``` @@ -107,16 +101,16 @@ From **repo root** `atlas/`: ```bash node scripts/git-hooks/run-precommit.mjs -node scripts/git-hooks/run-prepush.mjs ``` -**dashboard**-only local verify (same as before): +**dashboard**-only local verify: ```bash cd dashboard && npm run verify:precommit -cd dashboard && npm run verify:prepush ``` +(`verify:precommit` requires the npm script in `dashboard/package.json`.) + ## Limitations - **dashboardv2** has no ESLint in-repo; **`node --check`** only catches **syntax** on selected `.js` paths, not style. @@ -128,16 +122,16 @@ cd dashboard && npm run verify:prepush | Path | Role | |------|------| | `.githooks/pre-commit` | Root hook → `run-precommit.mjs` | -| `.githooks/pre-push` | Root hook → `run-prepush.mjs` | +| `.githooks/pre-push` | Root hook → `run-prepush.mjs` (no-op) | | `scripts/git-hooks/run-precommit.mjs` | Monorepo pre-commit orchestration | -| `scripts/git-hooks/run-prepush.mjs` | Monorepo pre-push orchestration | +| `scripts/git-hooks/run-prepush.mjs` | No-op (checks moved to pre-commit) | | `scripts/git-hooks/check-added-license-generic.mjs` | ASF header for v2/docs new files | | `scripts/git-hooks/syntax-check-staged.mjs` | `node --check` for v2/docs | | `scripts/git-hooks/lib/git-helpers.mjs` | `git diff` helpers | | `scripts/git-hooks/lib/extra-license-skip.mjs` | Path skip rules for v2/docs | | `dashboard/scripts/install-git-hooks.mjs` | Sets `core.hooksPath=.githooks` | -| `dashboard/scripts/git-precommit-verify.mjs` | Dashboard staged UI ↔ test guard | +| `dashboard/scripts/git-precommit-verify.mjs` | Dashboard UI ↔ test guard | | `dashboard/scripts/check-staged-new-file-license.mjs` | Dashboard ASF on new files | -| `dashboard/scripts/git-prepush-verify.mjs` | Dashboard Jest, ESLint | -| `dashboard/scripts/run-precommit-local.mjs` | `npm run verify:precommit` (dashboard only) | -| `dashboard/lint-staged.config.mjs` | ESLint on staged dashboard sources | +| `dashboard/scripts/git-precommit-tests-lint.mjs` | Dashboard Jest + ESLint | +| `dashboard/scripts/run-precommit-local.mjs` | Manual pre-commit verify (dashboard) | +| `dashboard/lint-staged.config.mjs` | Legacy ESLint-on-staged config (unused by hooks) | diff --git a/dashboard/scripts/git-prepush-verify.mjs b/dashboard/scripts/git-precommit-tests-lint.mjs similarity index 60% rename from dashboard/scripts/git-prepush-verify.mjs rename to dashboard/scripts/git-precommit-tests-lint.mjs index f1559fc198b..802f9dccdf2 100644 --- a/dashboard/scripts/git-prepush-verify.mjs +++ b/dashboard/scripts/git-precommit-tests-lint.mjs @@ -17,21 +17,17 @@ */ /** - * Pre-push: impact-related Jest tests, ESLint (src). + * Pre-commit: Jest --findRelatedTests on staged TS/TSX, then ESLint on src/. * Skip: SKIP_DASHBOARD_HOOKS=1 */ -import { execFileSync, execSync, spawnSync } from 'node:child_process' +import { execSync, spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { join, relative } from 'node:path' import { fileURLToPath } from 'node:url' -import { getPushRangeFiles } from './lib/git-changed-files.mjs' -import { - allUiChangesHaveTestHome, - isUiSourcePath, - toDashboardRelative, -} from './lib/test-path-helpers.mjs' +import { getStagedFiles } from './lib/git-changed-files.mjs' +import { toDashboardRelative } from './lib/test-path-helpers.mjs' if (process.env.SKIP_DASHBOARD_HOOKS === '1') { process.exit(0) @@ -44,23 +40,13 @@ if (!existsSync(join(dashboardRoot, 'package.json'))) { process.exit(1) } -if (process.env.SKIP_DASHBOARD_LICENSE_CHECK !== '1') { - console.log( - '\x1b[35m[dashboard pre-push]\x1b[0m RAT-aligned ASF header on newly added dashboard/src files…', - ) - execFileSync(process.execPath, ['scripts/check-push-new-file-license.mjs'], { - cwd: dashboardRoot, - stdio: 'inherit', - }) -} - const run = (cmd, opts = {}) => { console.log(`\x1b[36m▶\x1b[0m ${cmd}`) execSync(cmd, { stdio: 'inherit', cwd: dashboardRoot, ...opts }) } -const repoPaths = getPushRangeFiles() -const dashboardPaths = repoPaths.filter( +const staged = getStagedFiles() +const dashboardPaths = staged.filter( (p) => p.startsWith('dashboard/') || p.startsWith('src/'), ) @@ -77,36 +63,17 @@ const jestSourceArgs = dashRelFiles.filter((p) => { return /\.(ts|tsx)$/.test(p) }) -if (process.env.SKIP_DASHBOARD_TEST_GUARD !== '1') { - const hasUi = dashboardPaths.map(toDashboardRelative).some(isUiSourcePath) - if (hasUi) { - const { ok, missing } = allUiChangesHaveTestHome( - dashboardRoot, - dashboardPaths, - ) - if (!ok) { - console.error( - '\x1b[31m[dashboard pre-push]\x1b[0m These UI files have no colocated __tests__ or *.test.ts(x):', - ) - for (const m of missing) { - console.error(` - ${m}`) - } - console.error( - 'Add tests or set SKIP_DASHBOARD_TEST_GUARD=1 only for exceptions.\n', - ) - process.exit(1) - } - } +if (dashRelFiles.length > 0) { + console.log('\x1b[35m[dashboard pre-commit]\x1b[0m Staged paths (sample):') + console.log( + dashRelFiles.slice(0, 20).join('\n') + + (dashRelFiles.length > 20 ? '\n…' : ''), + ) } -console.log('\x1b[35m[dashboard pre-push]\x1b[0m Changed paths in range (sample):') -console.log( - dashRelFiles.slice(0, 20).join('\n') + (dashRelFiles.length > 20 ? '\n…' : ''), -) - if (jestSourceArgs.length > 0) { console.log( - '\x1b[35m[dashboard pre-push]\x1b[0m Running Jest --findRelatedTests (impact surface):', + '\x1b[35m[dashboard pre-commit]\x1b[0m Running Jest --findRelatedTests (staged sources):', ) const rel = jestSourceArgs.map((f) => relative(dashboardRoot, join(dashboardRoot, f)).replace(/\\/g, '/'), @@ -119,7 +86,7 @@ if (jestSourceArgs.length > 0) { if (res.status !== 0) process.exit(res.status ?? 1) } else { console.log( - '\x1b[33m[dashboard pre-push]\x1b[0m No TS source files in diff for --findRelatedTests; skipping Jest.', + '\x1b[33m[dashboard pre-commit]\x1b[0m No staged TS source files for --findRelatedTests; skipping Jest.', ) } @@ -127,5 +94,5 @@ run( 'npx eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 200', ) -console.log('\x1b[32m[dashboard pre-push]\x1b[0m All checks passed.\n') +console.log('\x1b[32m[dashboard pre-commit]\x1b[0m Jest and ESLint passed.\n') process.exit(0) diff --git a/dashboard/scripts/git-precommit-verify.mjs b/dashboard/scripts/git-precommit-verify.mjs index 0fd292ba519..134b7ebfbee 100644 --- a/dashboard/scripts/git-precommit-verify.mjs +++ b/dashboard/scripts/git-precommit-verify.mjs @@ -17,12 +17,20 @@ */ /** - * Pre-commit: ensure UI changes stage tests; lint-staged runs ESLint after this. + * Pre-commit: staged UI test guard + colocated tests on disk. * Skip: SKIP_DASHBOARD_HOOKS=1 or SKIP_DASHBOARD_TEST_GUARD=1 */ -import { stagedIncludesTestWhenUiChanges } from './lib/test-path-helpers.mjs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + import { getStagedFiles } from './lib/git-changed-files.mjs' +import { + allUiChangesHaveTestHome, + isUiSourcePath, + stagedIncludesTestWhenUiChanges, + toDashboardRelative, +} from './lib/test-path-helpers.mjs' if (process.env.SKIP_DASHBOARD_HOOKS === '1') { process.exit(0) @@ -32,6 +40,9 @@ if (process.env.SKIP_DASHBOARD_TEST_GUARD === '1') { process.exit(0) } +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const dashboardRoot = join(__dirname, '..') + const staged = getStagedFiles() const dashboardPaths = staged.filter( (p) => p.startsWith('dashboard/') || p.startsWith('src/'), @@ -41,10 +52,30 @@ if (dashboardPaths.length === 0) { process.exit(0) } -const guard = stagedIncludesTestWhenUiChanges(dashboardPaths) -if (!guard.ok) { - console.error('\x1b[31m[dashboard pre-commit]\x1b[0m', guard.message) +const stagedGuard = stagedIncludesTestWhenUiChanges(dashboardPaths) +if (!stagedGuard.ok) { + console.error('\x1b[31m[dashboard pre-commit]\x1b[0m', stagedGuard.message) process.exit(1) } +const hasUi = dashboardPaths.map(toDashboardRelative).some(isUiSourcePath) +if (hasUi) { + const { ok, missing } = allUiChangesHaveTestHome( + dashboardRoot, + dashboardPaths, + ) + if (!ok) { + console.error( + '\x1b[31m[dashboard pre-commit]\x1b[0m These staged UI files have no colocated __tests__ or *.test.ts(x):', + ) + for (const m of missing) { + console.error(` - ${m}`) + } + console.error( + 'Add tests or set SKIP_DASHBOARD_TEST_GUARD=1 only for exceptions.\n', + ) + process.exit(1) + } +} + process.exit(0) diff --git a/dashboard/scripts/run-precommit-local.mjs b/dashboard/scripts/run-precommit-local.mjs index bd97a62f555..e86590d8bfe 100644 --- a/dashboard/scripts/run-precommit-local.mjs +++ b/dashboard/scripts/run-precommit-local.mjs @@ -20,7 +20,7 @@ * Run the same checks as .githooks/pre-commit (for manual verification). * Execute from dashboard/: npm run verify:precommit * - * Order: test guard → ASF header on new files → lint-staged → tsc --noEmit + * Order: test guard → ASF header → Jest + ESLint → tsc --noEmit */ import { execFileSync } from 'node:child_process' @@ -50,20 +50,9 @@ try { }) }) - const repoRoot = String( - execFileSync('git', ['rev-parse', '--show-toplevel'], { - encoding: 'utf8', + run('Jest (related tests) + ESLint (src/)', () => { + execFileSync(process.execPath, ['scripts/git-precommit-tests-lint.mjs'], { cwd: dashboardDir, - }), - ).trim() - - run('lint-staged (ESLint on staged dashboard/src)', () => { - const lintStagedCli = join( - dashboardDir, - 'node_modules/lint-staged/bin/lint-staged.js', - ) - execFileSync(process.execPath, [lintStagedCli, '--config', 'dashboard/lint-staged.config.mjs'], { - cwd: repoRoot, stdio: 'inherit', }) }) diff --git a/scripts/git-hooks/run-precommit.mjs b/scripts/git-hooks/run-precommit.mjs index 68b2c63f8a1..5ee466400f7 100644 --- a/scripts/git-hooks/run-precommit.mjs +++ b/scripts/git-hooks/run-precommit.mjs @@ -62,23 +62,7 @@ const runDash = (title, file) => { if (touchDashboard && process.env.SKIP_DASHBOARD_HOOKS !== '1') { runDash('UI ↔ staged test guard', 'git-precommit-verify.mjs') runDash('ASF license (new staged files under src/)', 'check-staged-new-file-license.mjs') - - const lintStagedCli = join( - repoRoot, - 'dashboard/node_modules/lint-staged/bin/lint-staged.js', - ) - try { - execFileSync( - process.execPath, - [lintStagedCli, '--config', 'dashboard/lint-staged.config.mjs'], - { - cwd: repoRoot, - stdio: 'inherit', - }, - ) - } catch { - process.exit(1) - } + runDash('Jest (related tests) + ESLint (src/)', 'git-precommit-tests-lint.mjs') if (process.env.SKIP_DASHBOARD_TYPECHECK !== '1') { execFileSync( diff --git a/scripts/git-hooks/run-prepush.mjs b/scripts/git-hooks/run-prepush.mjs index c31a0b291b6..613957dfa01 100644 --- a/scripts/git-hooks/run-prepush.mjs +++ b/scripts/git-hooks/run-prepush.mjs @@ -17,19 +17,10 @@ */ /** - * Repo-wide pre-push: dashboard (tests + eslint). + * Pre-push: no checks (all dashboard verification runs on pre-commit). + * Hook file kept so core.hooksPath stays stable; exits immediately. */ -import { execFileSync } from 'node:child_process' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -import { getRepoRoot, getPushRangeFiles } from './lib/git-helpers.mjs' - -const __dirname = dirname(fileURLToPath(import.meta.url)) -const scriptsDir = join(__dirname, '..') -const repoRoot = getRepoRoot(scriptsDir) - if ( process.env.SKIP_ATLAS_HOOKS === '1' || process.env.SKIP_ALL_ATLAS_GIT_HOOKS === '1' @@ -37,20 +28,4 @@ if ( process.exit(0) } -const changed = getPushRangeFiles(repoRoot) -const touchDashboard = changed.some((p) => p.startsWith('dashboard/')) - -if (!touchDashboard) { - process.exit(0) -} - -if (process.env.SKIP_DASHBOARD_HOOKS !== '1') { - console.log('\x1b[35m[atlas pre-push]\x1b[0m dashboard package…') - execFileSync(process.execPath, ['scripts/git-prepush-verify.mjs'], { - cwd: join(repoRoot, 'dashboard'), - stdio: 'inherit', - }) -} - -console.log('\x1b[32m[atlas pre-push]\x1b[0m Done.\n') process.exit(0)