diff --git a/README.md b/README.md index 32fe99ad..fd2f1617 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,14 @@ steps: !path/**/*.tmp ``` +### Upload multiple artifacts using a JSON string +```yaml +- uses: actions/upload-artifact@v2 + with: + name: '["my-artifact", "my-artifact-2"]' + path: '["path/to/artifact/1/", "path/to/artifact/2/"]' +``` + For supported wildcards along with behavior and documentation, see [@actions/glob](https://github.com/actions/toolkit/tree/main/packages/glob) which is used internally to search for files. If a wildcard pattern is used, the path hierarchy will be preserved after the first wildcard pattern. diff --git a/action.yml b/action.yml index 2003cddc..2733633a 100644 --- a/action.yml +++ b/action.yml @@ -3,8 +3,7 @@ description: 'Upload a build artifact that can be used by subsequent workflow st author: 'GitHub' inputs: name: - description: 'Artifact name' - default: 'artifact' + description: 'Artifact name, default is "artifact"' path: description: 'A file, directory or wildcard pattern that describes what to upload' required: true diff --git a/dist/index.js b/dist/index.js index 343b4bfd..ec176089 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4028,44 +4028,49 @@ function run() { return __awaiter(this, void 0, void 0, function* () { try { const inputs = input_helper_1.getInputs(); - const searchResult = yield search_1.findFilesToUpload(inputs.searchPath); - if (searchResult.filesToUpload.length === 0) { - // No files were found, different use cases warrant different types of behavior if nothing is found - switch (inputs.ifNoFilesFound) { - case constants_1.NoFileOptions.warn: { - core.warning(`No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.`); - break; - } - case constants_1.NoFileOptions.error: { - core.setFailed(`No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.`); - break; - } - case constants_1.NoFileOptions.ignore: { - core.info(`No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.`); - break; + for (let i = 0; i < inputs.searchPath.length; i++) { + const searchPath = inputs.searchPath[i]; + const artifactName = inputs.artifactName[i]; + const retentionDays = inputs.retentionDays[i]; + const searchResult = yield search_1.findFilesToUpload(searchPath); + if (searchResult.filesToUpload.length === 0) { + // No files were found, different use cases warrant different types of behavior if nothing is found + switch (inputs.ifNoFilesFound) { + case constants_1.NoFileOptions.warn: { + core.warning(`No files were found with the provided path: ${searchPath}. No artifacts will be uploaded.`); + break; + } + case constants_1.NoFileOptions.error: { + core.setFailed(`No files were found with the provided path: ${searchPath}. No artifacts will be uploaded.`); + break; + } + case constants_1.NoFileOptions.ignore: { + core.info(`No files were found with the provided path: ${searchPath}. No artifacts will be uploaded.`); + break; + } } } - } - else { - const s = searchResult.filesToUpload.length === 1 ? '' : 's'; - core.info(`With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded`); - core.debug(`Root artifact directory is ${searchResult.rootDirectory}`); - if (searchResult.filesToUpload.length > 10000) { - core.warning(`There are over 10,000 files in this artifact, consider create an archive before upload to improve the upload performance.`); - } - const artifactClient = artifact_1.create(); - const options = { - continueOnError: false - }; - if (inputs.retentionDays) { - options.retentionDays = inputs.retentionDays; - } - const uploadResponse = yield artifactClient.uploadArtifact(inputs.artifactName, searchResult.filesToUpload, searchResult.rootDirectory, options); - if (uploadResponse.failedItems.length > 0) { - core.setFailed(`An error was encountered when uploading ${uploadResponse.artifactName}. There were ${uploadResponse.failedItems.length} items that failed to upload.`); - } else { - core.info(`Artifact ${uploadResponse.artifactName} has been successfully uploaded!`); + const s = searchResult.filesToUpload.length === 1 ? '' : 's'; + core.info(`With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded`); + core.debug(`Root artifact directory is ${searchResult.rootDirectory}`); + if (searchResult.filesToUpload.length > 10000) { + core.warning(`There are over 10,000 files in this artifact, consider create an archive before upload to improve the upload performance.`); + } + const artifactClient = artifact_1.create(); + const options = { + continueOnError: false + }; + if (retentionDays) { + options.retentionDays = retentionDays; + } + const uploadResponse = yield artifactClient.uploadArtifact(artifactName, searchResult.filesToUpload, searchResult.rootDirectory, options); + if (uploadResponse.failedItems.length > 0) { + core.setFailed(`An error was encountered when uploading ${uploadResponse.artifactName}. There were ${uploadResponse.failedItems.length} items that failed to upload.`); + } + else { + core.info(`Artifact ${uploadResponse.artifactName} has been successfully uploaded!`); + } } } } @@ -6576,26 +6581,62 @@ const constants_1 = __webpack_require__(694); function getInputs() { const name = core.getInput(constants_1.Inputs.Name); const path = core.getInput(constants_1.Inputs.Path, { required: true }); + const searchPath = parseFromJSON(path) || [path]; + const defaultArtifactName = 'artifact'; + // Accepts an individual value or an array as input, if array sizes don't match, use default value instead + const artifactName = parseParamaterToArrayFromInput(name, searchPath.length, defaultArtifactName, (defaultInput, index) => { + const artifactIndexStr = index == 0 ? '' : `_${index + 1}`; + return `${defaultInput}${artifactIndexStr}`; + }); + // Accepts an individual value or an array as input + const retention = core.getInput(constants_1.Inputs.RetentionDays); + const retentionDays = parseParamaterToArrayFromInput(retention, searchPath.length, undefined, defaultInput => defaultInput).map(parseRetentionDays); const ifNoFilesFound = core.getInput(constants_1.Inputs.IfNoFilesFound); const noFileBehavior = constants_1.NoFileOptions[ifNoFilesFound]; if (!noFileBehavior) { core.setFailed(`Unrecognized ${constants_1.Inputs.IfNoFilesFound} input. Provided: ${ifNoFilesFound}. Available options: ${Object.keys(constants_1.NoFileOptions)}`); } const inputs = { - artifactName: name, - searchPath: path, + artifactName, + searchPath, + retentionDays, ifNoFilesFound: noFileBehavior }; - const retentionDaysStr = core.getInput(constants_1.Inputs.RetentionDays); - if (retentionDaysStr) { - inputs.retentionDays = parseInt(retentionDaysStr); - if (isNaN(inputs.retentionDays)) { + return inputs; +} +exports.getInputs = getInputs; +function parseParamaterToArrayFromInput(input, requiredLength, defaultInput, defaultFunc) { + // Accepts an individual value or an array as input, if array size doesn't match the required length, fill the rest with a default value + const inputArray = parseFromJSON(input || '[]'); + if (inputArray != null) { + // If a stringified JSON array is provided, use it and concat it with the default when required + return inputArray.concat(Array.from({ length: Math.max(0, requiredLength - inputArray.length) }, (_, index) => defaultFunc(defaultInput, index))); + } + // If a string is provided, fill the array with that value + return Array.from({ length: Math.max(0, requiredLength) }, (_, index) => defaultFunc(input || defaultInput, index)); +} +function parseFromJSON(jsonStr) { + try { + const json = JSON.parse(jsonStr); + if (Array.isArray(json)) { + return json; + } + } + catch (_err) { + // Input wasn't a stringified JSON array (string[]), return undefined to signal an invalid JSON was provided + } + return undefined; +} +function parseRetentionDays(retentionDaysStr) { + if (retentionDaysStr != null) { + const retentionDays = parseInt(retentionDaysStr); + if (isNaN(retentionDays)) { core.setFailed('Invalid retention-days'); } + return retentionDays; } - return inputs; + return undefined; } -exports.getInputs = getInputs; /***/ }), diff --git a/src/input-helper.ts b/src/input-helper.ts index 83448236..c770137e 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -9,6 +9,29 @@ export function getInputs(): UploadInputs { const name = core.getInput(Inputs.Name) const path = core.getInput(Inputs.Path, {required: true}) + const searchPath = parseFromJSON(path) || [path] + + const defaultArtifactName = 'artifact' + // Accepts an individual value or an array as input, if array sizes don't match, use default value instead + const artifactName = parseParamaterToArrayFromInput( + name, + searchPath.length, + defaultArtifactName, + (defaultInput, index) => { + const artifactIndexStr = index == 0 ? '' : `_${index + 1}` + return `${defaultInput}${artifactIndexStr}` + } + ) + + // Accepts an individual value or an array as input + const retention = core.getInput(Inputs.RetentionDays) + const retentionDays = parseParamaterToArrayFromInput( + retention, + searchPath.length, + undefined, + defaultInput => defaultInput + ).map(parseRetentionDays) + const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound) const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound] @@ -23,18 +46,62 @@ export function getInputs(): UploadInputs { } const inputs = { - artifactName: name, - searchPath: path, + artifactName, + searchPath, + retentionDays, ifNoFilesFound: noFileBehavior } as UploadInputs - const retentionDaysStr = core.getInput(Inputs.RetentionDays) - if (retentionDaysStr) { - inputs.retentionDays = parseInt(retentionDaysStr) - if (isNaN(inputs.retentionDays)) { - core.setFailed('Invalid retention-days') + return inputs +} + +function parseParamaterToArrayFromInput( + input: string | undefined, + requiredLength: number, + defaultInput: string | undefined, + defaultFunc: ( + defaultInput: string | undefined, + index: number + ) => string | undefined +): (string | undefined)[] { + // Accepts an individual value or an array as input, if array size doesn't match the required length, fill the rest with a default value + const inputArray = parseFromJSON(input || '[]') + if (inputArray != null) { + // If a stringified JSON array is provided, use it and concat it with the default when required + return (<(string | undefined)[]>inputArray).concat( + Array.from( + {length: Math.max(0, requiredLength - inputArray.length)}, + (_, index) => defaultFunc(defaultInput, index) + ) + ) + } + // If a string is provided, fill the array with that value + return Array.from({length: Math.max(0, requiredLength)}, (_, index) => + defaultFunc(input || defaultInput, index) + ) +} + +function parseFromJSON(jsonStr: string): string[] | undefined { + try { + const json = JSON.parse(jsonStr) + if (Array.isArray(json)) { + return json } + } catch (_err) { + // Input wasn't a stringified JSON array (string[]), return undefined to signal an invalid JSON was provided } + return undefined +} - return inputs +function parseRetentionDays( + retentionDaysStr: string | undefined +): number | undefined { + if (retentionDaysStr != null) { + const retentionDays = parseInt(retentionDaysStr) + if (isNaN(retentionDays)) { + core.setFailed('Invalid retention-days') + } + return retentionDays + } + return undefined } diff --git a/src/upload-artifact.ts b/src/upload-artifact.ts index 3add2597..5c4501be 100644 --- a/src/upload-artifact.ts +++ b/src/upload-artifact.ts @@ -7,65 +7,72 @@ import {NoFileOptions} from './constants' async function run(): Promise { try { const inputs = getInputs() - const searchResult = await findFilesToUpload(inputs.searchPath) - if (searchResult.filesToUpload.length === 0) { - // No files were found, different use cases warrant different types of behavior if nothing is found - switch (inputs.ifNoFilesFound) { - case NoFileOptions.warn: { + + for (let i = 0; i < inputs.searchPath.length; i++) { + const searchPath = inputs.searchPath[i] + const artifactName = inputs.artifactName[i] + const retentionDays = inputs.retentionDays[i] + + const searchResult = await findFilesToUpload(searchPath) + if (searchResult.filesToUpload.length === 0) { + // No files were found, different use cases warrant different types of behavior if nothing is found + switch (inputs.ifNoFilesFound) { + case NoFileOptions.warn: { + core.warning( + `No files were found with the provided path: ${searchPath}. No artifacts will be uploaded.` + ) + break + } + case NoFileOptions.error: { + core.setFailed( + `No files were found with the provided path: ${searchPath}. No artifacts will be uploaded.` + ) + break + } + case NoFileOptions.ignore: { + core.info( + `No files were found with the provided path: ${searchPath}. No artifacts will be uploaded.` + ) + break + } + } + } else { + const s = searchResult.filesToUpload.length === 1 ? '' : 's' + core.info( + `With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded` + ) + core.debug(`Root artifact directory is ${searchResult.rootDirectory}`) + + if (searchResult.filesToUpload.length > 10000) { core.warning( - `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` + `There are over 10,000 files in this artifact, consider create an archive before upload to improve the upload performance.` ) - break } - case NoFileOptions.error: { - core.setFailed( - `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` - ) - break + + const artifactClient = create() + const options: UploadOptions = { + continueOnError: false } - case NoFileOptions.ignore: { - core.info( - `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` - ) - break + if (retentionDays) { + options.retentionDays = retentionDays } - } - } else { - const s = searchResult.filesToUpload.length === 1 ? '' : 's' - core.info( - `With the provided path, there will be ${searchResult.filesToUpload.length} file${s} uploaded` - ) - core.debug(`Root artifact directory is ${searchResult.rootDirectory}`) - if (searchResult.filesToUpload.length > 10000) { - core.warning( - `There are over 10,000 files in this artifact, consider create an archive before upload to improve the upload performance.` + const uploadResponse = await artifactClient.uploadArtifact( + artifactName, + searchResult.filesToUpload, + searchResult.rootDirectory, + options ) - } - const artifactClient = create() - const options: UploadOptions = { - continueOnError: false - } - if (inputs.retentionDays) { - options.retentionDays = inputs.retentionDays - } - - const uploadResponse = await artifactClient.uploadArtifact( - inputs.artifactName, - searchResult.filesToUpload, - searchResult.rootDirectory, - options - ) - - if (uploadResponse.failedItems.length > 0) { - core.setFailed( - `An error was encountered when uploading ${uploadResponse.artifactName}. There were ${uploadResponse.failedItems.length} items that failed to upload.` - ) - } else { - core.info( - `Artifact ${uploadResponse.artifactName} has been successfully uploaded!` - ) + if (uploadResponse.failedItems.length > 0) { + core.setFailed( + `An error was encountered when uploading ${uploadResponse.artifactName}. There were ${uploadResponse.failedItems.length} items that failed to upload.` + ) + } else { + core.info( + `Artifact ${uploadResponse.artifactName} has been successfully uploaded!` + ) + } } } } catch (err) { diff --git a/src/upload-inputs.ts b/src/upload-inputs.ts index 37325df3..4f0e0af2 100644 --- a/src/upload-inputs.ts +++ b/src/upload-inputs.ts @@ -4,12 +4,12 @@ export interface UploadInputs { /** * The name of the artifact that will be uploaded */ - artifactName: string + artifactName: string[] /** * The search path used to describe what to upload as part of the artifact */ - searchPath: string + searchPath: string[] /** * The desired behavior if no files are found with the provided search path @@ -19,5 +19,5 @@ export interface UploadInputs { /** * Duration after which artifact will expire in days */ - retentionDays: number + retentionDays: (number | undefined)[] }