diff --git a/.github/workflows/test-per-file.yml b/.github/workflows/test-per-file.yml new file mode 100644 index 00000000..51623c68 --- /dev/null +++ b/.github/workflows/test-per-file.yml @@ -0,0 +1,94 @@ +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/* + + - name: 'Upload artifact #1' + uses: ./ + with: + path: path/to/dir-1/file1.txt + artifact-per-file: true + + - name: 'Upload artifact #2' + uses: ./ + with: + path: path/to/dir-2/* + artifact-per-file: true + artifact-name-rule: ${dir}-${base} + + - name: 'Upload artifact #3' + uses: ./ + with: + path: path/to/dir-3/*.gz + artifact-per-file: true + artifact-name-rule: ${path}-${name}${ext} + + - name: 'Upload artifact #4' + uses: ./ + with: + path: | + path/**/dir-1/ + !path/to/dir-3/*.gz + artifact-per-file: true + artifact-name-rule: ${{ matrix.runs-on }}-${path}-${name} diff --git a/action.yml b/action.yml index 94a583af..188957db 100644 --- a/action.yml +++ b/action.yml @@ -3,8 +3,8 @@ description: 'Upload a build artifact that can be used by subsequent workflow st author: 'GitHub' inputs: name: - description: 'Artifact name' - default: 'artifact' + description: 'Artifacts name' + default: 'artifacts' path: description: 'A file, directory or wildcard pattern that describes what to upload' required: true @@ -23,6 +23,34 @@ inputs: Minimum 1 day. Maximum 90 days unless changed from the repository settings page. + artifact-per-file: + description: enable otption for uploading one artifact per file + default: "false" + artifact-name-rule: + description: > + // https://nodejs.org/docs/latest-v16.x/api/path.html#pathparsepath + // Modified from path.parse() + + path.parse('/home/user/dir/file.txt'); + // Returns: + // { root: '/', + // dir: '/home/user/dir', + // path: 'home/user/dir' + // base: 'file.txt', + // ext: '.txt', + // name: 'file' } + + ┌─────────────────────┬────────────┐ + │ dir sep base │ + ├──────┬──────────────┼──────┬─────┤ + │ root │ path │ name │ ext │ + " / home/user/dir / file .txt " + └──────┴──────────────┴──────┴─────┘ + (All spaces in the "" line should be ignored. They are purely for formatting.) + + Every key need in wrapper: ${} + sep just for prompt, can't be used + default: ${base} runs: using: 'node16' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 2be9f506..c994e441 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,91 @@ 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; + // GitHub workspace + let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] || undefined; + if (!githubWorkspacePath) { + core.warning('GITHUB_WORKSPACE not defined'); + } + else { + githubWorkspacePath = path_1.default.resolve(githubWorkspacePath); + core.info(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`); + } + const rootDirectory = searchResult.rootDirectory; + core.info('rootDirectory: ' + rootDirectory); + if (!artifactPerFile) { + const uploadResponse = yield artifactClient.uploadArtifact(artifactsName, searchResult.filesToUpload, 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: ' + file); + const pathObject = Object.assign({}, path_1.default.parse(file)); + const pathBase = pathObject.base; + const pathRoot = githubWorkspacePath + ? githubWorkspacePath + : path_1.default.parse(rootDirectory).dir; + pathObject.root = pathRoot; + core.info('root: ' + pathRoot); + pathObject['path'] = file.slice(pathRoot.length, file.length - path_1.default.sep.length - pathBase.length); + core.info('path: ' + pathObject['path']); + 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.startsWith(path_1.default.sep)) { + core.warning(`${artifactName} startsWith ${path_1.default.sep}`); + artifactName = artifactName.slice(path_1.default.sep.length); + } + if (artifactName.includes(':')) { + core.warning(`${artifactName} includes :`); + artifactName = artifactName.split(':').join('-'); + } + if (artifactName.includes(path_1.default.sep)) { + core.warning(`${artifactName} includes ${path_1.default.sep}`); + artifactName = artifactName.split(path_1.default.sep).join('_'); + } + core.debug(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], 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 +7257,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 +8312,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 894ff4c0..b5c9fb0c 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 83448236..5c381867 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 retentionDaysStr = core.getInput(Inputs.RetentionDays) - if (retentionDaysStr) { - inputs.retentionDays = parseInt(retentionDaysStr) - if (isNaN(inputs.retentionDays)) { - core.setFailed('Invalid retention-days') + const typedInputs = ( + artifactPerFile: boolean + ): UploadInputs | UploadPerFile => { + const retentionDaysStr = core.getInput(Inputs.RetentionDays) + + 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 d78e0691..98440b13 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,116 @@ 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 (uploadResponse.failedItems.length > 0) { - core.setFailed( - `An error was encountered when uploading ${uploadResponse.artifactName}. There were ${uploadResponse.failedItems.length} items that failed to upload.` - ) + // GitHub workspace + let githubWorkspacePath = process.env['GITHUB_WORKSPACE'] || undefined + if (!githubWorkspacePath) { + core.warning('GITHUB_WORKSPACE not defined') } else { - core.info( - `Artifact ${uploadResponse.artifactName} has been successfully uploaded!` + githubWorkspacePath = path.resolve(githubWorkspacePath) + core.info(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`) + } + + const rootDirectory = searchResult.rootDirectory + core.info('rootDirectory: ' + rootDirectory) + + if (!artifactPerFile) { + const uploadResponse = await artifactClient.uploadArtifact( + artifactsName, + searchResult.filesToUpload, + 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 { + 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: ' + file) + + const pathObject = Object.assign({}, path.parse(file)) + const pathBase = pathObject.base + const pathRoot = githubWorkspacePath + ? githubWorkspacePath + : path.parse(rootDirectory).dir + pathObject.root = pathRoot + core.info('root: ' + pathRoot) + + pathObject['path'] = file.slice( + pathRoot.length, + file.length - path.sep.length - pathBase.length + ) + core.info('path: ' + pathObject['path']) + + 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.startsWith(path.sep)) { + core.warning(`${artifactName} startsWith ${path.sep}`) + artifactName = artifactName.slice(path.sep.length) + } + if (artifactName.includes(':')) { + core.warning(`${artifactName} includes :`) + artifactName = artifactName.split(':').join('-') + } + if (artifactName.includes(path.sep)) { + core.warning(`${artifactName} includes ${path.sep}`) + artifactName = artifactName.split(path.sep).join('_') + } + core.debug(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], + 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 37325df3..8e3fa09e 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 +}