diff --git a/.github/workflows/test-blacksmith.yml b/.github/workflows/test-blacksmith.yml index d6390e8..bb00214 100644 --- a/.github/workflows/test-blacksmith.yml +++ b/.github/workflows/test-blacksmith.yml @@ -114,3 +114,99 @@ jobs: git fsck --no-dangling echo "Reused deep checkout verified" + + # The shape that bit real container jobs: a privileged job container with the + # runner's devices passed through, checking out as root into a directory + # owned by the runner user (the workspace is bind-mounted from the host), + # then dissociating from the mirror. Without the checkout's git environment + # the dissociate repack fails with "dubious ownership". The target is a + # subdirectory given the workspace's owner rather than the workspace root, + # so the checkout does not wipe the action code out from under the job. + # + # The mirror on the sticky disk was committed by VM jobs, so it arrives owned + # by the runner user and the root checkout has to take it over. (Every + # checkout step mounts its own copy of the last commit, so ownership cannot be + # flipped between two steps of one job; the takeover is only observable + # across jobs, and in the VM direction only after a container job committed.) + test-git-mirror-container: + runs-on: blacksmith + container: + image: ubuntu:24.04 + options: --privileged -v /dev:/dev + steps: + - name: Install git, sudo and the sticky disk tooling + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + bash git sudo util-linux e2fsprogs mount ca-certificates + + - name: Checkout action repo + uses: actions/checkout@v4 + with: + path: action + + - name: Create a checkout directory owned by the runner user + shell: bash + run: | + set -euo pipefail + test "$(id -u)" = 0 + mkdir host-owned && chown --reference=. host-owned + test "$(stat -c %u host-owned)" != 0 + echo "workspace owned by uid $(stat -c %u .), git runs as uid $(id -u)" + + - name: Test checkout with git mirror inside the container (dissociate) + uses: ./action + with: + path: host-owned + allow-inside-container: true + dissociate: true + + - name: Verify the mirror was mounted and the workspace dissociated + shell: bash + run: | + set -euo pipefail + test "$(id -u)" = 0 + mirror="/blacksmith-git-mirror/${GITHUB_REPOSITORY}/v1/${GITHUB_REPOSITORY_OWNER}-${GITHUB_REPOSITORY#*/}.git" + if ! mountpoint -q "$(dirname "$(dirname "$mirror")")"; then + echo "Sticky disk is not mounted inside the container; the mirror path was not exercised" + exit 1 + fi + test -d "$mirror" + if [ "$(stat -c %u "$mirror")" != 0 ]; then + echo "mirror still owned by uid $(stat -c %u "$mirror"); the root checkout did not take it over" + exit 1 + fi + cd host-owned + if [ -e .git/objects/info/alternates ]; then + echo "alternates file survived dissociate" + exit 1 + fi + git config --global --add safe.directory "$PWD" + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + git fsck --no-dangling + echo "Container checkout with dissociate verified" + + # The other direction: a VM job finding the mirror as a root container job + # left it. Real once a container job on a trusted trigger (push to main) has + # committed a root-owned mirror; the action logs the owner it took over from. + test-git-mirror-after-container: + runs-on: blacksmith + needs: test-git-mirror-container + steps: + - name: Checkout action repo + uses: actions/checkout@v4 + + - name: Test checkout against the mirror a container job left behind + uses: ./ + with: + path: after-container + + - name: Verify the mirror is owned by the runner user and was used + run: | + set -euo pipefail + mirror="/blacksmith-git-mirror/${GITHUB_REPOSITORY}/v1/${GITHUB_REPOSITORY_OWNER}-${GITHUB_REPOSITORY#*/}.git" + test -d "$mirror" + test "$(stat -c %u "$mirror")" = "$(id -u)" + grep -qF "$mirror/objects" after-container/.git/objects/info/alternates + git -C after-container fsck --no-dangling + echo "Checkout after a container job verified (mirror owned by uid $(id -u))" diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index f7edea9..68c3021 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -1040,6 +1040,7 @@ async function setup(testName: string): Promise { env: {}, fetch: jest.fn(), getDefaultBranch: jest.fn(), + getEnvironment: jest.fn(() => ({...git.env})), getSubmoduleConfigPaths: jest.fn(async () => []), getWorkingDirectory: jest.fn(() => workspace), init: jest.fn(), diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts index de79dc8..2cadd61 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -471,6 +471,7 @@ async function setup(testName: string): Promise { configExists: jest.fn(), fetch: jest.fn(), getDefaultBranch: jest.fn(), + getEnvironment: jest.fn(() => ({})), getSubmoduleConfigPaths: jest.fn(async () => []), getWorkingDirectory: jest.fn(() => repositoryPath), init: jest.fn(), diff --git a/__test__/mirror-workspace-env-git.test.ts b/__test__/mirror-workspace-env-git.test.ts new file mode 100644 index 0000000..373ac75 --- /dev/null +++ b/__test__/mirror-workspace-env-git.test.ts @@ -0,0 +1,169 @@ +/** + * Real-git coverage for the workspace-side mirror operations (ref copy, + * dissociate) on a workspace git considers owned by another user, as in a + * job container where the runner creates the workspace and git runs as + * root. Git's own GIT_TEST_ASSUME_DIFFERENT_OWNER knob stands in for the + * uid mismatch; the checkout's safe.directory entry lives in a temporary + * HOME, so the operations only succeed when run with that environment. + */ +jest.mock('@connectrpc/connect', () => ({ + createClient: jest.fn(), + ConnectError: class ConnectError extends Error {}, + Code: {Aborted: 'ABORTED'} +})) + +jest.mock('@connectrpc/connect-node', () => ({ + createGrpcTransport: jest.fn() +})) + +jest.mock( + '@buf/blacksmith_vm-agent.connectrpc_es/stickydisk/v1/stickydisk_connect', + () => ({ + StickyDiskService: {} + }) +) + +jest.mock('../src/container-detector', () => ({ + isRunningInContainer: jest.fn(() => false) +})) + +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' +import {execFileSync} from 'child_process' +import * as blacksmithCache from '../src/blacksmith-cache' + +function git(cwd: string, ...args: string[]): string { + return execFileSync('git', ['-C', cwd, ...args], {encoding: 'utf8'}).trim() +} + +function commit(repo: string, msg: string): void { + fs.writeFileSync(path.join(repo, 'file.txt'), msg) + git(repo, 'add', 'file.txt') + git( + repo, + '-c', + 'user.email=test@example.com', + '-c', + 'user.name=Test', + 'commit', + '-m', + msg + ) +} + +function baseEnv(): {[key: string]: string} { + const env: {[key: string]: string} = {} + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + env[key] = value + } + } + env['GIT_TEST_ASSUME_DIFFERENT_OWNER'] = '1' + return env +} + +function gitRefusesForeignOwner(): boolean { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'owner-probe-')) + try { + execFileSync('git', ['init', '-q', tmp]) + execFileSync('git', ['-C', tmp, 'rev-parse', '--git-dir'], { + env: baseEnv(), + stdio: 'ignore' + }) + return false + } catch { + return true + } finally { + fs.rmSync(tmp, {recursive: true, force: true}) + } +} + +const describeIfSupported = gitRefusesForeignOwner() ? describe : describe.skip + +describeIfSupported( + 'workspace mirror operations on a foreign-owned workspace', + () => { + let tmpDir: string + let mirrorPath: string + let workspace: string + let checkoutEnv: {[key: string]: string} + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mirror-env-test-')) + const sourceRepo = path.join(tmpDir, 'source') + mirrorPath = path.join(tmpDir, 'mirror') + workspace = path.join(tmpDir, 'workspace') + + fs.mkdirSync(sourceRepo) + git(sourceRepo, 'init', '-q', '-b', 'main', '.') + commit(sourceRepo, 'one') + commit(sourceRepo, 'two') + git(sourceRepo, 'tag', 'v1') + execFileSync('git', ['clone', '-q', '--mirror', sourceRepo, mirrorPath]) + + fs.mkdirSync(workspace) + git(workspace, 'init', '-q', '.') + git(workspace, 'remote', 'add', 'origin', sourceRepo) + const infoDir = path.join(workspace, '.git', 'objects', 'info') + fs.mkdirSync(infoDir, {recursive: true}) + fs.writeFileSync( + path.join(infoDir, 'alternates'), + `${mirrorPath}/objects\n` + ) + + // What the action's temporary global config holds after + // `git config --global --add safe.directory ` + const tempHome = path.join(tmpDir, 'home') + fs.mkdirSync(tempHome) + fs.writeFileSync( + path.join(tempHome, '.gitconfig'), + `[safe]\n\tdirectory = ${workspace}\n` + ) + checkoutEnv = {...baseEnv(), HOME: tempHome} + }) + + afterEach(() => { + fs.rmSync(tmpDir, {recursive: true, force: true}) + }) + + it('dissociate fails without the checkout environment', async () => { + await expect( + blacksmithCache.dissociate(workspace, baseEnv()) + ).rejects.toThrow(/exit code 128/) + expect( + fs.existsSync( + path.join(workspace, '.git', 'objects', 'info', 'alternates') + ) + ).toBe(true) + }) + + it('ref copy and dissociate succeed with the checkout environment and leave a self-contained workspace', async () => { + expect( + await blacksmithCache.fetchRefsFromMirror( + workspace, + mirrorPath, + checkoutEnv + ) + ).toBe(true) + // The fast path wrote packed-refs; the ref listing is what verifies it + expect(git(workspace, 'rev-parse', 'refs/remotes/origin/main')).toBe( + git(mirrorPath, 'rev-parse', 'refs/heads/main') + ) + + await blacksmithCache.dissociate(workspace, checkoutEnv) + + expect( + fs.existsSync( + path.join(workspace, '.git', 'objects', 'info', 'alternates') + ) + ).toBe(false) + fs.rmSync(mirrorPath, {recursive: true, force: true}) + git(workspace, 'fsck', '--no-dangling') + git(workspace, 'rev-list', '--objects', '--all', '--quiet') + expect(git(workspace, 'rev-parse', 'refs/tags/v1^{commit}')).toBe( + git(workspace, 'rev-parse', 'refs/remotes/origin/main') + ) + }) + } +) diff --git a/dist/index.js b/dist/index.js index 2b950bf..b9f0dda 100644 --- a/dist/index.js +++ b/dist/index.js @@ -373,6 +373,35 @@ function getAuthConfigArgs(repoUrl, authToken) { configValue: `AUTHORIZATION: basic ${basicCredential}` }; } +/** + * Jobs sharing a mirror do not all run as the same user: a VM job runs git + * as the runner user, a job container typically as root. Git refuses a + * repository owned by someone else ("dubious ownership"), and the runner + * user cannot write into a root-owned one, so a mirror hydrated by the + * other kind of job is taken over before it is read or synced. + */ +function adoptMirrorOwnership(mirrorPath) { + return __awaiter(this, void 0, void 0, function* () { + var _a, _b; + const uid = (_a = process.getuid) === null || _a === void 0 ? void 0 : _a.call(process); + const gid = (_b = process.getgid) === null || _b === void 0 ? void 0 : _b.call(process); + if (uid === undefined || gid === undefined) { + return; + } + const owner = (yield fs.promises.stat(mirrorPath)).uid; + if (owner === uid) { + return; + } + const start = Date.now(); + yield exec.exec('sudo', [ + 'chown', + '-R', + `${uid}:${gid}`, + path.dirname(mirrorPath) + ]); + core.info(`[git-mirror] Took over mirror owned by uid ${owner} in ${Date.now() - start}ms`); + }); +} /** * Build git environment with optional verbose flags */ @@ -456,6 +485,7 @@ function ensureMirror(mirrorPath_1, repoUrl_1, authToken_1) { // Mirror exists - the caller synchronizes it with the remote via // syncMirrorFromRemote() before populating the workspace from it core.info(`[git-mirror] Found existing mirror at ${mirrorPath}`); + yield adoptMirrorOwnership(mirrorPath); return false; // Not initial hydration } // First time - create a bare mirror clone (initial hydration) @@ -1036,9 +1066,14 @@ function buildPackedRefsContent(mirrorRefs) { * checkout in a reused workspace, so scripts never read fetch output that * predates this ref state. * + * `env` is the checkout's git environment (see + * IGitCommandManager.getEnvironment); the workspace directory may be owned + * by another user than the one running git (job containers), and only its + * temporary global config marks the workspace as a safe.directory. + * * @returns true on success, false if the caller should fall back */ -function copyRefsFromMirror(workspacePath, mirrorPath) { +function copyRefsFromMirror(workspacePath, mirrorPath, env) { return __awaiter(this, void 0, void 0, function* () { try { const start = Date.now(); @@ -1055,7 +1090,7 @@ function copyRefsFromMirror(workspacePath, mirrorPath) { // default loose format the way this path assumes. A reftable // repository ignores a packed-refs file entirely, so writing one would // silently produce a repo with no refs. - const refStorage = yield exec.getExecOutput('git', ['-C', workspacePath, 'config', '--get', 'extensions.refstorage'], { silent: true, ignoreReturnCode: true }); + const refStorage = yield exec.getExecOutput('git', ['-C', workspacePath, 'config', '--get', 'extensions.refstorage'], { silent: true, ignoreReturnCode: true, env }); const refBackend = refStorage.stdout.trim(); if (refBackend !== '' && refBackend !== 'files') { core.info(`[git-mirror] Workspace uses ref backend '${refBackend}', using local fetch instead of direct ref copy`); @@ -1076,7 +1111,7 @@ function copyRefsFromMirror(workspacePath, mirrorPath) { '--format=%(objectname) %(refname) %(symref)', 'refs/remotes/origin', 'refs/tags' - ], { silent: true }); + ], { silent: true, env }); const packedRefsPath = path.join(gitDir, 'packed-refs'); const freshWorkspace = workspaceRefs.stdout.trim() === '' && !fs.existsSync(packedRefsPath); if (freshWorkspace) { @@ -1095,10 +1130,12 @@ function copyRefsFromMirror(workspacePath, mirrorPath) { if (instructions.length > 0) { yield exec.exec('git', ['-C', workspacePath, 'update-ref', '--stdin'], { silent: true, - input: Buffer.from(`${instructions.join('\n')}\n`) + input: Buffer.from(`${instructions.join('\n')}\n`), + env }); yield exec.exec('git', ['-C', workspacePath, 'pack-refs', '--all'], { - silent: true + silent: true, + env }); } core.info(`[git-mirror] Reconciled ${instructions.length} refs from mirror in ${Date.now() - start}ms`); @@ -1228,10 +1265,10 @@ function removeStaleFetchHead(gitDir) { * @returns true on success, false if the local fetch failed and the caller * should fall back to a network fetch */ -function fetchRefsFromMirror(workspacePath, mirrorPath) { +function fetchRefsFromMirror(workspacePath, mirrorPath, env) { return __awaiter(this, void 0, void 0, function* () { core.info(`[git-mirror] Fetching refs locally from mirror at ${mirrorPath}`); - if (yield copyRefsFromMirror(workspacePath, mirrorPath)) { + if (yield copyRefsFromMirror(workspacePath, mirrorPath, env)) { return true; } try { @@ -1247,7 +1284,7 @@ function fetchRefsFromMirror(workspacePath, mirrorPath) { mirrorPath, '+refs/heads/*:refs/remotes/origin/*', '+refs/tags/*:refs/tags/*' - ]); + ], { env }); return true; } catch (error) { @@ -1274,11 +1311,11 @@ function writeAlternates(workspacePath, mirrorPath) { * Dissociate the repository from the mirror by copying all objects locally * This is needed for Docker-based actions that may not have access to the mirror mount */ -function dissociate(workspacePath) { +function dissociate(workspacePath, env) { return __awaiter(this, void 0, void 0, function* () { core.info('Dissociating repository from mirror'); // Copy all objects from alternates into local repo - yield exec.exec('git', ['-C', workspacePath, 'repack', '-a', '-d']); + yield exec.exec('git', ['-C', workspacePath, 'repack', '-a', '-d'], { env }); // Remove alternates file const alternatesFile = path.join(workspacePath, '.git', 'objects', 'info', 'alternates'); try { @@ -2604,6 +2641,18 @@ class GitCommandManager { throw new Error('Unexpected output when retrieving default branch'); }); } + getEnvironment() { + const env = {}; + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + env[key] = value; + } + } + for (const key of Object.keys(this.gitEnv)) { + env[key] = this.gitEnv[key]; + } + return env; + } getSubmoduleConfigPaths(recursive) { return __awaiter(this, void 0, void 0, function* () { // Get submodule config file paths. @@ -2839,13 +2888,7 @@ class GitCommandManager { return __awaiter(this, arguments, void 0, function* (args, allowAllExitCodes = false, silent = false, customListeners = {}) { fshelper.directoryExistsSync(this.workingDirectory, true); const result = new GitOutput(); - const env = {}; - for (const key of Object.keys(process.env)) { - env[key] = process.env[key]; - } - for (const key of Object.keys(this.gitEnv)) { - env[key] = this.gitEnv[key]; - } + const env = this.getEnvironment(); const defaultListener = { stdout: (data) => { stdout.push(data.toString()); @@ -3321,7 +3364,7 @@ function getSource(settings) { mirrorFresh && !fetchOptions.filter && !fsHelper.fileExistsSync(path.join(settings.repositoryPath, '.git', 'shallow'))) { - fetchedFromMirror = yield blacksmithCache.fetchRefsFromMirror(settings.repositoryPath, cacheInfo.mirrorPath); + fetchedFromMirror = yield blacksmithCache.fetchRefsFromMirror(settings.repositoryPath, cacheInfo.mirrorPath, git.getEnvironment()); } if (fetchedFromMirror) { // The mirror copy only materializes branches and tags. Any other ref @@ -3417,7 +3460,7 @@ function getSource(settings) { // This copies all objects from alternates into the local repo so it's independent if (settings.dissociate && cacheInfo) { core.startGroup('Dissociating from Blacksmith mirror'); - yield blacksmithCache.dissociate(settings.repositoryPath); + yield blacksmithCache.dissociate(settings.repositoryPath, git.getEnvironment()); core.endGroup(); } // Submodules diff --git a/src/blacksmith-cache.ts b/src/blacksmith-cache.ts index 0ed98c9..d546c86 100644 --- a/src/blacksmith-cache.ts +++ b/src/blacksmith-cache.ts @@ -401,6 +401,35 @@ function getAuthConfigArgs( } } +/** + * Jobs sharing a mirror do not all run as the same user: a VM job runs git + * as the runner user, a job container typically as root. Git refuses a + * repository owned by someone else ("dubious ownership"), and the runner + * user cannot write into a root-owned one, so a mirror hydrated by the + * other kind of job is taken over before it is read or synced. + */ +async function adoptMirrorOwnership(mirrorPath: string): Promise { + const uid = process.getuid?.() + const gid = process.getgid?.() + if (uid === undefined || gid === undefined) { + return + } + const owner = (await fs.promises.stat(mirrorPath)).uid + if (owner === uid) { + return + } + const start = Date.now() + await exec.exec('sudo', [ + 'chown', + '-R', + `${uid}:${gid}`, + path.dirname(mirrorPath) + ]) + core.info( + `[git-mirror] Took over mirror owned by uid ${owner} in ${Date.now() - start}ms` + ) +} + /** * Build git environment with optional verbose flags */ @@ -496,6 +525,7 @@ export async function ensureMirror( // Mirror exists - the caller synchronizes it with the remote via // syncMirrorFromRemote() before populating the workspace from it core.info(`[git-mirror] Found existing mirror at ${mirrorPath}`) + await adoptMirrorOwnership(mirrorPath) return false // Not initial hydration } @@ -1214,11 +1244,17 @@ export function buildPackedRefsContent(mirrorRefs: string): string { * checkout in a reused workspace, so scripts never read fetch output that * predates this ref state. * + * `env` is the checkout's git environment (see + * IGitCommandManager.getEnvironment); the workspace directory may be owned + * by another user than the one running git (job containers), and only its + * temporary global config marks the workspace as a safe.directory. + * * @returns true on success, false if the caller should fall back */ export async function copyRefsFromMirror( workspacePath: string, - mirrorPath: string + mirrorPath: string, + env?: {[key: string]: string} ): Promise { try { const start = Date.now() @@ -1245,7 +1281,7 @@ export async function copyRefsFromMirror( const refStorage = await exec.getExecOutput( 'git', ['-C', workspacePath, 'config', '--get', 'extensions.refstorage'], - {silent: true, ignoreReturnCode: true} + {silent: true, ignoreReturnCode: true, env} ) const refBackend = refStorage.stdout.trim() if (refBackend !== '' && refBackend !== 'files') { @@ -1277,7 +1313,7 @@ export async function copyRefsFromMirror( 'refs/remotes/origin', 'refs/tags' ], - {silent: true} + {silent: true, env} ) const packedRefsPath = path.join(gitDir, 'packed-refs') @@ -1306,10 +1342,12 @@ export async function copyRefsFromMirror( if (instructions.length > 0) { await exec.exec('git', ['-C', workspacePath, 'update-ref', '--stdin'], { silent: true, - input: Buffer.from(`${instructions.join('\n')}\n`) + input: Buffer.from(`${instructions.join('\n')}\n`), + env }) await exec.exec('git', ['-C', workspacePath, 'pack-refs', '--all'], { - silent: true + silent: true, + env }) } core.info( @@ -1462,26 +1500,31 @@ async function removeStaleFetchHead(gitDir: string): Promise { */ export async function fetchRefsFromMirror( workspacePath: string, - mirrorPath: string + mirrorPath: string, + env?: {[key: string]: string} ): Promise { core.info(`[git-mirror] Fetching refs locally from mirror at ${mirrorPath}`) - if (await copyRefsFromMirror(workspacePath, mirrorPath)) { + if (await copyRefsFromMirror(workspacePath, mirrorPath, env)) { return true } try { - await exec.exec('git', [ - '-C', - workspacePath, - '-c', - 'gc.auto=0', - 'fetch', - '--prune', - '--no-tags', - '--no-recurse-submodules', - mirrorPath, - '+refs/heads/*:refs/remotes/origin/*', - '+refs/tags/*:refs/tags/*' - ]) + await exec.exec( + 'git', + [ + '-C', + workspacePath, + '-c', + 'gc.auto=0', + 'fetch', + '--prune', + '--no-tags', + '--no-recurse-submodules', + mirrorPath, + '+refs/heads/*:refs/remotes/origin/*', + '+refs/tags/*:refs/tags/*' + ], + {env} + ) return true } catch (error) { core.warning( @@ -1512,11 +1555,14 @@ export async function writeAlternates( * Dissociate the repository from the mirror by copying all objects locally * This is needed for Docker-based actions that may not have access to the mirror mount */ -export async function dissociate(workspacePath: string): Promise { +export async function dissociate( + workspacePath: string, + env?: {[key: string]: string} +): Promise { core.info('Dissociating repository from mirror') // Copy all objects from alternates into local repo - await exec.exec('git', ['-C', workspacePath, 'repack', '-a', '-d']) + await exec.exec('git', ['-C', workspacePath, 'repack', '-a', '-d'], {env}) // Remove alternates file const alternatesFile = path.join( diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index e19476c..e12b2ca 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -50,6 +50,12 @@ export interface IGitCommandManager { configExists(configKey: string, globalConfig?: boolean): Promise fetch(refSpec: string[], options: FetchOptions): Promise getDefaultBranch(repositoryUrl: string): Promise + /** + * Environment for git commands run outside this manager against the + * workspace, so they see the same HOME (temporary global config with + * safe.directory, auth) as the manager's own commands. + */ + getEnvironment(): {[key: string]: string} getSubmoduleConfigPaths(recursive: boolean): Promise getWorkingDirectory(): string init(): Promise @@ -365,6 +371,19 @@ class GitCommandManager { throw new Error('Unexpected output when retrieving default branch') } + getEnvironment(): {[key: string]: string} { + const env: {[key: string]: string} = {} + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) { + env[key] = value + } + } + for (const key of Object.keys(this.gitEnv)) { + env[key] = this.gitEnv[key] + } + return env + } + async getSubmoduleConfigPaths(recursive: boolean): Promise { // Get submodule config file paths. // Use `--show-origin` to get the config file path for each submodule. @@ -639,13 +658,7 @@ class GitCommandManager { const result = new GitOutput() - const env = {} - for (const key of Object.keys(process.env)) { - env[key] = process.env[key] - } - for (const key of Object.keys(this.gitEnv)) { - env[key] = this.gitEnv[key] - } + const env = this.getEnvironment() const defaultListener = { stdout: (data: Buffer) => { diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 9e756b4..63cb15d 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -275,7 +275,8 @@ export async function getSource(settings: IGitSourceSettings): Promise { ) { fetchedFromMirror = await blacksmithCache.fetchRefsFromMirror( settings.repositoryPath, - cacheInfo.mirrorPath + cacheInfo.mirrorPath, + git.getEnvironment() ) } @@ -396,7 +397,10 @@ export async function getSource(settings: IGitSourceSettings): Promise { // This copies all objects from alternates into the local repo so it's independent if (settings.dissociate && cacheInfo) { core.startGroup('Dissociating from Blacksmith mirror') - await blacksmithCache.dissociate(settings.repositoryPath) + await blacksmithCache.dissociate( + settings.repositoryPath, + git.getEnvironment() + ) core.endGroup() }