Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/cache/__tests__/tar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,69 @@ test('zstd extract tar with windows BSDtar', async () => {
}
})

test('zstd extract tar prefers BSDtar when opt-in env var set', async () => {
if (IS_WINDOWS) {
const mkdirMock = jest.spyOn(io, 'mkdirP')
const execMock = jest.spyOn(exec, 'exec')
const previousPreferBsdTar =
process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS']
// GNU tar still available — without the env var this would take the GNU path.
jest
.spyOn(utils, 'getGnuTarPathOnWindows')
.mockReturnValue(Promise.resolve(GnuTarPathOnWindows))
process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] = 'true'

Comment on lines +147 to +148
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test deletes ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS in finally, which can clobber an existing value if the env var was already set in the test environment. To keep the test hermetic, capture the prior value before setting it and restore it in finally (set back to the old value or delete only if it was originally undefined).

Copilot uses AI. Check for mistakes.
try {
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: undefined,
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: undefined,
env: expect.objectContaining(defaultEnv)
}
)
} finally {
if (previousPreferBsdTar === undefined) {
delete process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS']
} else {
process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] =
previousPreferBsdTar
}
}
}
})

test('gzip extract tar', async () => {
const mkdirMock = jest.spyOn(io, 'mkdirP')
const execMock = jest.spyOn(exec, 'exec')
Expand Down
13 changes: 12 additions & 1 deletion packages/cache/src/internal/tar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,19 @@ const IS_WINDOWS = process.platform === 'win32'
async function getTarPath(): Promise<ArchiveTool> {
switch (process.platform) {
case 'win32': {
const gnuTar = await utils.getGnuTarPathOnWindows()
const systemTar = SystemTarPathOnWindows
// Opt-in: prefer BSD tar (libarchive, shipped as
// C:\Windows\System32\tar.exe on Windows 10+). Benchmarks on hosted
// runners show ~4x faster extract on many-small-files payloads
// compared to Git for Windows' MSYS GNU tar, likely due to native
// Win32/libarchive behavior and lower MSYS process/path translation
// overhead. See actions/cache#752 for context.
const preferBsdTar =
process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] === 'true'
if (preferBsdTar && existsSync(systemTar)) {
return <ArchiveTool>{path: systemTar, type: ArchiveToolType.BSD}
}
const gnuTar = await utils.getGnuTarPathOnWindows()
if (gnuTar) {
// Use GNUtar as default on windows
return <ArchiveTool>{path: gnuTar, type: ArchiveToolType.GNU}
Expand Down