diff --git a/packages/artifact/__tests__/upload-zip-specification.test.ts b/packages/artifact/__tests__/upload-zip-specification.test.ts index 0b59bff7c6..aebb6f16b2 100644 --- a/packages/artifact/__tests__/upload-zip-specification.test.ts +++ b/packages/artifact/__tests__/upload-zip-specification.test.ts @@ -38,6 +38,7 @@ const extraFileInFolderCPath = path.join( 'extra-file-in-folder-c.txt' ) const amazingFileInFolderHPath = path.join(root, 'folder-h', 'amazing-item.txt') +const symlinkToFolderDPath = path.join(root, 'symlink-to-folder-d') const artifactFilesToUpload = [ goodItem1Path, @@ -46,7 +47,8 @@ const artifactFilesToUpload = [ goodItem4Path, goodItem5Path, extraFileInFolderCPath, - amazingFileInFolderHPath + amazingFileInFolderHPath, + symlinkToFolderDPath ] describe('Search', () => { @@ -89,6 +91,8 @@ describe('Search', () => { await fs.writeFile(extraFileInFolderCPath, 'extra file') await fs.writeFile(amazingFileInFolderHPath, 'amazing file') + + await fs.symlink(path.join(root, 'folder-d'), symlinkToFolderDPath) /* Directory structure of files that get created: root/ @@ -112,6 +116,7 @@ describe('Search', () => { folder-i/ bad-item4.txt bad-item5.txt + symlink-to-folder-d -> folder-d/ good-item5.txt */ }) @@ -168,7 +173,7 @@ describe('Search', () => { artifactFilesToUpload, root ) - expect(specifications.length).toEqual(7) + expect(specifications.length).toEqual(8) const absolutePaths = specifications.map(item => item.sourcePath) expect(absolutePaths).toContain(goodItem1Path) @@ -178,6 +183,7 @@ describe('Search', () => { expect(absolutePaths).toContain(goodItem5Path) expect(absolutePaths).toContain(extraFileInFolderCPath) expect(absolutePaths).toContain(amazingFileInFolderHPath) + expect(absolutePaths).toContain(symlinkToFolderDPath) for (const specification of specifications) { if (specification.sourcePath === goodItem1Path) { @@ -213,6 +219,13 @@ describe('Search', () => { expect(specification.destinationPath).toEqual( path.join('/folder-h', 'amazing-item.txt') ) + } else if (specification.sourcePath === symlinkToFolderDPath) { + expect(specification.destinationPath).toEqual( + path.join('/symlink-to-folder-d') + ) + expect(specification.symlinkTargetPath).toEqual( + path.join(root, '/folder-d') + ) } else { throw new Error( 'Invalid specification found. This should never be reached' @@ -227,7 +240,7 @@ describe('Search', () => { artifactFilesToUpload, rootWithSlash ) - expect(specifications.length).toEqual(7) + expect(specifications.length).toEqual(8) const absolutePaths = specifications.map(item => item.sourcePath) expect(absolutePaths).toContain(goodItem1Path) @@ -237,6 +250,7 @@ describe('Search', () => { expect(absolutePaths).toContain(goodItem5Path) expect(absolutePaths).toContain(extraFileInFolderCPath) expect(absolutePaths).toContain(amazingFileInFolderHPath) + expect(absolutePaths).toContain(symlinkToFolderDPath) for (const specification of specifications) { if (specification.sourcePath === goodItem1Path) { @@ -272,6 +286,13 @@ describe('Search', () => { expect(specification.destinationPath).toEqual( path.join('/folder-h', 'amazing-item.txt') ) + } else if (specification.sourcePath === symlinkToFolderDPath) { + expect(specification.destinationPath).toEqual( + path.join('/symlink-to-folder-d') + ) + expect(specification.symlinkTargetPath).toEqual( + path.join(root, '/folder-d') + ) } else { throw new Error( 'Invalid specification found. This should never be reached' diff --git a/packages/artifact/src/internal/shared/interfaces.ts b/packages/artifact/src/internal/shared/interfaces.ts index eb55ae8beb..007b722c44 100644 --- a/packages/artifact/src/internal/shared/interfaces.ts +++ b/packages/artifact/src/internal/shared/interfaces.ts @@ -45,6 +45,13 @@ export interface UploadArtifactOptions { * For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads. */ compressionLevel?: number + + /** + * Whether or not symlinks in artifact ZIP should be followed. + * + * By default, artifact ZIP will follow symlinks. + */ + followSymlinks?: boolean } /** diff --git a/packages/artifact/src/internal/upload/upload-artifact.ts b/packages/artifact/src/internal/upload/upload-artifact.ts index e880102fe5..d4c9ffab14 100644 --- a/packages/artifact/src/internal/upload/upload-artifact.ts +++ b/packages/artifact/src/internal/upload/upload-artifact.ts @@ -70,7 +70,8 @@ export async function uploadArtifact( const zipUploadStream = await createZipUploadStream( zipSpecification, - options?.compressionLevel + options?.compressionLevel, + options?.followSymlinks ) // Upload zip to blob storage diff --git a/packages/artifact/src/internal/upload/upload-zip-specification.ts b/packages/artifact/src/internal/upload/upload-zip-specification.ts index c6e807e646..01ebe41dae 100644 --- a/packages/artifact/src/internal/upload/upload-zip-specification.ts +++ b/packages/artifact/src/internal/upload/upload-zip-specification.ts @@ -13,6 +13,11 @@ export interface UploadZipSpecification { * The destination path in a zip for a file */ destinationPath: string + + /** + * The relative path to a symlink target (file or directory) in a zip + */ + symlinkTargetPath?: string } /** @@ -78,7 +83,8 @@ export function getUploadZipSpecification( if (!fs.existsSync(file)) { throw new Error(`File ${file} does not exist`) } - if (!fs.statSync(file).isDirectory()) { + const fileLstat = fs.lstatSync(file) + if (!fileLstat.isDirectory()) { // Normalize and resolve, this allows for either absolute or relative paths to be used file = normalize(file) file = resolve(file) @@ -94,7 +100,10 @@ export function getUploadZipSpecification( specification.push({ sourcePath: file, - destinationPath: uploadPath + destinationPath: uploadPath, + symlinkTargetPath: fileLstat.isSymbolicLink() + ? fs.readlinkSync(file) + : undefined }) } else { // Empty directory diff --git a/packages/artifact/src/internal/upload/zip.ts b/packages/artifact/src/internal/upload/zip.ts index 10433fb871..d31f2d3023 100644 --- a/packages/artifact/src/internal/upload/zip.ts +++ b/packages/artifact/src/internal/upload/zip.ts @@ -23,7 +23,8 @@ export class ZipUploadStream extends stream.Transform { export async function createZipUploadStream( uploadSpecification: UploadZipSpecification[], - compressionLevel: number = DEFAULT_COMPRESSION_LEVEL + compressionLevel: number = DEFAULT_COMPRESSION_LEVEL, + followSymlinks = true ): Promise { core.debug( `Creating Artifact archive with compressionLevel: ${compressionLevel}` @@ -42,10 +43,15 @@ export async function createZipUploadStream( for (const file of uploadSpecification) { if (file.sourcePath !== null) { - // Add a normal file to the zip - zip.file(file.sourcePath, { - name: file.destinationPath - }) + if (file.symlinkTargetPath !== undefined && !followSymlinks) { + // Add a symlink to the zip + zip.symlink(file.destinationPath, file.symlinkTargetPath) + } else { + // Add a normal file to the zip + zip.file(file.sourcePath, { + name: file.destinationPath + }) + } } else { // Add a directory to the zip zip.append('', {name: file.destinationPath}) diff --git a/packages/glob/package-lock.json b/packages/glob/package-lock.json index 17817543d9..665b11d534 100644 --- a/packages/glob/package-lock.json +++ b/packages/glob/package-lock.json @@ -21,7 +21,7 @@ "packages": { "": { "name": "@actions/glob", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { "@actions/core": "^1.9.1", diff --git a/packages/http-client/package-lock.json b/packages/http-client/package-lock.json index e58eb440a7..553ea2d588 100644 --- a/packages/http-client/package-lock.json +++ b/packages/http-client/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@actions/http-client", - "version": "2.2.1", + "version": "2.2.2", "license": "MIT", "dependencies": { "tunnel": "^0.0.6",