Run Windows BSD tar/zstd extraction and listing in private temporary directories. Concurrent operations previously overwrote cache.tar in the process working directory and could extract the wrong archive without reporting an error. Debug listings during cache saves have the same race. Resolve and quote archive and workspace paths before changing the child working directory, and remove scratch files on success and failure. Leave GNU tar, gzip, and the cache archive format unchanged. Add deterministic regression tests for concurrent reads, relative paths with spaces, and cleanup after failures in either subprocess.
487 lines
13 KiB
TypeScript
487 lines
13 KiB
TypeScript
import * as exec from '@actions/exec'
|
|
import * as io from '@actions/io'
|
|
import * as path from 'path'
|
|
import {
|
|
CacheFilename,
|
|
CompressionMethod,
|
|
GnuTarPathOnWindows,
|
|
ManifestFilename,
|
|
SystemTarPathOnWindows,
|
|
TarFilename
|
|
} from '../src/internal/constants'
|
|
import * as tar from '../src/internal/tar'
|
|
import * as utils from '../src/internal/cacheUtils'
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
import fs = require('fs')
|
|
|
|
jest.mock('@actions/exec')
|
|
jest.mock('@actions/io')
|
|
|
|
const IS_WINDOWS = process.platform === 'win32'
|
|
const IS_MAC = process.platform === 'darwin'
|
|
|
|
const defaultTarPath = IS_MAC ? 'gtar' : 'tar'
|
|
|
|
const defaultEnv = {MSYS: 'winsymlinks:nativestrict'}
|
|
|
|
function getTempDir(): string {
|
|
return path.join(__dirname, '_temp', 'tar')
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
jest.spyOn(io, 'which').mockImplementation(async tool => {
|
|
return tool
|
|
})
|
|
|
|
process.env['GITHUB_WORKSPACE'] = process.cwd()
|
|
await jest.requireActual('@actions/io').rmRF(getTempDir())
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
jest.restoreAllMocks()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
delete process.env['GITHUB_WORKSPACE']
|
|
await jest.requireActual('@actions/io').rmRF(getTempDir())
|
|
})
|
|
|
|
test('zstd extract tar', async () => {
|
|
const mkdirMock = jest.spyOn(io, 'mkdirP')
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
|
|
const archivePath = IS_WINDOWS
|
|
? `${process.env['windir']}\\fakepath\\cache.tar`
|
|
: 'cache.tar'
|
|
const workspace = process.env['GITHUB_WORKSPACE']
|
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
|
|
|
await tar.extractTar(archivePath, CompressionMethod.Zstd)
|
|
|
|
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"${tarPath}"`,
|
|
'-xf',
|
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
|
'-P',
|
|
'-C',
|
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
|
]
|
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
|
.concat([
|
|
'--use-compress-program',
|
|
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
|
|
])
|
|
.join(' '),
|
|
undefined,
|
|
{
|
|
cwd: undefined,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
})
|
|
|
|
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')
|
|
.mockReturnValue(Promise.resolve(''))
|
|
|
|
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
|
|
const workspace = process.env['GITHUB_WORKSPACE']
|
|
const tarPath = SystemTarPathOnWindows
|
|
|
|
await tar.extractTar(archivePath, CompressionMethod.Zstd)
|
|
|
|
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
|
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}"`,
|
|
'-xf',
|
|
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
|
'-P',
|
|
'-C',
|
|
`"${workspace?.replace(/\\/g, '/')}"`
|
|
].join(' '),
|
|
undefined,
|
|
{
|
|
cwd: tempDirectory,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
}
|
|
})
|
|
|
|
test('gzip extract tar', async () => {
|
|
const mkdirMock = jest.spyOn(io, 'mkdirP')
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
const archivePath = IS_WINDOWS
|
|
? `${process.env['windir']}\\fakepath\\cache.tar`
|
|
: 'cache.tar'
|
|
const workspace = process.env['GITHUB_WORKSPACE']
|
|
|
|
await tar.extractTar(archivePath, CompressionMethod.Gzip)
|
|
|
|
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"${tarPath}"`,
|
|
'-xf',
|
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
|
'-P',
|
|
'-C',
|
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
|
]
|
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
|
.concat(['-z'])
|
|
.join(' '),
|
|
undefined,
|
|
{
|
|
cwd: undefined,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('gzip extract GNU tar on windows with GNUtar in path', async () => {
|
|
if (IS_WINDOWS) {
|
|
// GNU tar present in path but not at default location
|
|
jest
|
|
.spyOn(utils, 'getGnuTarPathOnWindows')
|
|
.mockReturnValue(Promise.resolve('tar'))
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
|
|
const workspace = process.env['GITHUB_WORKSPACE']
|
|
|
|
await tar.extractTar(archivePath, CompressionMethod.Gzip)
|
|
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"tar"`,
|
|
'-xf',
|
|
archivePath.replace(/\\/g, '/'),
|
|
'-P',
|
|
'-C',
|
|
workspace?.replace(/\\/g, '/'),
|
|
'--force-local',
|
|
'-z'
|
|
].join(' '),
|
|
undefined,
|
|
{
|
|
cwd: undefined,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
}
|
|
})
|
|
|
|
test('zstd create tar', async () => {
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
|
|
const archiveFolder = getTempDir()
|
|
const workspace = process.env['GITHUB_WORKSPACE']
|
|
const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`]
|
|
|
|
await fs.promises.mkdir(archiveFolder, {recursive: true})
|
|
|
|
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Zstd)
|
|
|
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
|
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"${tarPath}"`,
|
|
'--posix',
|
|
'-cf',
|
|
IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd,
|
|
'--exclude',
|
|
IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd,
|
|
'-P',
|
|
'-C',
|
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
|
'--files-from',
|
|
ManifestFilename
|
|
]
|
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
|
.concat([
|
|
'--use-compress-program',
|
|
IS_WINDOWS ? '"zstd -T0 --long=30"' : 'zstdmt --long=30'
|
|
])
|
|
.join(' '),
|
|
undefined, // args
|
|
{
|
|
cwd: archiveFolder,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('zstd create tar with windows BSDtar', async () => {
|
|
if (IS_WINDOWS) {
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
jest
|
|
.spyOn(utils, 'getGnuTarPathOnWindows')
|
|
.mockReturnValue(Promise.resolve(''))
|
|
|
|
const archiveFolder = getTempDir()
|
|
const workspace = process.env['GITHUB_WORKSPACE']
|
|
const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`]
|
|
|
|
await fs.promises.mkdir(archiveFolder, {recursive: true})
|
|
|
|
await tar.createTar(
|
|
archiveFolder,
|
|
sourceDirectories,
|
|
CompressionMethod.Zstd
|
|
)
|
|
|
|
const tarPath = SystemTarPathOnWindows
|
|
|
|
expect(execMock).toHaveBeenCalledTimes(2)
|
|
|
|
expect(execMock).toHaveBeenNthCalledWith(
|
|
1,
|
|
[
|
|
`"${tarPath}"`,
|
|
'--posix',
|
|
'-cf',
|
|
TarFilename.replace(/\\/g, '/'),
|
|
'--exclude',
|
|
TarFilename.replace(/\\/g, '/'),
|
|
'-P',
|
|
'-C',
|
|
`"${workspace?.replace(/\\/g, '/')}"`,
|
|
'--files-from',
|
|
ManifestFilename
|
|
].join(' '),
|
|
undefined, // args
|
|
{
|
|
cwd: archiveFolder,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
|
|
expect(execMock).toHaveBeenNthCalledWith(
|
|
2,
|
|
[
|
|
'zstd -T0 --long=30 --force -o',
|
|
CacheFilename.Zstd.replace(/\\/g, '/'),
|
|
TarFilename.replace(/\\/g, '/')
|
|
].join(' '),
|
|
undefined, // args
|
|
{
|
|
cwd: archiveFolder,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
}
|
|
})
|
|
|
|
test('gzip create tar', async () => {
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
|
|
const archiveFolder = getTempDir()
|
|
const workspace = process.env['GITHUB_WORKSPACE']
|
|
const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`]
|
|
|
|
await fs.promises.mkdir(archiveFolder, {recursive: true})
|
|
|
|
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Gzip)
|
|
|
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
|
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"${tarPath}"`,
|
|
'--posix',
|
|
'-cf',
|
|
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
|
|
'--exclude',
|
|
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
|
|
'-P',
|
|
'-C',
|
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
|
'--files-from',
|
|
ManifestFilename
|
|
]
|
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
|
.concat(['-z'])
|
|
.join(' '),
|
|
undefined, // args
|
|
{
|
|
cwd: archiveFolder,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('zstd list tar', async () => {
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
|
|
const archivePath = IS_WINDOWS
|
|
? `${process.env['windir']}\\fakepath\\cache.tar`
|
|
: 'cache.tar'
|
|
|
|
await tar.listTar(archivePath, CompressionMethod.Zstd)
|
|
|
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"${tarPath}"`,
|
|
'-tf',
|
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
|
'-P'
|
|
]
|
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
|
.concat([
|
|
'--use-compress-program',
|
|
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
|
|
])
|
|
.join(' '),
|
|
undefined,
|
|
{
|
|
cwd: undefined,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
})
|
|
|
|
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')
|
|
|
|
const archivePath = IS_WINDOWS
|
|
? `${process.env['windir']}\\fakepath\\cache.tar`
|
|
: 'cache.tar'
|
|
|
|
await tar.listTar(archivePath, CompressionMethod.ZstdWithoutLong)
|
|
|
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"${tarPath}"`,
|
|
'-tf',
|
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
|
'-P'
|
|
]
|
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
|
.concat(['--use-compress-program', IS_WINDOWS ? '"zstd -d"' : 'unzstd'])
|
|
.join(' '),
|
|
undefined,
|
|
{
|
|
cwd: undefined,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
})
|
|
|
|
test('gzip list tar', async () => {
|
|
const execMock = jest.spyOn(exec, 'exec')
|
|
const archivePath = IS_WINDOWS
|
|
? `${process.env['windir']}\\fakepath\\cache.tar`
|
|
: 'cache.tar'
|
|
|
|
await tar.listTar(archivePath, CompressionMethod.Gzip)
|
|
|
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
|
expect(execMock).toHaveBeenCalledTimes(1)
|
|
expect(execMock).toHaveBeenCalledWith(
|
|
[
|
|
`"${tarPath}"`,
|
|
'-tf',
|
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
|
'-P'
|
|
]
|
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
|
.concat(['-z'])
|
|
.join(' '),
|
|
undefined,
|
|
{
|
|
cwd: undefined,
|
|
env: expect.objectContaining(defaultEnv)
|
|
}
|
|
)
|
|
})
|