diff --git a/.github/workflows/test-per-file.yml b/.github/workflows/test-per-file.yml new file mode 100644 index 0000000..1b56e1b --- /dev/null +++ b/.github/workflows/test-per-file.yml @@ -0,0 +1,98 @@ +name: Test Artifact Per File +on: + workflow_dispatch: + push: + branches: + - main + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + +jobs: + + build: + name: Build + + strategy: + matrix: + runs-on: [ubuntu-latest, macos-latest, windows-latest] + fail-fast: false + + runs-on: ${{ matrix.runs-on }} + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set Node.js 12.x + uses: actions/setup-node@v1 + with: + node-version: 12.x + + - name: Install dependencies + run: npm ci + + - name: Compile + run: npm run build + + - name: npm test + run: npm test + + - name: Lint + run: npm run lint + + - name: Format + run: npm run format-check + + # Test end-to-end by uploading two artifacts and then downloading them + - name: Create artifact files + run: | + mkdir -p path/to/dir-1 + mkdir -p path/to/dir-2 + mkdir -p path/to/dir-3 + mkdir -p path/from/dir-1 + + echo > path/to/dir-1/file1.txt "path/to/dir-1/file1.txt" + echo > path/to/dir-2/file1.txt "path/to/dir-2/file1.txt" + echo > path/to/dir-2/file2.txt "path/to/dir-2/file2.txt" + echo > path/to/dir-3/file1.txt "path/to/dir-3/file1.txt" + echo > path/to/dir-3/file2.txt "path/to/dir-3/file2.txt" + echo > path/to/dir-3/file3.txt "path/to/dir-3/file3.txt" + echo > path/from/dir-1/file1.txt "path/from/dir-1/file1.txt" + + tar -zvcf path/to/dir-3/all.gz path/to/dir-3/* + + # Upload a single file artifact + - name: 'Upload artifact #1' + uses: ./ + with: + path: path/to/dir-1/file1.txt + artifact-per-file: true + + # Upload using a wildcard pattern, name should default to 'artifact' if not provided + - name: 'Upload artifact #2' + uses: ./ + with: + path: path/to/dir-2/* + artifact-per-file: true + artifact-name-rule: ${dir}-${base} + + # Upload a directory that contains a file that will be uploaded with GZip + - name: 'Upload artifact #3' + uses: ./ + with: + path: path/to/dir-3/*.gz + artifact-per-file: true + artifact-name-rule: ${dir}-${name}${ext} + + # Upload a directory that contains a file that will be uploaded with GZip + - name: 'Upload artifact #4' + uses: ./ + with: + path: | + path/**/dir*/ + !path/to/dir-3/*.gz + artifact-per-file: true + artifact-name-rule: ${{ matrix.runs-on }}-${base} diff --git a/dist/index.js b/dist/index.js index 2be9f50..68e2cd2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4733,12 +4733,16 @@ var __importStar = (this && this.__importStar) || function (mod) { result["default"] = mod; return result; }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); const artifact_1 = __webpack_require__(214); const search_1 = __webpack_require__(575); const input_helper_1 = __webpack_require__(583); const constants_1 = __webpack_require__(694); +const path_1 = __importDefault(__webpack_require__(622)); function run() { return __awaiter(this, void 0, void 0, function* () { try { @@ -4775,12 +4779,68 @@ function run() { 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.`); + const artifactsName = inputs['artifactsName'] || 'artifacts'; + const artifactPerFile = inputs['artifactPerFile'] || false; + if (!artifactPerFile) { + const uploadResponse = yield artifactClient.uploadArtifact(artifactsName, 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!`); + } } else { - core.info(`Artifact ${uploadResponse.artifactName} has been successfully uploaded!`); + const filesToUpload = searchResult.filesToUpload; + const SuccessedItems = []; + const FailedItems = []; + const artifactNameRule = inputs['artifactNameRule']; + for (let i = 0; i < filesToUpload.length; i++) { + const file = filesToUpload[i]; + core.info(file); + const pathObject = path_1.default.parse(file); + let artifactName = artifactNameRule; + for (const key of Object.keys(pathObject)) { + const re = `$\{${key}}`; + if (artifactNameRule.includes(re)) { + const value = pathObject[key] || ''; + artifactName = artifactName.replace(re, value); + } + } + if (artifactName.includes(path_1.default.sep)) { + core.warning(`${artifactName} includes ${path_1.default.sep}`); + artifactName = artifactName.split(path_1.default.sep).join('_'); + } + if (artifactName.includes(':')) { + core.warning(`${artifactName} includes :`); + artifactName = artifactName.split(':').join('-'); + } + core.info(artifactName); + const artifactItemExist = SuccessedItems.includes(artifactName); + if (artifactItemExist) { + const oldArtifactName = artifactName; + core.warning(`${artifactName} artifact alreay exist`); + artifactName = `${i}__${artifactName}`; + core.warning(`${oldArtifactName} => ${artifactName}`); + } + const uploadResponse = yield artifactClient.uploadArtifact(artifactName, [file], searchResult.rootDirectory, options); + if (uploadResponse.failedItems.length > 0) { + FailedItems.push(artifactName); + } + else { + SuccessedItems.push(artifactName); + } + } + if (FailedItems.length > 0) { + let errMsg = `${FailedItems.length} artifacts failed to upload, they were:\n`; + errMsg += FailedItems.join('\n'); + core.setFailed(errMsg); + } + if (SuccessedItems.length > 0) { + let infoMsg = `${SuccessedItems.length} artifacts has been successfully uploaded! They were:\n`; + infoMsg += SuccessedItems.join('\n'); + core.info(infoMsg); + } } } } @@ -7174,26 +7234,59 @@ const constants_1 = __webpack_require__(694); * Helper to get all the inputs for the action */ function getInputs() { - const name = core.getInput(constants_1.Inputs.Name); + const TRUE_MAP = ['true', 'True', 'TRUE']; + let artifactPerFile = false; + const artifactPerFileStr = core.getInput(constants_1.Inputs.ArtifactPerFile); + if (artifactPerFileStr) { + artifactPerFile = TRUE_MAP.includes(artifactPerFileStr) ? true : false; + } + let name = ''; + let artifactNameRule = ''; + if (!artifactPerFile) { + name = core.getInput(constants_1.Inputs.Name); + } + else { + artifactNameRule = core.getInput(constants_1.Inputs.ArtifactNameRule) || '${base}'; + } const path = core.getInput(constants_1.Inputs.Path, { required: true }); 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, - 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'); + const typedInputs = (artifactPerFile) => { + const retentionDaysStr = core.getInput(constants_1.Inputs.RetentionDays); + if (!artifactPerFile) { + const inputs = { + artifactsName: name, + searchPath: path, + ifNoFilesFound: noFileBehavior + }; + if (retentionDaysStr) { + inputs.retentionDays = parseInt(retentionDaysStr); + if (isNaN(inputs.retentionDays)) { + core.setFailed('Invalid retention-days'); + } + } + return inputs; } - } - return inputs; + else { + const inputs = { + searchPath: path, + ifNoFilesFound: noFileBehavior, + artifactPerFile: artifactPerFile, + artifactNameRule: artifactNameRule + }; + if (retentionDaysStr) { + inputs.retentionDays = parseInt(retentionDaysStr); + if (isNaN(inputs.retentionDays)) { + core.setFailed('Invalid retention-days'); + } + } + return inputs; + } + }; + return typedInputs(artifactPerFile); } exports.getInputs = getInputs; @@ -8196,6 +8289,8 @@ var Inputs; Inputs["Path"] = "path"; Inputs["IfNoFilesFound"] = "if-no-files-found"; Inputs["RetentionDays"] = "retention-days"; + Inputs["ArtifactPerFile"] = "artifact-per-file"; + Inputs["ArtifactNameRule"] = "artifact-name-rule"; })(Inputs = exports.Inputs || (exports.Inputs = {})); var NoFileOptions; (function (NoFileOptions) { diff --git a/src/constants.ts b/src/constants.ts index 894ff4c..b5c9fb0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,7 +2,9 @@ export enum Inputs { Name = 'name', Path = 'path', IfNoFilesFound = 'if-no-files-found', - RetentionDays = 'retention-days' + RetentionDays = 'retention-days', + ArtifactPerFile = 'artifact-per-file', + ArtifactNameRule = 'artifact-name-rule' } export enum NoFileOptions { diff --git a/src/input-helper.ts b/src/input-helper.ts index 8344823..5c38186 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -1,14 +1,28 @@ import * as core from '@actions/core' import {Inputs, NoFileOptions} from './constants' -import {UploadInputs} from './upload-inputs' +import {UploadInputs, UploadPerFile} from './upload-inputs' /** * Helper to get all the inputs for the action */ -export function getInputs(): UploadInputs { - const name = core.getInput(Inputs.Name) - const path = core.getInput(Inputs.Path, {required: true}) +export function getInputs(): UploadInputs | UploadPerFile { + const TRUE_MAP = ['true', 'True', 'TRUE'] + let artifactPerFile = false + const artifactPerFileStr = core.getInput(Inputs.ArtifactPerFile) + if (artifactPerFileStr) { + artifactPerFile = TRUE_MAP.includes(artifactPerFileStr) ? true : false + } + + let name = '' + let artifactNameRule = '' + if (!artifactPerFile) { + name = core.getInput(Inputs.Name) + } else { + artifactNameRule = core.getInput(Inputs.ArtifactNameRule) || '${base}' + } + + const path = core.getInput(Inputs.Path, {required: true}) const ifNoFilesFound = core.getInput(Inputs.IfNoFilesFound) const noFileBehavior: NoFileOptions = NoFileOptions[ifNoFilesFound] @@ -22,19 +36,44 @@ export function getInputs(): UploadInputs { ) } - const inputs = { - artifactName: name, - searchPath: path, - ifNoFilesFound: noFileBehavior - } as UploadInputs + const typedInputs = ( + artifactPerFile: boolean + ): UploadInputs | UploadPerFile => { + const retentionDaysStr = core.getInput(Inputs.RetentionDays) - const retentionDaysStr = core.getInput(Inputs.RetentionDays) - if (retentionDaysStr) { - inputs.retentionDays = parseInt(retentionDaysStr) - if (isNaN(inputs.retentionDays)) { - core.setFailed('Invalid retention-days') + if (!artifactPerFile) { + const inputs = { + artifactsName: name, + searchPath: path, + ifNoFilesFound: noFileBehavior + } as UploadInputs + + if (retentionDaysStr) { + inputs.retentionDays = parseInt(retentionDaysStr) + if (isNaN(inputs.retentionDays)) { + core.setFailed('Invalid retention-days') + } + } + + return inputs + } else { + const inputs = { + searchPath: path, + ifNoFilesFound: noFileBehavior, + artifactPerFile: artifactPerFile, + artifactNameRule: artifactNameRule + } as UploadPerFile + + if (retentionDaysStr) { + inputs.retentionDays = parseInt(retentionDaysStr) + if (isNaN(inputs.retentionDays)) { + core.setFailed('Invalid retention-days') + } + } + + return inputs } } - return inputs + return typedInputs(artifactPerFile) } diff --git a/src/upload-artifact.ts b/src/upload-artifact.ts index d78e069..b09e33e 100644 --- a/src/upload-artifact.ts +++ b/src/upload-artifact.ts @@ -3,10 +3,12 @@ import {create, UploadOptions} from '@actions/artifact' import {findFilesToUpload} from './search' import {getInputs} from './input-helper' import {NoFileOptions} from './constants' +import {UploadInputs, UploadPerFile} from './upload-inputs' +import path from 'path' async function run(): Promise { try { - const inputs = getInputs() + const inputs: UploadInputs | UploadPerFile = 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 @@ -51,21 +53,85 @@ async function run(): Promise { options.retentionDays = inputs.retentionDays } - const uploadResponse = await artifactClient.uploadArtifact( - inputs.artifactName, - searchResult.filesToUpload, - searchResult.rootDirectory, - options - ) + const artifactsName = inputs['artifactsName'] || 'artifacts' + const artifactPerFile = inputs['artifactPerFile'] || false + if (!artifactPerFile) { + const uploadResponse = await artifactClient.uploadArtifact( + artifactsName, + 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.` - ) + 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!` + ) + } } else { - core.info( - `Artifact ${uploadResponse.artifactName} has been successfully uploaded!` - ) + const filesToUpload = searchResult.filesToUpload + const SuccessedItems: string[] = [] + const FailedItems: string[] = [] + + const artifactNameRule = inputs['artifactNameRule'] + for (let i = 0; i < filesToUpload.length; i++) { + const file = filesToUpload[i] + core.info(file) + + const pathObject = path.parse(file) + let artifactName = artifactNameRule + for (const key of Object.keys(pathObject)) { + const re = `$\{${key}}` + if (artifactNameRule.includes(re)) { + const value = pathObject[key] || '' + artifactName = artifactName.replace(re, value) + } + } + if (artifactName.includes(path.sep)) { + core.warning(`${artifactName} includes ${path.sep}`) + artifactName = artifactName.split(path.sep).join('_') + } + if (artifactName.includes(':')) { + core.warning(`${artifactName} includes :`) + artifactName = artifactName.split(':').join('-') + } + core.info(artifactName) + + const artifactItemExist = SuccessedItems.includes(artifactName) + if (artifactItemExist) { + const oldArtifactName = artifactName + core.warning(`${artifactName} artifact alreay exist`) + artifactName = `${i}__${artifactName}` + core.warning(`${oldArtifactName} => ${artifactName}`) + } + + const uploadResponse = await artifactClient.uploadArtifact( + artifactName, + [file], + searchResult.rootDirectory, + options + ) + if (uploadResponse.failedItems.length > 0) { + FailedItems.push(artifactName) + } else { + SuccessedItems.push(artifactName) + } + } + + if (FailedItems.length > 0) { + let errMsg = `${FailedItems.length} artifacts failed to upload, they were:\n` + errMsg += FailedItems.join('\n') + core.setFailed(errMsg) + } + if (SuccessedItems.length > 0) { + let infoMsg = `${SuccessedItems.length} artifacts has been successfully uploaded! They were:\n` + infoMsg += SuccessedItems.join('\n') + core.info(infoMsg) + } } } } catch (err) { diff --git a/src/upload-inputs.ts b/src/upload-inputs.ts index 37325df..8e3fa09 100644 --- a/src/upload-inputs.ts +++ b/src/upload-inputs.ts @@ -4,7 +4,7 @@ export interface UploadInputs { /** * The name of the artifact that will be uploaded */ - artifactName: string + artifactsName: string /** * The search path used to describe what to upload as part of the artifact @@ -21,3 +21,20 @@ export interface UploadInputs { */ retentionDays: number } + +export interface UploadPerFile { + searchPath: string + ifNoFilesFound: NoFileOptions + retentionDays: number + + // artifact-per-file: {true | false} + // @default: false + artifactPerFile: boolean + + // https://nodejs.org/docs/latest-v16.x/api/path.html#pathparsepath + // @args: searchResult.filesToUpload + // @return: String.replace() + // @default: pathObject.base + // @default rule: "${base}" + artifactNameRule: string +}