From 6ced2e75b488f9784c8c2afbc4380d365ed16c50 Mon Sep 17 00:00:00 2001 From: Daniel Kennedy Date: Thu, 29 Jan 2026 15:55:42 -0500 Subject: [PATCH] Check the URL path for `.zip` to see if we can auto-decompress --- .../__tests__/download-artifact.test.ts | 34 +++++++++++++++++++ .../internal/download/download-artifact.ts | 10 ++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/artifact/__tests__/download-artifact.test.ts b/packages/artifact/__tests__/download-artifact.test.ts index 858cef12..bd4efab9 100644 --- a/packages/artifact/__tests__/download-artifact.test.ts +++ b/packages/artifact/__tests__/download-artifact.test.ts @@ -821,6 +821,40 @@ describe('download-artifact', () => { await expectExtractedArchive(fixtures.workspaceDir) }) + it('should extract zip when URL ends with .zip even if content-type is not application/zip', async () => { + const blobUrlWithZipExtension = + 'https://blob-storage.local/artifact.zip?sig=abc123' + + const mockGetZipByUrl = jest.fn(() => { + const message = new http.IncomingMessage(new net.Socket()) + message.statusCode = 200 + // Azure Blob Storage may return a generic content-type + message.headers['content-type'] = 'application/octet-stream' + message.push(fs.readFileSync(fixtures.exampleArtifact.path)) + message.push(null) + return { + message + } + }) + + const mockHttpClient = (HttpClient as jest.Mock).mockImplementation( + () => { + return { + get: mockGetZipByUrl + } + } + ) + + await streamExtractExternal( + blobUrlWithZipExtension, + fixtures.workspaceDir + ) + + expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString()) + // Verify files were extracted based on URL .zip extension + 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( () => { diff --git a/packages/artifact/src/internal/download/download-artifact.ts b/packages/artifact/src/internal/download/download-artifact.ts index 19fbc71b..81c52e9a 100644 --- a/packages/artifact/src/internal/download/download-artifact.ts +++ b/packages/artifact/src/internal/download/download-artifact.ts @@ -81,10 +81,16 @@ export async function streamExtractExternal( const contentType = response.message.headers['content-type'] || '' const mimeType = contentType.split(';', 1)[0].trim().toLowerCase() + + // Check if the URL path ends with .zip (ignoring query parameters) + const urlPath = new URL(url).pathname.toLowerCase() + const urlEndsWithZip = urlPath.endsWith('.zip') + const isZip = mimeType === 'application/zip' || mimeType === 'application/x-zip-compressed' || - mimeType === 'application/zip-compressed' + mimeType === 'application/zip-compressed' || + urlEndsWithZip // Extract filename from Content-Disposition header const contentDisposition = @@ -100,7 +106,7 @@ export async function streamExtractExternal( } core.debug( - `Content-Type: ${contentType}, isZip: ${isZip}, skipDecompress: ${skipDecompress}` + `Content-Type: ${contentType}, mimeType: ${mimeType}, urlEndsWithZip: ${urlEndsWithZip}, isZip: ${isZip}, skipDecompress: ${skipDecompress}` ) core.debug( `Content-Disposition: ${contentDisposition}, fileName: ${fileName}`