Add support for specifying whether to skip decompressing

This commit is contained in:
Daniel Kennedy
2026-01-29 15:17:38 -05:00
parent c2139ac002
commit e7359c0031
3 changed files with 51 additions and 9 deletions
@@ -815,5 +815,31 @@ describe('download-artifact', () => {
// Verify files were extracted // Verify files were extracted
await expectExtractedArchive(fixtures.workspaceDir) await expectExtractedArchive(fixtures.workspaceDir)
}) })
it('should skip decompression when skipDecompress option is true even for zip content-type', async () => {
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetArtifactSuccess
}
}
)
await streamExtractExternal(
fixtures.blobStorageUrl,
fixtures.workspaceDir,
{skipDecompress: true}
)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
// Verify zip was saved as-is, not extracted
// When skipDecompress is true, the file should be saved with default name 'artifact'
const savedFilePath = path.join(fixtures.workspaceDir, 'artifact')
expect(fs.existsSync(savedFilePath)).toBe(true)
// The saved file should be the raw zip content
const savedContent = fs.readFileSync(savedFilePath)
const originalZipContent = fs.readFileSync(fixtures.exampleArtifact.path)
expect(savedContent).toEqual(originalZipContent)
})
}) })
}) })
@@ -45,12 +45,13 @@ async function exists(path: string): Promise<boolean> {
async function streamExtract( async function streamExtract(
url: string, url: string,
directory: string directory: string,
skipDecompress?: boolean
): Promise<StreamExtractResponse> { ): Promise<StreamExtractResponse> {
let retryCount = 0 let retryCount = 0
while (retryCount < 5) { while (retryCount < 5) {
try { try {
return await streamExtractExternal(url, directory) return await streamExtractExternal(url, directory, {skipDecompress})
} catch (error) { } catch (error) {
retryCount++ retryCount++
core.debug( core.debug(
@@ -67,8 +68,9 @@ async function streamExtract(
export async function streamExtractExternal( export async function streamExtractExternal(
url: string, url: string,
directory: string, directory: string,
opts: {timeout: number} = {timeout: 30 * 1000} opts: {timeout?: number; skipDecompress?: boolean} = {}
): Promise<StreamExtractResponse> { ): Promise<StreamExtractResponse> {
const {timeout = 30 * 1000, skipDecompress = false} = opts
const client = new httpClient.HttpClient(getUserAgentString()) const client = new httpClient.HttpClient(getUserAgentString())
const response = await client.get(url) const response = await client.get(url)
if (response.message.statusCode !== 200) { if (response.message.statusCode !== 200) {
@@ -94,7 +96,7 @@ export async function streamExtractExternal(
fileName = decodeURIComponent(filenameMatch[1].trim()) fileName = decodeURIComponent(filenameMatch[1].trim())
} }
core.debug(`Content-Type: ${contentType}, isZip: ${isZip}`) core.debug(`Content-Type: ${contentType}, isZip: ${isZip}, skipDecompress: ${skipDecompress}`)
core.debug(`Content-Disposition: ${contentDisposition}, fileName: ${fileName}`) core.debug(`Content-Disposition: ${contentDisposition}, fileName: ${fileName}`)
let sha256Digest: string | undefined = undefined let sha256Digest: string | undefined = undefined
@@ -102,12 +104,12 @@ export async function streamExtractExternal(
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timerFn = (): void => { const timerFn = (): void => {
const timeoutError = new Error( const timeoutError = new Error(
`Blob storage chunk did not respond in ${opts.timeout}ms` `Blob storage chunk did not respond in ${timeout}ms`
) )
response.message.destroy(timeoutError) response.message.destroy(timeoutError)
reject(timeoutError) reject(timeoutError)
} }
const timer = setTimeout(timerFn, opts.timeout) const timer = setTimeout(timerFn, timeout)
const onError = (error: Error): void => { const onError = (error: Error): void => {
core.debug( core.debug(
@@ -137,7 +139,7 @@ export async function streamExtractExternal(
resolve({sha256Digest: `sha256:${sha256Digest}`}) resolve({sha256Digest: `sha256:${sha256Digest}`})
} }
if (isZip) { if (isZip && !skipDecompress) {
// Extract zip file // Extract zip file
passThrough.pipe(unzip.Extract({path: directory})).on('close', onClose).on('error', onError) passThrough.pipe(unzip.Extract({path: directory})).on('close', onClose).on('error', onError)
} else { } else {
@@ -193,7 +195,11 @@ export async function downloadArtifactPublic(
try { try {
core.info(`Starting download of artifact to: ${downloadPath}`) core.info(`Starting download of artifact to: ${downloadPath}`)
const extractResponse = await streamExtract(location, downloadPath) const extractResponse = await streamExtract(
location,
downloadPath,
options?.skipDecompress
)
core.info(`Artifact download completed successfully.`) core.info(`Artifact download completed successfully.`)
if (options?.expectedHash) { if (options?.expectedHash) {
if (options?.expectedHash !== extractResponse.sha256Digest) { if (options?.expectedHash !== extractResponse.sha256Digest) {
@@ -254,7 +260,11 @@ export async function downloadArtifactInternal(
try { try {
core.info(`Starting download of artifact to: ${downloadPath}`) core.info(`Starting download of artifact to: ${downloadPath}`)
const extractResponse = await streamExtract(signedUrl, downloadPath) const extractResponse = await streamExtract(
signedUrl,
downloadPath,
options?.skipDecompress
)
core.info(`Artifact download completed successfully.`) core.info(`Artifact download completed successfully.`)
if (options?.expectedHash) { if (options?.expectedHash) {
if (options?.expectedHash !== extractResponse.sha256Digest) { if (options?.expectedHash !== extractResponse.sha256Digest) {
@@ -113,6 +113,12 @@ export interface DownloadArtifactOptions {
* matches the expected hash. * matches the expected hash.
*/ */
expectedHash?: string expectedHash?: string
/**
* If true, the downloaded artifact will not be automatically extracted/decompressed.
* The artifact will be saved as-is to the destination path.
*/
skipDecompress?: boolean
} }
export interface StreamExtractResponse { export interface StreamExtractResponse {