From 0b90c2eee6c2b66775e5fd9daef43da324e58528 Mon Sep 17 00:00:00 2001 From: BToersche Date: Wed, 5 May 2021 00:43:01 +0200 Subject: [PATCH 1/2] Added support for uploading multiple artifacts --- dist/index.js | 114 ++++++++++++++++++++++++--------------- src/input-helper.ts | 50 +++++++++++++++--- src/upload-artifact.ts | 117 ++++++++++++++++++++++------------------- src/upload-inputs.ts | 6 +-- 4 files changed, 178 insertions(+), 109 deletions(-) diff --git a/dist/index.js b/dist/index.js index 343b4bf..d2d85be 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; + 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; + } } - 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; - } - } - } - 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,49 @@ 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 = Array.isArray(path) ? 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 = Array.isArray(name) + ? name.concat(new Array(Math.max(0, searchPath.length - name.length)).fill(defaultArtifactName)) + : new Array(searchPath.length).fill(name || defaultArtifactName); 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, ifNoFilesFound: noFileBehavior }; - const retentionDaysStr = core.getInput(constants_1.Inputs.RetentionDays); - if (retentionDaysStr) { - inputs.retentionDays = parseInt(retentionDaysStr); - if (isNaN(inputs.retentionDays)) { - core.setFailed('Invalid retention-days'); - } + // Accepts an individual value or an array as input + const retentionDays = core.getInput(constants_1.Inputs.RetentionDays); + if (Array.isArray(retentionDays)) { + // If array sizes don't match, use default value instead + inputs.retentionDays = retentionDays + .map(parseRetentionDays) + .concat(new Array(Math.max(0, searchPath.length - retentionDays.length)).fill(undefined)); + } + else { + const retention = parseRetentionDays(retentionDays); + inputs.retentionDays = new Array(searchPath.length).fill(retention); } return inputs; } exports.getInputs = getInputs; +function parseRetentionDays(retentionDaysStr) { + if (retentionDaysStr) { + const retentionDays = parseInt(retentionDaysStr); + if (isNaN(retentionDays)) { + core.setFailed('Invalid retention-days'); + } + return retentionDays; + } + else { + return undefined; + } +} /***/ }), diff --git a/src/input-helper.ts b/src/input-helper.ts index 8344823..2397ca1 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -9,6 +9,18 @@ export function getInputs(): UploadInputs { const name = core.getInput(Inputs.Name) const path = core.getInput(Inputs.Path, {required: true}) + const searchPath = Array.isArray(path) ? 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 = Array.isArray(name) + ? name.concat( + new Array(Math.max(0, searchPath.length - name.length)).fill( + defaultArtifactName + ) + ) + : new Array(searchPath.length).fill(name || defaultArtifactName) + const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound) const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound] @@ -23,18 +35,40 @@ export function getInputs(): UploadInputs { } const inputs = { - artifactName: name, - searchPath: path, + artifactName, + searchPath, 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') - } + // Accepts an individual value or an array as input + const retentionDays = core.getInput(Inputs.RetentionDays) + if (Array.isArray(retentionDays)) { + // If array sizes don't match, use default value instead + inputs.retentionDays = retentionDays + .map(parseRetentionDays) + .concat( + new Array(Math.max(0, searchPath.length - retentionDays.length)).fill( + undefined + ) + ) + } else { + const retention = parseRetentionDays(retentionDays) + inputs.retentionDays = new Array(searchPath.length).fill(retention) } return inputs } + +function parseRetentionDays( + retentionDaysStr: string | undefined +): number | undefined { + if (retentionDaysStr) { + const retentionDays = parseInt(retentionDaysStr) + if (isNaN(retentionDays)) { + core.setFailed('Invalid retention-days') + } + return retentionDays + } else { + return undefined + } +} diff --git a/src/upload-artifact.ts b/src/upload-artifact.ts index 3add259..5c4501b 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: { - core.warning( - `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 = 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 + } } - case NoFileOptions.error: { - core.setFailed( - `No files were found with the provided path: ${inputs.searchPath}. No artifacts will be uploaded.` - ) - break - } - case NoFileOptions.ignore: { - core.info( - `No files were found with the provided path: ${inputs.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 = 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 { + const s = searchResult.filesToUpload.length === 1 ? '' : 's' core.info( - `Artifact ${uploadResponse.artifactName} has been successfully uploaded!` + `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 = create() + const options: UploadOptions = { + continueOnError: false + } + if (retentionDays) { + options.retentionDays = retentionDays + } + + const uploadResponse = await 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!` + ) + } } } } catch (err) { diff --git a/src/upload-inputs.ts b/src/upload-inputs.ts index 37325df..4f0e0af 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)[] } From 5442a4e8a3867a1c14be1ee3a04c4f47b261d632 Mon Sep 17 00:00:00 2001 From: BToersche Date: Thu, 13 May 2021 22:27:39 +0200 Subject: [PATCH 2/2] Allow inputs to accept stringified JSON --- README.md | 8 +++++ action.yml | 3 +- dist/index.js | 53 ++++++++++++++++----------- src/input-helper.ts | 87 +++++++++++++++++++++++++++++++-------------- 4 files changed, 102 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 32fe99a..fd2f161 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 2003cdd..2733633 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 d2d85be..ec17608 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6581,12 +6581,16 @@ 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 = Array.isArray(path) ? path : [path]; + 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 = Array.isArray(name) - ? name.concat(new Array(Math.max(0, searchPath.length - name.length)).fill(defaultArtifactName)) - : new Array(searchPath.length).fill(name || defaultArtifactName); + 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) { @@ -6595,34 +6599,43 @@ function getInputs() { const inputs = { artifactName, searchPath, + retentionDays, ifNoFilesFound: noFileBehavior }; - // Accepts an individual value or an array as input - const retentionDays = core.getInput(constants_1.Inputs.RetentionDays); - if (Array.isArray(retentionDays)) { - // If array sizes don't match, use default value instead - inputs.retentionDays = retentionDays - .map(parseRetentionDays) - .concat(new Array(Math.max(0, searchPath.length - retentionDays.length)).fill(undefined)); - } - else { - const retention = parseRetentionDays(retentionDays); - inputs.retentionDays = new Array(searchPath.length).fill(retention); - } 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) { + if (retentionDaysStr != null) { const retentionDays = parseInt(retentionDaysStr); if (isNaN(retentionDays)) { core.setFailed('Invalid retention-days'); } return retentionDays; } - else { - return undefined; - } + return undefined; } diff --git a/src/input-helper.ts b/src/input-helper.ts index 2397ca1..c770137 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -9,17 +9,28 @@ export function getInputs(): UploadInputs { const name = core.getInput(Inputs.Name) const path = core.getInput(Inputs.Path, {required: true}) - const searchPath = Array.isArray(path) ? path : [path] + 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 = Array.isArray(name) - ? name.concat( - new Array(Math.max(0, searchPath.length - name.length)).fill( - defaultArtifactName - ) - ) - : new Array(searchPath.length).fill(name || defaultArtifactName) + 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] @@ -37,38 +48,60 @@ export function getInputs(): UploadInputs { const inputs = { artifactName, searchPath, + retentionDays, ifNoFilesFound: noFileBehavior } as UploadInputs - // Accepts an individual value or an array as input - const retentionDays = core.getInput(Inputs.RetentionDays) - if (Array.isArray(retentionDays)) { - // If array sizes don't match, use default value instead - inputs.retentionDays = retentionDays - .map(parseRetentionDays) - .concat( - new Array(Math.max(0, searchPath.length - retentionDays.length)).fill( - undefined - ) - ) - } else { - const retention = parseRetentionDays(retentionDays) - inputs.retentionDays = new Array(searchPath.length).fill(retention) - } - 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 +} + function parseRetentionDays( retentionDaysStr: string | undefined ): number | undefined { - if (retentionDaysStr) { + if (retentionDaysStr != null) { const retentionDays = parseInt(retentionDaysStr) if (isNaN(retentionDays)) { core.setFailed('Invalid retention-days') } return retentionDays - } else { - return undefined } + return undefined }