git-env.test.js (4913B)
1 // test/git-env.test.js - reading repositories the conductor does not own 2 // 3 // Project paths belong to whoever put them on disk, and in a container the 4 // conductor is a different user again. Git calls that "dubious ownership" 5 // and refuses. This went unnoticed for a while because the old image ran 6 // as uid 1000, which happened to match the repositories being read, so it 7 // worked by coincidence rather than by design. 8 9 import test from 'node:test'; 10 import assert from 'node:assert/strict'; 11 import fs from 'node:fs/promises'; 12 import { existsSync } from 'node:fs'; 13 import os from 'node:os'; 14 import path from 'node:path'; 15 import { execFile } from 'node:child_process'; 16 import { promisify } from 'node:util'; 17 18 import { gitEnv } from '../src/lib/git.js'; 19 20 const execFileAsync = promisify(execFile); 21 22 test('git jobs with safe.directory set', () => { 23 const env = gitEnv({}); 24 assert.equal(env.GIT_CONFIG_COUNT, '1'); 25 assert.equal(env.GIT_CONFIG_KEY_0, 'safe.directory'); 26 assert.equal(env.GIT_CONFIG_VALUE_0, '*'); 27 }); 28 29 test('it appends to configuration already in the environment', () => { 30 const env = gitEnv({ 31 GIT_CONFIG_COUNT: '2', 32 GIT_CONFIG_KEY_0: 'http.version', 33 GIT_CONFIG_VALUE_0: 'HTTP/1.1', 34 GIT_CONFIG_KEY_1: 'core.quotePath', 35 GIT_CONFIG_VALUE_1: 'false', 36 }); 37 38 // Git reads indices 0..count-1, so ours has to land at the end and the 39 // count has to grow rather than be replaced. 40 assert.equal(env.GIT_CONFIG_COUNT, '3'); 41 assert.equal(env.GIT_CONFIG_KEY_0, 'http.version'); 42 assert.equal(env.GIT_CONFIG_KEY_1, 'core.quotePath'); 43 assert.equal(env.GIT_CONFIG_KEY_2, 'safe.directory'); 44 assert.equal(env.GIT_CONFIG_VALUE_2, '*'); 45 }); 46 47 test('a count that is not a number does not corrupt the sequence', () => { 48 for (const bad of ['', 'garbage', '-1', '0']) { 49 const env = gitEnv({ GIT_CONFIG_COUNT: bad }); 50 assert.equal(env.GIT_CONFIG_COUNT, '1', `count ${JSON.stringify(bad)}`); 51 assert.equal(env.GIT_CONFIG_KEY_0, 'safe.directory'); 52 } 53 }); 54 55 test('git accepts the environment we build', async () => { 56 // Proves the variables are actually well formed. A malformed set makes 57 // git fail outright, which is what this would catch. 58 const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'conductor-gitenv-')); 59 try { 60 const env = gitEnv(process.env); 61 await execFileAsync('git', ['init', '-q', '-b', 'main', dir], { env }); 62 const { stdout } = await execFileAsync( 63 'git', ['-C', dir, 'config', '--get', 'safe.directory'], { env }, 64 ); 65 assert.equal(stdout.trim(), '*'); 66 } finally { 67 await fs.rm(dir, { recursive: true, force: true }); 68 } 69 }); 70 71 test('a file missing from a commit reads as missing, whatever sits in the working directory', async () => { 72 // Given --git-dir, git treats the process working directory as a work 73 // tree and changes its wording when a file of that name happens to be 74 // there: "exists on disk, but not in <sha>" rather than a plain 75 // absence. The conductor decides a project has no pipeline from that 76 // message, so the wrong wording turned a missing pipeline into a 500. 77 // Reproduced here rather than left to the happy accident that the test 78 // suite jobs from a directory containing a .conductor.yml. 79 const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'conductor-cwd-')); 80 const work = path.join(dir, 'work'); 81 const mirror = path.join(dir, 'mirror.git'); 82 const env = gitEnv(process.env); 83 84 try { 85 await fs.mkdir(work); 86 await execFileAsync('git', ['init', '-q', '-b', 'main', work], { env }); 87 await execFileAsync('git', ['-C', work, 'config', 'user.email', 't@example.invalid'], { env }); 88 await execFileAsync('git', ['-C', work, 'config', 'user.name', 'test'], { env }); 89 await fs.writeFile(path.join(work, 'other.txt'), 'present\n'); 90 await execFileAsync('git', ['-C', work, 'add', '-A'], { env }); 91 await execFileAsync('git', ['-C', work, 'commit', '-q', '-m', 'only other.txt'], { env }); 92 await execFileAsync('git', ['clone', '-q', '--mirror', work, mirror], { env }); 93 94 const { stdout } = await execFileAsync('git', ['--git-dir', mirror, 'rev-parse', 'HEAD'], { env }); 95 const sha = stdout.trim(); 96 97 // A name that is absent from the commit but present in the directory 98 // the suite jobs from. 99 const decoy = 'package.json'; 100 assert.ok(existsSync(path.join(process.cwd(), decoy)), `expected ${decoy} in the working directory`); 101 102 let stderr = ''; 103 try { 104 await execFileAsync( 105 'git', ['--git-dir', mirror, 'cat-file', 'blob', `${sha}:${decoy}`], 106 { env, cwd: mirror }, 107 ); 108 assert.fail('expected git to report the path as absent'); 109 } catch (error) { 110 stderr = error.stderr ?? ''; 111 } 112 113 // Job inside the bare mirror there is no work tree to confuse it. 114 assert.match(stderr, /does not exist/); 115 } finally { 116 await fs.rm(dir, { recursive: true, force: true }); 117 } 118 });