Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions packages/spec/scripts/build-schemas-check-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { gitFreeEnv } from '../../../scripts/git-env.mjs';
import { schemaStamp } from '../../../scripts/check-regen-pending.mjs';
import { RENAMED_DEFS } from './lib/renamed-defs';
import { CONVERSIONS_BY_MAJOR } from '../src/conversions/registry';
Expand Down Expand Up @@ -232,25 +233,23 @@ let surfaceBaseDescription: string;
// are already serial — vitest runs a file's tests one at a time, and every repo
// here is an `fs.mkdtemp`, so no concurrently running test FILE can name one.

/** `GIT_*` variables that would point a fixture's git at a different repository
* (or a different index/object store) than the directory it was handed. */
const LEAKED_GIT_ENV = [
'GIT_DIR',
'GIT_WORK_TREE',
'GIT_COMMON_DIR',
'GIT_INDEX_FILE',
'GIT_OBJECT_DIRECTORY',
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_NAMESPACE',
'GIT_CEILING_DIRECTORIES',
'GIT_TEMPLATE_DIR',
'GIT_CONFIG',
] as const;
/* #16644 -- a hand-maintained allowlist of ten GIT_* location variables used to stand
* here. It is retired in favour of the blanket strip in `scripts/git-env.mjs`, and the
* constant is not re-spelled anywhere in this file so that a census of the retired shape
* does not match this paragraph.
*
* The reason the allowlist goes rather than gets one more entry: it had to be kept level
* with git's own list of location variables, and its failure mode is that THE KEY IT
* MISSES IS THE KEY THAT BITES. `gitFreeEnv()` removes every GIT_-prefixed key instead,
* which is the shape #16624 landed and #16753 applied. ⛔ One spelling in the repo, not
* two -- no "either is fine" transition state.
*
* Every git this file spawns is LOCAL-ONLY: it operates on the `fs.mkdtemp` fixture named
* by its `cwd`, so the blanket strip takes no transport configuration away from it. */

/** The environment every fixture git — and every generator run inside one — gets. */
const HERMETIC_ENV: NodeJS.ProcessEnv = (() => {
const env = { ...process.env };
for (const key of LEAKED_GIT_ENV) delete env[key];
const env = gitFreeEnv();
// git's own documented "read no config file" spellings. `/dev/null` parses as
// an empty config, which is what makes `init.templateDir`, `core.hooksPath`
// and any ambient `[gc]` block unable to reach a fixture.
Expand Down
32 changes: 16 additions & 16 deletions packages/spec/scripts/sharded-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { gitFreeEnv } from '../../../scripts/git-env.mjs';

import {
API_SURFACE_DIR_NAME,
AUTHORABLE_SURFACE_DIR_NAME,
Expand Down Expand Up @@ -411,25 +413,23 @@ describe('sharded artifacts — entry-point shard naming (#5837)', () => {
// Nothing about what these cases assert changes: the fixtures are built from the
// same writes and read by the same `readShardedKeysAtRev`, only insulated.

/** `GIT_*` variables that would point a fixture's git at a different repository
* (or a different index/object store) than the directory it was handed. */
const LEAKED_GIT_ENV = [
'GIT_DIR',
'GIT_WORK_TREE',
'GIT_COMMON_DIR',
'GIT_INDEX_FILE',
'GIT_OBJECT_DIRECTORY',
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_NAMESPACE',
'GIT_CEILING_DIRECTORIES',
'GIT_TEMPLATE_DIR',
'GIT_CONFIG',
] as const;
/* #16644 -- a hand-maintained allowlist of ten GIT_* location variables used to stand
* here. It is retired in favour of the blanket strip in `scripts/git-env.mjs`, and the
* constant is not re-spelled anywhere in this file so that a census of the retired shape
* does not match this paragraph.
*
* The reason the allowlist goes rather than gets one more entry: it had to be kept level
* with git's own list of location variables, and its failure mode is that THE KEY IT
* MISSES IS THE KEY THAT BITES. `gitFreeEnv()` removes every GIT_-prefixed key instead,
* which is the shape #16624 landed and #16753 applied. ⛔ One spelling in the repo, not
* two -- no "either is fine" transition state.
*
* Every git this file spawns is LOCAL-ONLY: it operates on the `fs.mkdtemp` fixture named
* by its `cwd`, so the blanket strip takes no transport configuration away from it. */

/** The environment every fixture git in this file gets. */
const HERMETIC_ENV: NodeJS.ProcessEnv = (() => {
const env = { ...process.env };
for (const key of LEAKED_GIT_ENV) delete env[key];
const env = gitFreeEnv();
// git's own documented "read no config file" spellings. `/dev/null` parses as
// an empty config, which is what makes `init.templateDir`, `core.hooksPath`
// and any ambient `[gc]` block unable to reach a fixture.
Expand Down
1 change: 1 addition & 0 deletions packages/spec/vitest.repo-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"scripts/references-banner.test.ts",
"scripts/root-index.test.ts",
"scripts/schema-tree-freshness.test.ts",
"scripts/sharded-artifacts.test.ts",
"scripts/solution-blueprint-header-row.test.ts",
"scripts/strictness-ledger-doc.test.ts",
"scripts/strictness-ledger.test.ts",
Expand Down
12 changes: 11 additions & 1 deletion scripts/ablation-dist-preflight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import process from 'node:process';
import { gitFreeEnv } from './git-env.mjs';
import { isEntrypoint } from './invoked-as.mjs';
import { WORKSPACE_FILE, parseWorkspaceGlobs, workspacePackageDirs } from './workspace-enumerator.mjs';

Expand Down Expand Up @@ -450,6 +451,11 @@ function readTreeStatus(repoRoot) {
try {
const out = execFileSync('git', ['status', '--porcelain', '-z'], {
cwd: repoRoot,
// The tree under test is the one `repoRoot` names and nothing else (#16644).
// An inherited GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE outranks `cwd`, so under a
// hook this would certify SOME OTHER tree as restored -- the one direction this
// preflight exists to make impossible.
env: gitFreeEnv(),
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
Expand All @@ -475,6 +481,8 @@ function markerPresence(repoRoot, entries, marker) {
try {
head = execFileSync('git', ['show', `HEAD:${e.path}`], {
cwd: repoRoot,
env: gitFreeEnv(), // #16644: the HEAD blob of THIS tree, never a hook's

maxBuffer: 64 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'ignore'],
});
Expand Down Expand Up @@ -714,7 +722,9 @@ function selfTest() {
// verdict mean anything. This leg replays the measured incident end to end.
const repo = mkdtempSync(join(tmpdir(), 'ablation-preflight-git-'));
try {
const git = (...args) => execFileSync('git', args, { cwd: repo, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
// #16644: a throwaway corpus, so every child is spawned with GIT_* stripped --
// `git init` here under an inherited GIT_DIR writes core.bare into the SHARED config.
const git = (...args) => execFileSync('git', args, { cwd: repo, env: gitFreeEnv(), stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
const srcPath = join(repo, 'source.ts');
const genPath = join(repo, 'generated-baseline.json');
const MARK = 'OS_ABLATION_LEAK_MARK';
Expand Down
10 changes: 8 additions & 2 deletions scripts/check-adr-0087-registration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'nod
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gitFreeEnv } from './git-env.mjs';
import { isEntrypoint } from './invoked-as.mjs';
import { maskComments, maskCommentsAndLiterals } from './js-comment-mask.mjs';
// #16421 — the DIRECTION ARM, read through the fleet's one declaration reader.
Expand Down Expand Up @@ -2002,11 +2003,16 @@ export function projectedMigrationIds(specChangesJson) {
// ---------------------------------------------------------------------------

function git(args, cwd) {
// `env: gitFreeEnv()` (#16644): every caller of this helper -- the real checkout on a
// gate run, a mkdtemp fixture in the self-test -- names its repository by `cwd`. An
// inherited GIT_DIR outranks `cwd`, so without the strip the fixture legs read and
// write THE REAL REPOSITORY under a hook, silently and with `ok` printed throughout.
//
// stderr is PIPED, not inherited: `showOrNull` probes paths that legitimately do
// not exist at a rev (a ledger file added mid-history, a changeset deleted), and
// git's "fatal: path ... does not exist" would otherwise print as though the gate
// had failed while it is in fact answering the question it asked.
return execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
return execFileSync('git', args, { cwd, env: gitFreeEnv(), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
}

/** File contents at a rev, or `null` when the path does not exist there. */
Expand Down Expand Up @@ -2035,7 +2041,7 @@ function showManyOrNull(rev, paths, cwd) {
let out;
try {
out = execFileSync('git', ['cat-file', '--batch'], {
cwd, input, maxBuffer: 512 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'],
cwd, env: gitFreeEnv(), input, maxBuffer: 512 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'],
});
} catch { return found; }

Expand Down
15 changes: 12 additions & 3 deletions scripts/check-bash32-floor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import process from 'node:process';

import { gitFreeEnv } from './git-env.mjs';
import { isEntrypoint } from './invoked-as.mjs';

const REPO_ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
Expand Down Expand Up @@ -847,6 +848,11 @@ function unreadableReason(err) {
*/
export function listPopulation(root) {
const out = spawnSync('git', ['-C', root, 'ls-files', '-z', '--', ...WALK_ROOTS], {
// #16644: `-C root` is the ONLY thing that may decide which index is read. An
// inherited GIT_DIR outranks it, and this function is called with a mkdtemp fixture
// as `root` in every end-to-end leg below -- under a hook those legs would census
// the real repository and report a number about the wrong tree.
env: gitFreeEnv(),
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
});
Expand Down Expand Up @@ -953,12 +959,15 @@ function reportUnreadable(unreadable, population, findings) {
*/
function fixtureRepo(files) {
const dir = mkdtempSync(join(tmpdir(), 'bash32-floor-'));
spawnSync('git', ['-C', dir, 'init', '-q'], { encoding: 'utf8' });
// #16644: `init` and `add -A` are the two commands the measured incident ran. With an
// inherited GIT_DIR the `init` writes core.bare into the SHARED .git/config and the
// `add -A` stages the real tree as deleted, both silently.
spawnSync('git', ['-C', dir, 'init', '-q'], { encoding: 'utf8', env: gitFreeEnv() });
for (const [rel, body] of Object.entries(files)) {
mkdirSync(join(dir, dirname(rel)), { recursive: true });
writeFileSync(join(dir, rel), body);
}
spawnSync('git', ['-C', dir, 'add', '-A'], { encoding: 'utf8' });
spawnSync('git', ['-C', dir, 'add', '-A'], { encoding: 'utf8', env: gitFreeEnv() });
return dir;
}

Expand Down Expand Up @@ -1402,7 +1411,7 @@ function selfTest() {
// a fixture whose index also lost the path would make every case below pass
// by testing nothing.
rmSync(join(partialRepo, 'scripts/absent.sh'));
const stillIndexed = spawnSync('git', ['-C', partialRepo, 'ls-files', '--', ...WALK_ROOTS], { encoding: 'utf8' });
const stillIndexed = spawnSync('git', ['-C', partialRepo, 'ls-files', '--', ...WALK_ROOTS], { encoding: 'utf8', env: gitFreeEnv() });
t(
'the fixture really is INDEX-vs-DISK: the index still lists the removed path',
stillIndexed.stdout.includes('scripts/absent.sh'),
Expand Down
8 changes: 8 additions & 0 deletions scripts/check-changeset-no-major.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { gitFreeEnv } from './git-env.mjs';
import { isEntrypoint } from './invoked-as.mjs';
// #16055, the level axis below. Both are IMPORTED rather than restated: the
// clause-② declaration has exactly one legal spelling and exactly one label
Expand Down Expand Up @@ -469,6 +470,13 @@ const isChangesetFile = (p) => p.startsWith('.changeset/') && p.endsWith('.md')
function git(args, cwd, { quiet = false } = {}) {
return execFileSync('git', args, {
cwd,
// #16644: `cwd` is the only thing that may name the repository here, and the
// self-test hands it mkdtemp fixtures. GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE
// outrank `cwd`, so an inherited one redirects `init`, `add -A` and `commit`
// onto the real checkout. ⭐ This helper is also the one that runs `fetch` in the
// #4690 leg -- its remote there is another LOCAL mkdtemp repository passed by
// path, so no transport configuration is in play and the strip is safe.
env: gitFreeEnv(),
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
// `execFileSync` inherits the child's stderr by default. That is right for
Expand Down
8 changes: 7 additions & 1 deletion scripts/check-empty-changeset.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { gitFreeEnv } from './git-env.mjs';
import { isEntrypoint } from './invoked-as.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -294,8 +295,13 @@ const isChangesetFile = (p) => p.startsWith('.changeset/') && p.endsWith('.md')

// ── git helpers ──────────────────────────────────────────────────────────────

// #16644: `cwd` is the only thing that may name the repository, and the self-test
// hands this helper mkdtemp fixtures -- `init`, `add -A`, `commit`, `fetch` from a
// sibling temp repo. An inherited GIT_DIR outranks `cwd` and redirects all of them
// onto the real checkout. The `fetch` legs name their remote by local PATH, so the
// strip costs them no transport configuration.
function git(args, cwd) {
return execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
return execFileSync('git', args, { cwd, env: gitFreeEnv(), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
}

/** File contents at a rev, or `null` when the path does not exist there. */
Expand Down
29 changes: 25 additions & 4 deletions scripts/check-engine-split-ratio.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { gitFreeEnv, withoutGitEnv } from './git-env.mjs';
import { historyHorizon } from './pm/git-history.mjs';
import { isEntrypoint } from './invoked-as.mjs';

Expand Down Expand Up @@ -166,14 +167,22 @@ function main(argv) {
return EXIT_CANNOT_COMPUTE;
}

// LOCAL-ONLY (#16644). Every `git` below is `log` / `rev-list` against the checkout
// `--cwd` names -- the self-test drives this same entry point with a mkdtemp fixture as
// `--cwd`, so an inherited GIT_DIR would have it measure the real repository and report
// a ratio about the wrong tree. No child here reaches a remote.
const git = (...args) =>
execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
execFileSync('git', args, { cwd: repoRoot, env: gitFreeEnv(), encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });

// ── the horizon comes FIRST: nothing below may run on a window this
// checkout cannot see all of.
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
const sinceIso = new Date(sinceMs).toISOString();
const horizon = historyHorizon({ cwd: repoRoot, ref: 'HEAD', sinceMs });
// `withoutGitEnv`, not `gitFreeEnv`: the git child is spawned one frame down inside
// `scripts/pm/git-history.mjs`, which passes no environment of its own, so the only way
// to reach it is to detach the PROCESS for the call (#16644, the second half of the
// rule in scripts/git-env.mjs). Restored in a `finally` by that helper.
const horizon = withoutGitEnv(() => historyHorizon({ cwd: repoRoot, ref: 'HEAD', sinceMs }));
if (!horizon.covered) {
console.error(renderRefusal({ horizon, days, sinceIso }));
return EXIT_CANNOT_COMPUTE;
Expand Down Expand Up @@ -298,14 +307,22 @@ function selfTest() {
// ── real repos: the defect, then both legs of the guard ───────────────────
battery('real repos: the defect, then both legs of the guard');
const root = mkdtempSync(join(tmpdir(), 'engine-split-selftest-'));
const g = (args, cwd) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
// LOCAL-ONLY (#16644): `init`, `config`, `add`, `log`, and two `clone`s whose source is
// a `file://` URL under this battery's own mkdtemp root -- a local object transfer that
// needs none of the GIT_CONFIG_* / GIT_SSL_* transport configuration this container
// carries, so the blanket strip costs them nothing and keeps `init` off the shared repo.
const g = (args, cwd) => execFileSync('git', args, { cwd, env: gitFreeEnv(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
const self = fileURLToPath(import.meta.url);
// spawnSync, not execFileSync: this script writes to stderr on SUCCESS too
// (the zero-scan warning), and execFileSync surfaces stderr only when it
// throws -- a self-test that reads stderr only on failure is half blind.
const runAllowFail = (args, cwd) => {
const r = spawnSync(process.execPath, [self, ...args, '--cwd', cwd], {
cwd,
// #16644: the child re-enters this file against a FIXTURE. Its own helpers strip,
// but the strip is applied here too so nothing the child spawns -- including the
// shared `historyHorizon` -- can be redirected by a variable this process inherited.
env: gitFreeEnv(),
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
Expand All @@ -330,7 +347,11 @@ function selfTest() {
execFileSync('git', ['commit', '--quiet', '-m', `c${i}`], {
cwd: up,
encoding: 'utf8',
env: { ...process.env, GIT_AUTHOR_DATE: d, GIT_COMMITTER_DATE: d },
// ⭐ NOT a blanket replace: the two date variables are set DELIBERATELY and are
// what dates the fixture, so they are re-applied ON TOP of the strip. Spreading
// `process.env` here instead would carry an inherited GIT_DIR straight into a
// `commit` -- the one command in this battery that writes refs (#16644).
env: { ...gitFreeEnv(), GIT_AUTHOR_DATE: d, GIT_COMMITTER_DATE: d },
});
}
// `--days` is relative to now, so re-date the whole fixture to end today.
Expand Down
Loading
Loading