conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 });