Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload multiple artifacts dynamically #205

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions action.yml
Expand Up @@ -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
Expand Down
127 changes: 84 additions & 43 deletions dist/index.js
Expand Up @@ -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!`);
}
}
}
}
Expand Down Expand Up @@ -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;


/***/ }),
Expand Down
83 changes: 75 additions & 8 deletions src/input-helper.ts
Expand Up @@ -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]

Expand All @@ -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 = <string[]>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
}