From fce871591384b3b26dc38d4386b762337b9c63be Mon Sep 17 00:00:00 2001 From: qmuntal Date: Thu, 17 Sep 2026 15:53:20 +0200 Subject: [PATCH] chore(cache): minimize tar isolation diff Restore the existing helper layout and separate test cases rather than bundling unrelated consolidation with the bug fix. Leave archive creation unchanged and retain only the read isolation, necessary path handling, and regression coverage. --- __tests__/tar.test.ts | 105 ++++++++++++++++++++++++++++++------------ src/internal/tar.ts | 74 +++++++++++++++++------------ 2 files changed, 121 insertions(+), 58 deletions(-) diff --git a/__tests__/tar.test.ts b/__tests__/tar.test.ts index f85a51d..ed4ab42 100644 --- a/__tests__/tar.test.ts +++ b/__tests__/tar.test.ts @@ -84,56 +84,57 @@ test('zstd extract tar', async () => { ) }) -test.each([ - {operation: 'extract', read: tar.extractTar, flag: '-xf'}, - {operation: 'list', read: tar.listTar, flag: '-tf'} -])( - 'zstd $operation tar with windows BSDtar', - async ({operation, read, flag}) => { - if (!IS_WINDOWS) { - return - } +test('zstd extract tar with windows BSDtar', async () => { + if (IS_WINDOWS) { const mkdirMock = jest.spyOn(io, 'mkdirP') const execMock = jest.spyOn(exec, 'exec') const tempDirectory = getTempDir() jest.spyOn(utils, 'createTempDirectory').mockResolvedValue(tempDirectory) - jest.spyOn(utils, 'getGnuTarPathOnWindows').mockResolvedValue('') + jest + .spyOn(utils, 'getGnuTarPathOnWindows') + .mockReturnValue(Promise.resolve('')) const archivePath = `${process.env['windir']}\\fakepath\\cache.tar` const workspace = process.env['GITHUB_WORKSPACE'] - const options = { - cwd: tempDirectory, - env: expect.objectContaining(defaultEnv) - } - const tarArgs = [`"${SystemTarPathOnWindows}"`, flag, TarFilename, '-P'] - if (operation === 'extract') { - tarArgs.push('-C', `"${workspace?.replace(/\\/g, '/')}"`) - } + const tarPath = SystemTarPathOnWindows - await read(archivePath, CompressionMethod.Zstd) + await tar.extractTar(archivePath, CompressionMethod.Zstd) - if (operation === 'extract') { - expect(mkdirMock).toHaveBeenCalledWith(workspace) - } + expect(mkdirMock).toHaveBeenCalledWith(workspace) expect(execMock).toHaveBeenCalledTimes(2) + expect(execMock).toHaveBeenNthCalledWith( 1, [ 'zstd -d --long=30 --force -o', - TarFilename, - `"${archivePath.replace(/\\/g, '/')}"` + TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + `"${archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')}"` ].join(' '), undefined, - options + { + cwd: tempDirectory, + env: expect.objectContaining(defaultEnv) + } ) + expect(execMock).toHaveBeenNthCalledWith( 2, - tarArgs.join(' '), + [ + `"${tarPath}"`, + '-xf', + TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P', + '-C', + `"${workspace?.replace(/\\/g, '/')}"` + ].join(' '), undefined, - options + { + cwd: tempDirectory, + env: expect.objectContaining(defaultEnv) + } ) } -) +}) test('gzip extract tar', async () => { const mkdirMock = jest.spyOn(io, 'mkdirP') @@ -279,7 +280,7 @@ test('zstd create tar with windows BSDtar', async () => { TarFilename.replace(/\\/g, '/'), '-P', '-C', - `"${workspace?.replace(/\\/g, '/')}"`, + workspace?.replace(/\\/g, '/'), '--files-from', ManifestFilename ].join(' '), @@ -379,6 +380,52 @@ test('zstd list tar', async () => { ) }) +test('zstd list tar with windows BSDtar', async () => { + if (IS_WINDOWS) { + const execMock = jest.spyOn(exec, 'exec') + const tempDirectory = getTempDir() + jest.spyOn(utils, 'createTempDirectory').mockResolvedValue(tempDirectory) + jest + .spyOn(utils, 'getGnuTarPathOnWindows') + .mockReturnValue(Promise.resolve('')) + const archivePath = `${process.env['windir']}\\fakepath\\cache.tar` + + await tar.listTar(archivePath, CompressionMethod.Zstd) + + const tarPath = SystemTarPathOnWindows + expect(execMock).toHaveBeenCalledTimes(2) + + expect(execMock).toHaveBeenNthCalledWith( + 1, + [ + 'zstd -d --long=30 --force -o', + TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + `"${archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')}"` + ].join(' '), + undefined, + { + cwd: tempDirectory, + env: expect.objectContaining(defaultEnv) + } + ) + + expect(execMock).toHaveBeenNthCalledWith( + 2, + [ + `"${tarPath}"`, + '-tf', + TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P' + ].join(' '), + undefined, + { + cwd: tempDirectory, + env: expect.objectContaining(defaultEnv) + } + ) + } +}) + test('zstdWithoutLong list tar', async () => { const execMock = jest.spyOn(exec, 'exec') diff --git a/src/internal/tar.ts b/src/internal/tar.ts index 1e80329..e3334df 100644 --- a/src/internal/tar.ts +++ b/src/internal/tar.ts @@ -19,17 +19,6 @@ interface TarCommands { requiresTempDirectory: boolean } -function isBsdTarZstd( - tarPath: ArchiveTool, - compressionMethod: CompressionMethod -): boolean { - return ( - IS_WINDOWS && - tarPath.type === ArchiveToolType.BSD && - compressionMethod !== CompressionMethod.Gzip - ) -} - // Returns tar path and type: BSD or GNU async function getTarPath(): Promise { switch (process.platform) { @@ -74,16 +63,14 @@ async function getTarArgs( archivePath = '' ): Promise { const args = [`"${tarPath.path}"`] - const BSD_TAR_ZSTD = isBsdTarZstd(tarPath, compressionMethod) - const tarFile = BSD_TAR_ZSTD - ? TarFilename - : (type === 'create' - ? utils.getCacheFileName(compressionMethod) - : archivePath - ).replace(new RegExp(`\\${path.sep}`, 'g'), '/') - const workingDirectory = BSD_TAR_ZSTD - ? quoteAbsolutePath(getWorkingDirectory()) - : getWorkingDirectory().replace(new RegExp(`\\${path.sep}`, 'g'), '/') + const cacheFileName = utils.getCacheFileName(compressionMethod) + const tarFile = 'cache.tar' + const workingDirectory = getWorkingDirectory() + // Speficic args for BSD tar on windows for workaround + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS // Method specific args switch (type) { @@ -91,21 +78,41 @@ async function getTarArgs( args.push( '--posix', '-cf', - tarFile, + BSD_TAR_ZSTD + ? tarFile + : cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), '--exclude', - tarFile, + BSD_TAR_ZSTD + ? tarFile + : cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), '-P', '-C', - workingDirectory, + workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), '--files-from', ManifestFilename ) break case 'extract': - args.push('-xf', tarFile, '-P', '-C', workingDirectory) + args.push( + '-xf', + BSD_TAR_ZSTD + ? tarFile + : archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P', + '-C', + BSD_TAR_ZSTD + ? quoteAbsolutePath(workingDirectory) + : workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/') + ) break case 'list': - args.push('-tf', tarFile, '-P') + args.push( + '-tf', + BSD_TAR_ZSTD + ? tarFile + : archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P' + ) break } @@ -143,7 +150,10 @@ async function getCommands( type !== 'create' ? await getDecompressionProgram(tarPath, compressionMethod, archivePath) : await getCompressionProgram(tarPath, compressionMethod) - const BSD_TAR_ZSTD = isBsdTarZstd(tarPath, compressionMethod) + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS if (BSD_TAR_ZSTD && type !== 'create') { args = [[...compressionArgs].join(' '), [...tarArgs].join(' ')] @@ -175,7 +185,10 @@ async function getDecompressionProgram( // unzstd is equivalent to 'zstd -d' // --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit. // Using 30 here because we also support 32-bit self-hosted runners. - const BSD_TAR_ZSTD = isBsdTarZstd(tarPath, compressionMethod) + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS switch (compressionMethod) { case CompressionMethod.Zstd: return BSD_TAR_ZSTD @@ -208,7 +221,10 @@ async function getCompressionProgram( compressionMethod: CompressionMethod ): Promise { const cacheFileName = utils.getCacheFileName(compressionMethod) - const BSD_TAR_ZSTD = isBsdTarZstd(tarPath, compressionMethod) + const BSD_TAR_ZSTD = + tarPath.type === ArchiveToolType.BSD && + compressionMethod !== CompressionMethod.Gzip && + IS_WINDOWS switch (compressionMethod) { case CompressionMethod.Zstd: return BSD_TAR_ZSTD