diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32e561ab..44fcd639 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -285,23 +285,6 @@ jobs: env: GITHUB_STEP_SUMMARY: /dev/null - no-private-leak: - name: No private leak - timeout-minutes: 2 - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - submodules: false - fetch-depth: 0 # setup action needs PR base SHA for turbo affected comparisons - - - uses: $/.github/actions/setup/ - - - name: Check for private content on public branch - run: pnpm run check:no-private-leak -- --mode workspace --visibility auto - check: name: Repository checks timeout-minutes: 2 @@ -551,7 +534,6 @@ jobs: starters-copy-out, ssr-sidecar-image, lint-dependencies, - no-private-leak, zizmor, check-changeset, ] diff --git a/.husky/pre-commit b/.husky/pre-commit index 6b75b9f0..0e66169f 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,3 @@ #!/usr/bin/env sh pnpm run lint-staged -node scripts/check-no-private-leak.mts --mode staged --visibility auto diff --git a/.husky/pre-push b/.husky/pre-push index 96c6306b..9fc43374 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -51,5 +51,3 @@ if [ -n "$blocked" ]; then printf 'Blocked branch(es):%s\n' "$blocked" exit 1 fi - -printf '%s' "$push_data" | node scripts/check-no-private-leak.mts --mode push --visibility public --remote "$remote" diff --git a/configs/lint-staged.mts b/configs/lint-staged.mts index b38813c9..8d30eaf0 100644 --- a/configs/lint-staged.mts +++ b/configs/lint-staged.mts @@ -6,7 +6,7 @@ import type {Configuration} from "lint-staged"; type PackageInfo = { filter: string; relativePath: string; - workspaceDir: "packages" | "apps" | "private/packages" | "private/apps"; + workspaceDir: "packages" | "apps"; }; export const PRETTIER_WRITE = "pnpx prettier --write"; @@ -14,15 +14,6 @@ export const PRETTIER_WRITE = "pnpx prettier --write"; const toRepoRelativePath = (filePath: string): string => { const parts = path.normalize(filePath).split(path.sep).filter(Boolean); - const privateIdx = parts.indexOf("private"); - if ( - privateIdx !== -1 && - (parts[privateIdx + 1] === "apps" || - parts[privateIdx + 1] === "packages") - ) { - return parts.slice(privateIdx).join(path.sep); - } - const packagesIdx = parts.indexOf("packages"); if (packagesIdx !== -1) { return parts.slice(packagesIdx).join(path.sep); @@ -39,24 +30,6 @@ const toRepoRelativePath = (filePath: string): string => { export const getPackageInfo = (filePath: string): PackageInfo | null => { const parts = toRepoRelativePath(filePath).split(path.sep); - if ( - parts[0] === "private" && - (parts[1] === "apps" || parts[1] === "packages") && - parts[2] - ) { - const relativePath = parts.slice(3).join(path.sep); - if (!relativePath) { - return null; - } - - return { - filter: parts[2], - relativePath, - workspaceDir: `${parts[0]}/${parts[1]}` as - "private/packages" | "private/apps", - }; - } - if ((parts[0] === "packages" || parts[0] === "apps") && parts[1]) { const relativePath = parts.slice(2).join(path.sep); if (!relativePath) { diff --git a/configs/lint-staged.test.mts b/configs/lint-staged.test.mts index a9f6419a..2761fbc9 100644 --- a/configs/lint-staged.test.mts +++ b/configs/lint-staged.test.mts @@ -16,17 +16,6 @@ describe("getPackageInfo", () => { }); }); - it("resolves private package paths", () => { - assert.deepEqual( - getPackageInfo("/repo/private/packages/foo/src/index.ts"), - { - filter: "foo", - relativePath: "src/index.ts", - workspaceDir: "private/packages", - }, - ); - }); - it("returns null for paths outside packages or apps", () => { assert.equal(getPackageInfo("/repo/scripts/check.mts"), null); }); diff --git a/docs/development/LINKED_HOST_APPS.md b/docs/development/LINKED_HOST_APPS.md index d0cbdc77..1c7410ca 100644 --- a/docs/development/LINKED_HOST_APPS.md +++ b/docs/development/LINKED_HOST_APPS.md @@ -6,8 +6,8 @@ pre-OSS migration / parity work. ## Prefer a stable checkout -Point `link:../mapsight/packages/...` at a **stable** tree (`main` or -`private/develop`), not an ephemeral PR worktree under `mapsight-worktrees/`. +Point `link:../mapsight/packages/...` at a **stable** tree (`main`), not an +ephemeral PR worktree under `mapsight-worktrees/`. Worktrees disappear after merge and leave broken Vite/Sass absolute paths (e.g. missing `ol/ol.css` still resolving into a deleted directory). @@ -56,17 +56,3 @@ pnpm worktrees:stale ``` Does not delete anything. Skips `private/*` and `wip/*` branches. - -## Private sync - -When this checkout includes `private/`, integrate public `main` **into** -private branches only — see [`private/README.md`](../../private/README.md) -(if present): - -```bash -git fetch origin -git switch private/develop -git merge origin/main -pnpm install -git push private private/develop -``` diff --git a/docs/development/STANDARDS.md b/docs/development/STANDARDS.md index 62e0529c..59b91e41 100644 --- a/docs/development/STANDARDS.md +++ b/docs/development/STANDARDS.md @@ -75,7 +75,7 @@ Pre-commit: Husky runs `lint-staged` on staged files. | Script | Purpose | | ------------------------------- | ----------------------------------------------------------- | -| `check:no-private-leak` | No `private/` paths on public branches; lockfile importers | +| `check:starter-pins` | Starter apps pin published `@mapsight/*` (no workspace:) | | `check:node-ts-runtime` | Node scripts use plain `node file.ts` — no ts-node/tsx/jiti | | `check:typecheck-test-coverage` | Packages with tests must include them in `tsconfig.json` | @@ -102,34 +102,6 @@ Aligned with [Decision 005](../architecture/decisions/005-fetch-and-tanstack-que --- -## Private workspace - -Mapsight is open source, but some development still happens outside this tree. The monorepo reserves workspace paths for -that (`private/apps/*`, `private/packages/*` in [`pnpm-workspace.yaml`](../../pnpm-workspace.yaml)) without publishing -anything under `private/` here. - -On checkouts that include a private workspace: - -- Never leak customer-specific deployment details into public paths (`apps/`, `packages/`, `docs/`, root config, CI, - hooks, etc.) -- `pnpm run check:no-private-leak` enforces on public branches; `pnpm-lock.yaml` must not list importers under - `private/` on public branches -- On **`private/*` branches**, these checks are skipped -- Merge direction: integrate **open source into private**, not the reverse — see [ - `private/README.md`](../../private/README.md) - when present - -To prevent accidental leaks, this repository enforces checks locally and in CI: - -- Git hooks: [`.husky/pre-commit`](../../.husky/pre-commit), [`.husky/pre-push`](../../.husky/pre-push) -- Script: [`scripts/check-no-private-leak.mts`](../../scripts/check-no-private-leak.mts) — - `pnpm run check:no-private-leak` -- CI: `no-private-leak` job in [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) - -**Please do not open pull requests that remove or weaken these guards.** - ---- - ## Project docs - Architecture, integration, and contributor docs live in `docs/` diff --git a/package.json b/package.json index a5c9d0f0..7f73e9a6 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,7 @@ }, "scripts": { "build": "turbo build", - "check": "run-p check:no-private-leak check:starter-pins", - "check:no-private-leak": "node scripts/check-no-private-leak.mts", + "check": "pnpm run check:starter-pins", "check:starter-pins": "node scripts/check-starter-package-pins.mts", "check:typecheck-test-coverage": "node scripts/check-typecheck-test-coverage.mts", "clean": "turbo clean", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f6e44701..00c79525 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,8 +2,6 @@ minimumReleaseAge: 1440 packages: - "packages/*" - "apps/*" - - "private/apps/*" - - "private/packages/*" - "starters/*" allowBuilds: "@parcel/watcher": true diff --git a/scripts/check-no-private-leak.mts b/scripts/check-no-private-leak.mts deleted file mode 100644 index 7c5eb628..00000000 --- a/scripts/check-no-private-leak.mts +++ /dev/null @@ -1,291 +0,0 @@ -import {execSync} from "node:child_process"; -import {existsSync, readFileSync, readdirSync} from "node:fs"; -import path from "node:path"; - -const PATH_PREFIXES = ["private/"] as const; -const LOCKFILE_IMPORTER_PREFIX = "private/"; -const PRIVATE_BRANCH_PATTERNS = ["private/*"] as const; - -type Mode = "commits" | "push" | "staged" | "workspace"; -type Visibility = "auto" | "private" | "public"; - -const args = process.argv.slice(2); -const options = { - commitRange: "", - mode: "workspace" as Mode, - remote: "", - visibility: "auto" as Visibility, -}; - -for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; - if (arg === "--") { - continue; - } - - switch (arg) { - case "--mode": - options.mode = args[++index] as Mode; - break; - case "--visibility": - options.visibility = args[++index] as Visibility; - break; - case "--commit-range": - options.commitRange = args[++index] ?? ""; - break; - case "--remote": - options.remote = args[++index] ?? ""; - break; - default: - throw new Error(`Unknown argument: ${arg}`); - } -} - -const root = execSync("git rev-parse --show-toplevel", { - encoding: "utf8", -}).trim(); - -const normalizePath = (filePath: string): string => - filePath.replace(/\\/g, "/"); - -const matchesPattern = (value: string, pattern: string): boolean => { - if (pattern.endsWith("/*")) { - return value.startsWith(pattern.slice(0, -1)); - } - - return value === pattern; -}; - -const getCurrentBranch = (): string => { - try { - return execSync("git branch --show-current", { - cwd: root, - encoding: "utf8", - }).trim(); - } catch { - return ""; - } -}; - -const resolveVisibility = (): Visibility => { - if (options.visibility !== "auto") { - return options.visibility; - } - - const branch = getCurrentBranch(); - if ( - branch && - PRIVATE_BRANCH_PATTERNS.some((pattern) => - matchesPattern(branch, pattern), - ) - ) { - return "private"; - } - - return "public"; -}; - -const isForbiddenPath = (filePath: string): boolean => { - const normalized = normalizePath(filePath); - - return PATH_PREFIXES.some( - (prefix) => - normalized === prefix.replace(/\/$/, "") || - normalized.startsWith(prefix), - ); -}; - -const getLockfileViolations = (lockfileContent: string): string[] => { - const prefix = LOCKFILE_IMPORTER_PREFIX.replace(/\/$/, ""); - const pattern = new RegExp( - `^ (${prefix.replaceAll("/", "\\/")}\\/[^:]+):`, - "gm", - ); - const violations = new Set(); - - for (const match of lockfileContent.matchAll(pattern)) { - violations.add(`pnpm-lock.yaml importer "${match[1]}"`); - } - - return [...violations]; -}; - -const getStagedPaths = (): string[] => - execSync("git diff --cached --name-only --diff-filter=ACMR", { - cwd: root, - encoding: "utf8", - }) - .split("\n") - .map(normalizePath) - .filter(Boolean); - -const getCommitPaths = (commitRange: string): string[] => - execSync(`git diff --name-only --diff-filter=ACMR ${commitRange}`, { - cwd: root, - encoding: "utf8", - }) - .split("\n") - .map(normalizePath) - .filter(Boolean); - -const getWorkspacePaths = (): string[] => { - const tracked = execSync("git ls-files", {cwd: root, encoding: "utf8"}) - .split("\n") - .map(normalizePath) - .filter(Boolean); - - const privateDir = path.join(root, "private"); - if (!existsSync(privateDir)) { - return tracked; - } - - const walkPrivate = ( - directory: string, - relativeDirectory: string, - ): string[] => { - const entries = readdirSync(directory, {withFileTypes: true}); - const paths: string[] = []; - - for (const entry of entries) { - const relativePath = relativeDirectory - ? `${relativeDirectory}/${entry.name}` - : entry.name; - const gitPath = `private/${relativePath}`; - - if (entry.isDirectory()) { - paths.push( - ...walkPrivate( - path.join(directory, entry.name), - relativePath, - ), - ); - continue; - } - - if (!tracked.includes(gitPath)) { - paths.push(gitPath); - } - } - - return paths; - }; - - return [...tracked, ...walkPrivate(privateDir, "")]; -}; - -const getPushCommitRanges = (): string[] => { - const stdin = readFileSync(0, "utf8").trim(); - if (!stdin) { - return []; - } - - const ranges: string[] = []; - - for (const line of stdin.split("\n")) { - const [localRef, localSha, , remoteSha] = line.split(/\s+/); - if (!localRef || !localSha) { - continue; - } - - if (localSha === "0000000000000000000000000000000000000000") { - continue; - } - - if (remoteSha === "0000000000000000000000000000000000000000") { - ranges.push(localSha); - continue; - } - - ranges.push(`${remoteSha}..${localSha}`); - } - - return ranges; -}; - -const collectViolations = (paths: string[]): string[] => { - const violations = paths - .filter(isForbiddenPath) - .map((filePath) => `path "${filePath}"`); - - const lockfilePaths = paths.filter( - (filePath) => filePath === "pnpm-lock.yaml", - ); - if (lockfilePaths.length > 0) { - const lockfilePath = path.join(root, "pnpm-lock.yaml"); - if (existsSync(lockfilePath)) { - const lockfileContent = readFileSync(lockfilePath, "utf8"); - violations.push(...getLockfileViolations(lockfileContent)); - } - } - - return violations; -}; - -const run = (): void => { - const visibility = resolveVisibility(); - if (visibility === "private") { - return; - } - - const violations = new Set(); - - if (options.mode === "staged") { - for (const violation of collectViolations(getStagedPaths())) { - violations.add(violation); - } - } - - if (options.mode === "workspace") { - for (const violation of collectViolations(getWorkspacePaths())) { - violations.add(violation); - } - - const lockfilePath = path.join(root, "pnpm-lock.yaml"); - if (existsSync(lockfilePath)) { - for (const violation of getLockfileViolations( - readFileSync(lockfilePath, "utf8"), - )) { - violations.add(violation); - } - } - } - - if (options.mode === "commits") { - if (!options.commitRange) { - throw new Error("--commit-range is required for commits mode"); - } - - for (const violation of collectViolations( - getCommitPaths(options.commitRange), - )) { - violations.add(violation); - } - } - - if (options.mode === "push") { - for (const commitRange of getPushCommitRanges()) { - for (const violation of collectViolations( - getCommitPaths(commitRange), - )) { - violations.add(violation); - } - } - } - - if (violations.size === 0) { - return; - } - - const branch = getCurrentBranch(); - const remoteSuffix = options.remote ? ` (remote: ${options.remote})` : ""; - - throw new Error( - [ - `error: private content must not be published from branch "${branch || "detached"}"${remoteSuffix}.`, - "Blocked:", - ...[...violations].map((violation) => ` - ${violation}`), - ].join("\n"), - ); -}; - -run();