From cb6558bb10fe4afe4054d0be4b3136e673eb5e7f Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Thu, 15 Aug 2024 16:22:09 -0400 Subject: [PATCH] Exclude hidden files by default --- README.md | 5 +++ __tests__/search.test.ts | 54 +++++++++++++++++++++++++++++++ action.yml | 5 +++ dist/merge/index.js | 16 +++++---- dist/upload/index.js | 16 +++++---- docs/MIGRATION.md | 61 ++++++++++++++++++++++++++++------- merge/action.yml | 5 +++ package-lock.json | 4 +-- package.json | 2 +- src/merge/constants.ts | 3 +- src/merge/input-helper.ts | 4 ++- src/merge/merge-artifacts.ts | 2 +- src/merge/merge-inputs.ts | 5 +++ src/shared/search.ts | 9 +++--- src/upload/constants.ts | 3 +- src/upload/input-helper.ts | 4 ++- src/upload/upload-artifact.ts | 2 +- src/upload/upload-inputs.ts | 5 +++ 18 files changed, 169 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index a4f4c7f6..81436685 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ There is also a new sub-action, `actions/upload-artifact/merge`. For more info, Due to how Artifacts are created in this new version, it is no longer possible to upload to the same named Artifact multiple times. You must either split the uploads into multiple Artifacts with different names, or only upload once. Otherwise you _will_ encounter an error. 3. Limit of Artifacts for an individual job. Each job in a workflow run now has a limit of 500 artifacts. +4. With `v4.4` and later, hidden files are excluded by default. For assistance with breaking changes, see [MIGRATION.md](docs/MIGRATION.md). @@ -107,6 +108,10 @@ For assistance with breaking changes, see [MIGRATION.md](docs/MIGRATION.md). # Does not fail if the artifact does not exist. # Optional. Default is 'false' overwrite: + + # Whether to include hidden files in the provided path in the artifact + # Optional. Default is 'false' + include-hidden-files: ``` ### Outputs diff --git a/__tests__/search.test.ts b/__tests__/search.test.ts index e0ab26df..5cbc0325 100644 --- a/__tests__/search.test.ts +++ b/__tests__/search.test.ts @@ -61,6 +61,20 @@ const lonelyFilePath = path.join( 'lonely-file.txt' ) +const fileInHiddenFolderPath = path.join( + root, + '.hidden-folder', + 'folder-in-hidden-folder', + 'file.txt' +) +const hiddenFile = path.join(root, '.hidden-file.txt') +const fileInHiddenFolderInFolderA = path.join( + root, + 'folder-a', + '.hidden-folder-in-folder-a', + 'file.txt' +) + describe('Search', () => { beforeAll(async () => { // mock all output so that there is less noise when running tests @@ -93,6 +107,14 @@ describe('Search', () => { recursive: true }) + await fs.mkdir( + path.join(root, '.hidden-folder', 'folder-in-hidden-folder'), + {recursive: true} + ) + await fs.mkdir(path.join(root, 'folder-a', '.hidden-folder-in-folder-a'), { + recursive: true + }) + await fs.writeFile(searchItem1Path, 'search item1 file') await fs.writeFile(searchItem2Path, 'search item2 file') await fs.writeFile(searchItem3Path, 'search item3 file') @@ -113,7 +135,12 @@ describe('Search', () => { /* Directory structure of files that get created: root/ + .hidden-folder/ + folder-in-hidden-folder/ + file.txt folder-a/ + .hidden-folder-in-folder-a/ + file.txt folder-b/ folder-c/ search-item1.txt @@ -136,6 +163,7 @@ describe('Search', () => { folder-j/ folder-k/ lonely-file.txt + .hidden-file.txt search-item5.txt */ }) @@ -352,4 +380,30 @@ describe('Search', () => { ) expect(searchResult.filesToUpload.includes(lonelyFilePath)).toEqual(true) }) + + it('Hidden files ignored by default', async () => { + const searchPath = path.join(root, '**/*') + const searchResult = await findFilesToUpload(searchPath) + + expect(searchResult.filesToUpload.includes(hiddenFile)).toEqual(false) + expect(searchResult.filesToUpload.includes(fileInHiddenFolderPath)).toEqual( + false + ) + expect( + searchResult.filesToUpload.includes(fileInHiddenFolderInFolderA) + ).toEqual(false) + }) + + it('Hidden files included', async () => { + const searchPath = path.join(root, '**/*') + const searchResult = await findFilesToUpload(searchPath, true) + + expect(searchResult.filesToUpload.includes(hiddenFile)).toEqual(false) + expect(searchResult.filesToUpload.includes(fileInHiddenFolderPath)).toEqual( + false + ) + expect( + searchResult.filesToUpload.includes(fileInHiddenFolderInFolderA) + ).toEqual(false) + }) }) diff --git a/action.yml b/action.yml index 38d4fdcd..d3eb907d 100644 --- a/action.yml +++ b/action.yml @@ -40,6 +40,11 @@ inputs: If false, the action will fail if an artifact for the given name already exists. Does not fail if the artifact does not exist. default: 'false' + include-hidden-files: + description: > + If true, hidden files will be included in the merged artifact. + If false, hidden files will be excluded from the merged artifact. + default: 'false' outputs: artifact-id: diff --git a/dist/merge/index.js b/dist/merge/index.js index c09bb45e..699ddacd 100644 --- a/dist/merge/index.js +++ b/dist/merge/index.js @@ -125727,6 +125727,7 @@ var Inputs; Inputs["RetentionDays"] = "retention-days"; Inputs["CompressionLevel"] = "compression-level"; Inputs["DeleteMerged"] = "delete-merged"; + Inputs["IncludeHiddenFiles"] = "include-hidden-files"; })(Inputs = exports.Inputs || (exports.Inputs = {})); @@ -125810,13 +125811,15 @@ function getInputs() { const pattern = core.getInput(constants_1.Inputs.Pattern, { required: true }); const separateDirectories = core.getBooleanInput(constants_1.Inputs.SeparateDirectories); const deleteMerged = core.getBooleanInput(constants_1.Inputs.DeleteMerged); + const includeHiddenFiles = core.getBooleanInput(constants_1.Inputs.IncludeHiddenFiles); const inputs = { name, pattern, separateDirectories, deleteMerged, retentionDays: 0, - compressionLevel: 6 + compressionLevel: 6, + includeHiddenFiles, }; const retentionDaysStr = core.getInput(constants_1.Inputs.RetentionDays); if (retentionDaysStr) { @@ -125932,7 +125935,7 @@ function run() { if (typeof inputs.compressionLevel !== 'undefined') { options.compressionLevel = inputs.compressionLevel; } - const searchResult = yield (0, search_1.findFilesToUpload)(tmpDir); + const searchResult = yield (0, search_1.findFilesToUpload)(tmpDir, inputs.includeHiddenFiles); yield (0, upload_artifact_1.uploadArtifact)(inputs.name, searchResult.filesToUpload, searchResult.rootDirectory, options); core.info(`The ${artifacts.length} artifact(s) have been successfully merged!`); if (inputs.deleteMerged) { @@ -125999,11 +126002,12 @@ const fs_1 = __nccwpck_require__(57147); const path_1 = __nccwpck_require__(71017); const util_1 = __nccwpck_require__(73837); const stats = (0, util_1.promisify)(fs_1.stat); -function getDefaultGlobOptions() { +function getDefaultGlobOptions(_includeHiddenFiles) { return { followSymbolicLinks: true, implicitDescendants: true, - omitBrokenSymbolicLinks: true + omitBrokenSymbolicLinks: true, + // excludeHiddenFiles: !includeHiddenFiles, }; } /** @@ -126057,10 +126061,10 @@ function getMultiPathLCA(searchPaths) { } return path.join(...commonPaths); } -function findFilesToUpload(searchPath, globOptions) { +function findFilesToUpload(searchPath, includeHiddenFiles) { return __awaiter(this, void 0, void 0, function* () { const searchResults = []; - const globber = yield glob.create(searchPath, globOptions || getDefaultGlobOptions()); + const globber = yield glob.create(searchPath, getDefaultGlobOptions(includeHiddenFiles || false)); const rawSearchResults = yield globber.glob(); /* Files are saved with case insensitivity. Uploading both a.txt and A.txt will files to be overwritten diff --git a/dist/upload/index.js b/dist/upload/index.js index b28794fe..d3b87434 100644 --- a/dist/upload/index.js +++ b/dist/upload/index.js @@ -125757,11 +125757,12 @@ const fs_1 = __nccwpck_require__(57147); const path_1 = __nccwpck_require__(71017); const util_1 = __nccwpck_require__(73837); const stats = (0, util_1.promisify)(fs_1.stat); -function getDefaultGlobOptions() { +function getDefaultGlobOptions(_includeHiddenFiles) { return { followSymbolicLinks: true, implicitDescendants: true, - omitBrokenSymbolicLinks: true + omitBrokenSymbolicLinks: true, + // excludeHiddenFiles: !includeHiddenFiles, }; } /** @@ -125815,10 +125816,10 @@ function getMultiPathLCA(searchPaths) { } return path.join(...commonPaths); } -function findFilesToUpload(searchPath, globOptions) { +function findFilesToUpload(searchPath, includeHiddenFiles) { return __awaiter(this, void 0, void 0, function* () { const searchResults = []; - const globber = yield glob.create(searchPath, globOptions || getDefaultGlobOptions()); + const globber = yield glob.create(searchPath, getDefaultGlobOptions(includeHiddenFiles || false)); const rawSearchResults = yield globber.glob(); /* Files are saved with case insensitivity. Uploading both a.txt and A.txt will files to be overwritten @@ -125956,6 +125957,7 @@ var Inputs; Inputs["RetentionDays"] = "retention-days"; Inputs["CompressionLevel"] = "compression-level"; Inputs["Overwrite"] = "overwrite"; + Inputs["IncludeHiddenFiles"] = "include-hidden-files"; })(Inputs = exports.Inputs || (exports.Inputs = {})); var NoFileOptions; (function (NoFileOptions) { @@ -126053,6 +126055,7 @@ function getInputs() { const name = core.getInput(constants_1.Inputs.Name); const path = core.getInput(constants_1.Inputs.Path, { required: true }); const overwrite = core.getBooleanInput(constants_1.Inputs.Overwrite); + const includeHiddenFiles = core.getBooleanInput(constants_1.Inputs.IncludeHiddenFiles); const ifNoFilesFound = core.getInput(constants_1.Inputs.IfNoFilesFound); const noFileBehavior = constants_1.NoFileOptions[ifNoFilesFound]; if (!noFileBehavior) { @@ -126062,7 +126065,8 @@ function getInputs() { artifactName: name, searchPath: path, ifNoFilesFound: noFileBehavior, - overwrite: overwrite + overwrite: overwrite, + includeHiddenFiles: includeHiddenFiles, }; const retentionDaysStr = core.getInput(constants_1.Inputs.RetentionDays); if (retentionDaysStr) { @@ -126151,7 +126155,7 @@ function deleteArtifactIfExists(artifactName) { function run() { return __awaiter(this, void 0, void 0, function* () { const inputs = (0, input_helper_1.getInputs)(); - const searchResult = yield (0, search_1.findFilesToUpload)(inputs.searchPath); + const searchResult = yield (0, search_1.findFilesToUpload)(inputs.searchPath, inputs.includeHiddenFiles); 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/docs/MIGRATION.md b/docs/MIGRATION.md index 1c656fca..55ddd010 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -4,6 +4,7 @@ - [Multiple uploads to the same named Artifact](#multiple-uploads-to-the-same-named-artifact) - [Overwriting an Artifact](#overwriting-an-artifact) - [Merging multiple artifacts](#merging-multiple-artifacts) + - [Hidden files](#hidden-files) Several behavioral differences exist between Artifact actions `v3` and below vs `v4`. This document outlines common scenarios in `v3`, and how they would be handled in `v4`. @@ -189,21 +190,59 @@ jobs: - name: Create a File run: echo "hello from ${{ matrix.runs-on }}" > file-${{ matrix.runs-on }}.txt - name: Upload Artifact -- uses: actions/upload-artifact@v3 -+ uses: actions/upload-artifact@v4 +- uses: actions/upload-artifact@v3 ++ uses: actions/upload-artifact@v4 with: - name: all-my-files + name: my-artifact-${{ matrix.runs-on }} path: file-${{ matrix.runs-on }}.txt -+ merge: -+ runs-on: ubuntu-latest -+ needs: upload -+ steps: -+ - name: Merge Artifacts -+ uses: actions/upload-artifact/merge@v4 -+ with: -+ name: all-my-files -+ pattern: my-artifact-* ++ merge: ++ runs-on: ubuntu-latest ++ needs: upload ++ steps: ++ - name: Merge Artifacts ++ uses: actions/upload-artifact/merge@v4 ++ with: ++ name: all-my-files ++ pattern: my-artifact-* ``` Note that this will download all artifacts to a temporary directory and reupload them as a single artifact. For more information on inputs and other use cases for `actions/upload-artifact/merge@v4`, see [the action documentation](../merge/README.md). + +## Hidden Files + +By default, hidden files are ignored by this action to avoid unintentionally uploading sensitive +information. + +In versions of this action before v4.4.0, these hidden files were included by default. + +If you need to upload hidden files, you can use the `include-hidden-files` input. + +```yaml +jobs: + upload: + runs-on: ubuntu-latest + steps: + - name: Create a Hidden File + run: echo "hello from a hidden file" > .hidden-file.txt + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + path: .hidden-file.txt +``` + + +```diff +jobs: + upload: + runs-on: ubuntu-latest + steps: + - name: Create a Hidden File + run: echo "hello from a hidden file" > .hidden-file.txt + - name: Upload Artifact +- uses: actions/upload-artifact@v3 ++ uses: actions/upload-artifact@v4 + with: + path: .hidden-file.txt ++ include-hidden-files: true +``` \ No newline at end of file diff --git a/merge/action.yml b/merge/action.yml index 8d85864a..81407eea 100644 --- a/merge/action.yml +++ b/merge/action.yml @@ -36,6 +36,11 @@ inputs: If true, the artifacts that were merged will be deleted. If false, the artifacts will still exist. default: 'false' + include-hidden-files: + description: > + If true, hidden files will be included in the merged artifact. + If false, hidden files will be excluded from the merged artifact. + default: 'false' outputs: artifact-id: diff --git a/package-lock.json b/package-lock.json index 296b9f18..e1c4ca9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "upload-artifact", - "version": "4.3.6", + "version": "4.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "upload-artifact", - "version": "4.3.6", + "version": "4.4.0", "license": "MIT", "dependencies": { "@actions/artifact": "2.1.8", diff --git a/package.json b/package.json index 8f51092a..7219abe1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "upload-artifact", - "version": "4.3.6", + "version": "4.4.0", "description": "Upload an Actions Artifact in a workflow run", "main": "dist/upload/index.js", "scripts": { diff --git a/src/merge/constants.ts b/src/merge/constants.ts index 8bc9539e..7869ab1a 100644 --- a/src/merge/constants.ts +++ b/src/merge/constants.ts @@ -5,5 +5,6 @@ export enum Inputs { SeparateDirectories = 'separate-directories', RetentionDays = 'retention-days', CompressionLevel = 'compression-level', - DeleteMerged = 'delete-merged' + DeleteMerged = 'delete-merged', + IncludeHiddenFiles = 'include-hidden-files', } diff --git a/src/merge/input-helper.ts b/src/merge/input-helper.ts index de53a2fd..cc26a966 100644 --- a/src/merge/input-helper.ts +++ b/src/merge/input-helper.ts @@ -10,6 +10,7 @@ export function getInputs(): MergeInputs { const pattern = core.getInput(Inputs.Pattern, {required: true}) const separateDirectories = core.getBooleanInput(Inputs.SeparateDirectories) const deleteMerged = core.getBooleanInput(Inputs.DeleteMerged) + const includeHiddenFiles = core.getBooleanInput(Inputs.IncludeHiddenFiles) const inputs = { name, @@ -17,7 +18,8 @@ export function getInputs(): MergeInputs { separateDirectories, deleteMerged, retentionDays: 0, - compressionLevel: 6 + compressionLevel: 6, + includeHiddenFiles, } as MergeInputs const retentionDaysStr = core.getInput(Inputs.RetentionDays) diff --git a/src/merge/merge-artifacts.ts b/src/merge/merge-artifacts.ts index b45ef9c6..8562e869 100644 --- a/src/merge/merge-artifacts.ts +++ b/src/merge/merge-artifacts.ts @@ -62,7 +62,7 @@ export async function run(): Promise { options.compressionLevel = inputs.compressionLevel } - const searchResult = await findFilesToUpload(tmpDir) + const searchResult = await findFilesToUpload(tmpDir, inputs.includeHiddenFiles) await uploadArtifact( inputs.name, diff --git a/src/merge/merge-inputs.ts b/src/merge/merge-inputs.ts index def507ae..adfdff67 100644 --- a/src/merge/merge-inputs.ts +++ b/src/merge/merge-inputs.ts @@ -30,4 +30,9 @@ export interface MergeInputs { * If false, the artifacts will be merged into the root of the destination. */ separateDirectories: boolean + + /** + * Whether or not to include hidden files in the artifact + */ + includeHiddenFiles: boolean } diff --git a/src/shared/search.ts b/src/shared/search.ts index bd801648..465507f0 100644 --- a/src/shared/search.ts +++ b/src/shared/search.ts @@ -11,11 +11,12 @@ export interface SearchResult { rootDirectory: string } -function getDefaultGlobOptions(): glob.GlobOptions { +function getDefaultGlobOptions(_includeHiddenFiles: boolean): glob.GlobOptions { return { followSymbolicLinks: true, implicitDescendants: true, - omitBrokenSymbolicLinks: true + omitBrokenSymbolicLinks: true, + // excludeHiddenFiles: !includeHiddenFiles, } } @@ -80,12 +81,12 @@ function getMultiPathLCA(searchPaths: string[]): string { export async function findFilesToUpload( searchPath: string, - globOptions?: glob.GlobOptions + includeHiddenFiles?: boolean, ): Promise { const searchResults: string[] = [] const globber = await glob.create( searchPath, - globOptions || getDefaultGlobOptions() + getDefaultGlobOptions(includeHiddenFiles || false) ) const rawSearchResults: string[] = await globber.glob() diff --git a/src/upload/constants.ts b/src/upload/constants.ts index 272f8428..2c281ded 100644 --- a/src/upload/constants.ts +++ b/src/upload/constants.ts @@ -5,7 +5,8 @@ export enum Inputs { IfNoFilesFound = 'if-no-files-found', RetentionDays = 'retention-days', CompressionLevel = 'compression-level', - Overwrite = 'overwrite' + Overwrite = 'overwrite', + IncludeHiddenFiles = 'include-hidden-files', } export enum NoFileOptions { diff --git a/src/upload/input-helper.ts b/src/upload/input-helper.ts index 3e24f25c..f1912f1c 100644 --- a/src/upload/input-helper.ts +++ b/src/upload/input-helper.ts @@ -9,6 +9,7 @@ export function getInputs(): UploadInputs { const name = core.getInput(Inputs.Name) const path = core.getInput(Inputs.Path, {required: true}) const overwrite = core.getBooleanInput(Inputs.Overwrite) + const includeHiddenFiles = core.getBooleanInput(Inputs.IncludeHiddenFiles) const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound) const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound] @@ -27,7 +28,8 @@ export function getInputs(): UploadInputs { artifactName: name, searchPath: path, ifNoFilesFound: noFileBehavior, - overwrite: overwrite + overwrite: overwrite, + includeHiddenFiles: includeHiddenFiles, } as UploadInputs const retentionDaysStr = core.getInput(Inputs.RetentionDays) diff --git a/src/upload/upload-artifact.ts b/src/upload/upload-artifact.ts index 8c775437..c6368bfc 100644 --- a/src/upload/upload-artifact.ts +++ b/src/upload/upload-artifact.ts @@ -24,7 +24,7 @@ async function deleteArtifactIfExists(artifactName: string): Promise { export async function run(): Promise { const inputs = getInputs() - const searchResult = await findFilesToUpload(inputs.searchPath) + const searchResult = await findFilesToUpload(inputs.searchPath, inputs.includeHiddenFiles) 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/upload-inputs.ts b/src/upload/upload-inputs.ts index 1e7a46f3..9d680d3a 100644 --- a/src/upload/upload-inputs.ts +++ b/src/upload/upload-inputs.ts @@ -30,4 +30,9 @@ export interface UploadInputs { * Whether or not to replace an existing artifact with the same name */ overwrite: boolean + + /** + * Whether or not to include hidden files in the artifact + */ + includeHiddenFiles: boolean }