diff --git a/__tests__/search.test.ts b/__tests__/search.test.ts index cbf54ac..1e3309d 100644 --- a/__tests__/search.test.ts +++ b/__tests__/search.test.ts @@ -2,7 +2,7 @@ import * as core from '@actions/core' import * as path from 'path' import * as io from '@actions/io' import {promises as fs} from 'fs' -import {findFilesToUpload} from '../src/search' +import {findFilesToUpload, getDefaultGlobOptions} from '../src/search' const root = path.join(__dirname, '_temp', 'search') const searchItem1Path = path.join( @@ -110,6 +110,12 @@ describe('Search', () => { await fs.writeFile(amazingFileInFolderHPath, 'amazing file') await fs.writeFile(lonelyFilePath, 'all by itself') + + await fs.symlink( + path.join(root, 'folder-d'), + path.join(root, 'symlink-to-folder-d') + ) + /* Directory structure of files that get created: root/ @@ -136,6 +142,7 @@ describe('Search', () => { folder-j/ folder-k/ lonely-file.txt + symlink-to-folder-d/ -> ./folder-d/ search-item5.txt */ }) @@ -227,7 +234,8 @@ describe('Search', () => { it('Wildcard search - Absolute Path', async () => { const searchPath = path.join(root, '**/*[Ss]earch*') const searchResult = await findFilesToUpload(searchPath) - expect(searchResult.filesToUpload.length).toEqual(10) + // folder-d items included twice because symlink is followed by default + expect(searchResult.filesToUpload.length).toEqual(14) expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true) expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true) @@ -261,7 +269,8 @@ describe('Search', () => { '**/*[Ss]earch*' ) const searchResult = await findFilesToUpload(searchPath) - expect(searchResult.filesToUpload.length).toEqual(10) + // folder-d items included twice because symlink is followed by default + expect(searchResult.filesToUpload.length).toEqual(14) expect(searchResult.filesToUpload.includes(searchItem1Path)).toEqual(true) expect(searchResult.filesToUpload.includes(searchItem2Path)).toEqual(true) @@ -352,4 +361,15 @@ describe('Search', () => { ) expect(searchResult.filesToUpload.includes(lonelyFilePath)).toEqual(true) }) + + it('Declines to follow symlinks when requested', async () => { + const searchPath = path.join(root, 'symlink-to-folder-d') + const globOptions = { + ...getDefaultGlobOptions(), + followSymbolicLinks: false + } + + const searchResult = await findFilesToUpload(searchPath, globOptions) + expect(searchResult.filesToUpload.length).toEqual(1) + }) }) diff --git a/action.yml b/action.yml index 2003cdd..becfae4 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,12 @@ inputs: Minimum 1 day. Maximum 90 days unless changed from the repository settings page. + follow-symlinks: + description: > + Whether symbolic links should be followed and expanded when building the set of files to be + archived (true), or if symbolic links should be included in the archived artifact verbatim + (false). + default: true runs: using: 'node12' main: 'dist/index.js' diff --git a/src/constants.ts b/src/constants.ts index 894ff4c..266b3ee 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,7 +2,8 @@ export enum Inputs { Name = 'name', Path = 'path', IfNoFilesFound = 'if-no-files-found', - RetentionDays = 'retention-days' + RetentionDays = 'retention-days', + FollowSymlinks = 'follow-symlinks' } export enum NoFileOptions { diff --git a/src/input-helper.ts b/src/input-helper.ts index 8344823..81359c8 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -10,6 +10,11 @@ export function getInputs(): UploadInputs { const path = core.getInput(Inputs.Path, {required: true}) const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound) + + // getBooleanInput is not released yet :( + const followSymlinks = + core.getInput(Inputs.FollowSymlinks).toLowerCase() == 'true' + const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound] if (!noFileBehavior) { @@ -25,7 +30,8 @@ export function getInputs(): UploadInputs { const inputs = { artifactName: name, searchPath: path, - ifNoFilesFound: noFileBehavior + ifNoFilesFound: noFileBehavior, + followSymlinks } as UploadInputs const retentionDaysStr = core.getInput(Inputs.RetentionDays) diff --git a/src/search.ts b/src/search.ts index bd80164..9038b86 100644 --- a/src/search.ts +++ b/src/search.ts @@ -1,7 +1,7 @@ import * as glob from '@actions/glob' import * as path from 'path' import {debug, info} from '@actions/core' -import {stat} from 'fs' +import {promises as fsPromises, stat} from 'fs' import {dirname} from 'path' import {promisify} from 'util' const stats = promisify(stat) @@ -11,7 +11,7 @@ export interface SearchResult { rootDirectory: string } -function getDefaultGlobOptions(): glob.GlobOptions { +export function getDefaultGlobOptions(): glob.GlobOptions { return { followSymbolicLinks: true, implicitDescendants: true, @@ -83,10 +83,8 @@ export async function findFilesToUpload( globOptions?: glob.GlobOptions ): Promise { const searchResults: string[] = [] - const globber = await glob.create( - searchPath, - globOptions || getDefaultGlobOptions() - ) + const resolvedGlobOptions = globOptions || getDefaultGlobOptions() + const globber = await glob.create(searchPath, resolvedGlobOptions) const rawSearchResults: string[] = await globber.glob() /* @@ -100,8 +98,12 @@ export async function findFilesToUpload( directories so filter any directories out from the raw search results */ for (const searchResult of rawSearchResults) { - const fileStats = await stats(searchResult) - // isDirectory() returns false for symlinks if using fs.lstat(), make sure to use fs.stat() instead + /* isDirectory() returns false for symlinks if using fs.lstat(), make sure to use fs.stat() instead + * if we're following symlinks so that stat follows the symlink too */ + const fileStats = resolvedGlobOptions.followSymbolicLinks + ? await stats(searchResult) + : await fsPromises.lstat(searchResult) + if (!fileStats.isDirectory()) { debug(`File:${searchResult} was found using the provided searchPath`) searchResults.push(searchResult) diff --git a/src/upload-artifact.ts b/src/upload-artifact.ts index 3add259..c008298 100644 --- a/src/upload-artifact.ts +++ b/src/upload-artifact.ts @@ -1,13 +1,18 @@ import * as core from '@actions/core' import {create, UploadOptions} from '@actions/artifact' -import {findFilesToUpload} from './search' +import {findFilesToUpload, getDefaultGlobOptions} from './search' import {getInputs} from './input-helper' import {NoFileOptions} from './constants' async function run(): Promise { try { const inputs = getInputs() - const searchResult = await findFilesToUpload(inputs.searchPath) + const globOptions = { + ...getDefaultGlobOptions(), + followSymbolicLinks: inputs.followSymlinks + } + const searchResult = await findFilesToUpload(inputs.searchPath, globOptions) + if (searchResult.filesToUpload.length === 0) { // No files were found, different use cases warrant different types of behavior if nothing is found switch (inputs.ifNoFilesFound) { diff --git a/src/upload-inputs.ts b/src/upload-inputs.ts index 37325df..a735d70 100644 --- a/src/upload-inputs.ts +++ b/src/upload-inputs.ts @@ -20,4 +20,11 @@ export interface UploadInputs { * Duration after which artifact will expire in days */ retentionDays: number + + /** + * Whether symbolic links should be followed and expanded when building the set of files to be + * archived (true), or if symbolic links should be included in the archived artifact verbatim + * (false). + */ + followSymlinks: boolean }