From 2b274221872f0403cca17e49cff3c7404d8bbe32 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Wed, 21 Sep 2022 16:26:44 +0200 Subject: [PATCH] Code refactor --- .eslintrc.json | 110 ++-- .github/dependabot.yml | 4 +- .github/workflows/check-dist.yml | 1 - .github/workflows/test.yml | 1 - README.md | 1 + dist/index.js | 949 +++++++++++++++---------------- package-lock.json | 340 ++++++----- src/main.ts | 919 +++++++++++++++--------------- tsconfig.json | 14 +- 9 files changed, 1147 insertions(+), 1192 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d9e6a05..2e66d25 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,54 +1,60 @@ { - "plugins": ["@typescript-eslint"], - "extends": ["plugin:github/recommended"], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "module", - "project": "./tsconfig.json" - }, - "rules": { - "i18n-text/no-en": "off", - "eslint-comments/no-use": "off", - "import/no-namespace": "off", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], - "@typescript-eslint/no-require-imports": "error", - "@typescript-eslint/array-type": "error", - "@typescript-eslint/await-thenable": "error", - "@typescript-eslint/ban-ts-comment": "error", - "camelcase": "off", - "@typescript-eslint/consistent-type-assertions": "error", - "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], - "@typescript-eslint/func-call-spacing": ["error", "never"], - "@typescript-eslint/no-array-constructor": "error", - "@typescript-eslint/no-empty-interface": "error", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-extraneous-class": "error", - "@typescript-eslint/no-for-in-array": "error", - "@typescript-eslint/no-inferrable-types": "error", - "@typescript-eslint/no-misused-new": "error", - "@typescript-eslint/no-namespace": "error", - "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/no-unnecessary-qualifier": "error", - "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/no-useless-constructor": "error", - "@typescript-eslint/no-var-requires": "error", - "@typescript-eslint/prefer-for-of": "warn", - "@typescript-eslint/prefer-function-type": "warn", - "@typescript-eslint/prefer-includes": "error", - "@typescript-eslint/prefer-string-starts-ends-with": "error", - "@typescript-eslint/promise-function-async": "error", - "@typescript-eslint/require-array-sort-compare": "error", - "@typescript-eslint/restrict-plus-operands": "error", - "semi": "off", - "@typescript-eslint/semi": ["error", "never"], - "@typescript-eslint/type-annotation-spacing": "error", - "@typescript-eslint/unbound-method": "error" - }, - "env": { - "node": true, - "es6": true - } + "plugins": ["@typescript-eslint"], + "extends": ["plugin:github/recommended"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 9, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "rules": { + "i18n-text/no-en": "off", + "eslint-comments/no-use": "off", + "import/no-namespace": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/explicit-member-accessibility": [ + "error", + {"accessibility": "no-public"} + ], + "@typescript-eslint/no-require-imports": "error", + "@typescript-eslint/array-type": "error", + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/ban-ts-comment": "error", + "camelcase": "off", + "@typescript-eslint/consistent-type-assertions": "error", + "@typescript-eslint/explicit-function-return-type": [ + "error", + {"allowExpressions": true} + ], + "@typescript-eslint/func-call-spacing": ["error", "never"], + "@typescript-eslint/no-array-constructor": "error", + "@typescript-eslint/no-empty-interface": "error", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-extraneous-class": "error", + "@typescript-eslint/no-for-in-array": "error", + "@typescript-eslint/no-inferrable-types": "error", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-namespace": "error", + "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/no-unnecessary-qualifier": "error", + "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-useless-constructor": "error", + "@typescript-eslint/no-var-requires": "error", + "@typescript-eslint/prefer-for-of": "warn", + "@typescript-eslint/prefer-function-type": "warn", + "@typescript-eslint/prefer-includes": "error", + "@typescript-eslint/prefer-string-starts-ends-with": "error", + "@typescript-eslint/promise-function-async": "error", + "@typescript-eslint/require-array-sort-compare": "error", + "@typescript-eslint/restrict-plus-operands": "error", + "semi": "off", + "@typescript-eslint/semi": ["error", "never"], + "@typescript-eslint/type-annotation-spacing": "error", + "@typescript-eslint/unbound-method": "error" + }, + "env": { + "node": true, + "es6": true } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eb6da0a..57a017a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,11 +4,11 @@ updates: directory: / schedule: interval: monthly - time: "04:00" + time: '04:00' - package-ecosystem: npm directory: / schedule: interval: monthly - time: "04:00" + time: '04:00' open-pull-requests-limit: 10 diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml index c0169df..a7df2df 100644 --- a/.github/workflows/check-dist.yml +++ b/.github/workflows/check-dist.yml @@ -50,4 +50,3 @@ jobs: with: name: dist path: dist/ - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 51b2d6d..e0e5753 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,4 +85,3 @@ jobs: if: ${{ steps.skip_check.outputs.should_skip == 'false' }} run: | echo "Do stuff..." && sleep 30 - diff --git a/README.md b/README.md index f53e168..04edcf0 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,7 @@ You can use `skip-duplicate-actions` to either skip individual steps or entire j To minimize changes to existing jobs, it is often easier to skip entire jobs. > **Note** +> > - You may need to use [`fromJSON`](https://docs.github.com/en/actions/learn-github-actions/expressions#fromjson) to access properties of object outputs. For example, for `skipped_by.runId`, you can use the expression: `${{ fromJSON(steps.skip_check.outputs.skipped_by).runId }}`. > - For GitHub repositories where [default permissions](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#setting-the-permissions-of-the-github_token-for-your-repository) for `GITHUB_TOKEN` has been set to "permissive (read-only)", the following lines must be included in the workflow (see [permissions syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions)): > ```yaml diff --git a/dist/index.js b/dist/index.js index 7fadd1e..9ca988c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -46,135 +46,46 @@ const core = __importStar(__nccwpck_require__(2186)); const github = __importStar(__nccwpck_require__(5438)); const micromatch_1 = __importDefault(__nccwpck_require__(6228)); const js_yaml_1 = __importDefault(__nccwpck_require__(1917)); -const concurrentSkippingMap = { - always: null, - same_content: null, - same_content_newer: null, - outdated_runs: null, - never: null -}; -function getConcurrentSkippingOptions() { - return Object.keys(concurrentSkippingMap); -} -function parseWorkflowRun(run) { - var _a, _b, _c, _d; - const treeHash = (_a = run.head_commit) === null || _a === void 0 ? void 0 : _a.tree_id; - if (!treeHash) { - logFatal(` - Could not find the tree hash of run ${run.id} (workflow: $ {run.workflow_id}, - name: ${run.name}, head_branch: ${run.head_branch}, head_sha: ${run.head_sha}). - You might have a run associated with a headless or removed commit. - `); - } - const workflowId = run.workflow_id; - if (!workflowId) { - logFatal(`Could not find the workflow ID of run ${run.id}`); +const workflowRunTriggerOptions = [ + 'pull_request', + 'push', + 'workflow_dispatch', + 'schedule' +]; +const concurrentSkippingOptions = [ + 'always', + 'same_content', + 'same_content_newer', + 'outdated_runs', + 'never' +]; +class SkipDuplicateActions { + constructor(inputs, context) { + this.globOptions = { + dot: true // Match dotfiles. Otherwise dotfiles are ignored unless a "." is explicitly defined in the pattern. + }; + this.inputs = inputs; + this.context = context; } - return { - event: run.event, - treeHash, - commitHash: run.head_sha, - status: run.status, - conclusion: (_b = run.conclusion) !== null && _b !== void 0 ? _b : null, - html_url: run.html_url, - branch: (_c = run.head_branch) !== null && _c !== void 0 ? _c : null, - repo: (_d = run.head_repository.full_name) !== null && _d !== void 0 ? _d : null, - runId: run.id, - workflowId, - createdAt: run.created_at, - runNumber: run.run_number - }; -} -function parseAllRuns(response) { - return response.workflow_runs - .filter(run => run.head_commit && run.workflow_id) - .map(run => parseWorkflowRun(run)); -} -function parseOlderRuns(response, currentRun) { - const olderRuns = response.workflow_runs.filter(run => { - // Only consider older workflow runs to prevent some nasty race conditions and edge cases. - return (new Date(run.created_at).getTime() < - new Date(currentRun.createdAt).getTime()); - }); - return olderRuns - .filter(run => run.head_commit && run.workflow_id) - .map(run => parseWorkflowRun(run)); -} -function main() { - return __awaiter(this, void 0, void 0, function* () { - try { - const token = core.getInput('github_token', { required: true }); - if (!token) { - logFatal('Did not find github_token'); - } - const repo = github.context.repo; - const repoOwner = repo === null || repo === void 0 ? void 0 : repo.owner; - if (!repoOwner) { - logFatal('Did not find the repo owner'); - } - const repoName = repo === null || repo === void 0 ? void 0 : repo.repo; - if (!repoName) { - logFatal('Did not find the repo name'); - } - const runId = github.context.runId; - if (!runId) { - logFatal('Did not find runId'); - } - let context; - try { - const octokit = github.getOctokit(token); - const { data: current_run } = yield octokit.rest.actions.getWorkflowRun({ - owner: repoOwner, - repo: repoName, - run_id: runId - }); - const currentRun = parseWorkflowRun(current_run); - const { data } = yield octokit.rest.actions.listWorkflowRuns({ - owner: repoOwner, - repo: repoName, - workflow_id: currentRun.workflowId, - per_page: 100 - }); - context = { - repoOwner, - repoName, - currentRun, - olderRuns: parseOlderRuns(data, currentRun), - allRuns: parseAllRuns(data), - octokit, - pathsIgnore: getStringArrayInput('paths_ignore'), - paths: getStringArrayInput('paths'), - pathsFilter: getPathsFilterInput('paths_filter'), - doNotSkip: getStringArrayInput('do_not_skip'), - concurrentSkipping: getConcurrentSkippingInput('concurrent_skipping') - }; - } - catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.warning(e); - } - core.warning('Failed to fetch the required workflow information'); - exitSuccess({ - shouldSkip: false, - reason: 'no_workflow_information' - }); - } - const cancelOthers = getBooleanInput('cancel_others', false); - if (cancelOthers) { - yield cancelOutdatedRuns(context); + run() { + return __awaiter(this, void 0, void 0, function* () { + // Cancel outdated runs. + if (this.inputs.cancelOthers) { + yield this.cancelOutdatedRuns(); } - if (context.doNotSkip.includes(context.currentRun.event)) { - core.info(`Do not skip execution because the workflow was triggered with '${context.currentRun.event}'`); + // Abort early if current run has been triggered by an event that should never be skipped. + if (this.inputs.doNotSkip.includes(this.context.currentRun.event)) { + core.info(`Do not skip execution because the workflow was triggered with '${this.context.currentRun.event}'`); exitSuccess({ shouldSkip: false, reason: 'do_not_skip' }); } - const skipAfterSuccessfulDuplicates = getBooleanInput('skip_after_successful_duplicate', true); - if (skipAfterSuccessfulDuplicates) { - const successfulDuplicateRun = detectSuccessfulDuplicateRuns(context); + // Skip on successful duplicate run. + if (this.inputs.skipAfterSuccessfulDuplicates) { + const successfulDuplicateRun = this.findSuccessfulDuplicateRun(this.context.currentRun.treeHash); if (successfulDuplicateRun) { - core.info(`Skip execution because the exact same files have been successfully checked in run ${successfulDuplicateRun.html_url}`); + core.info(`Skip execution because the exact same files have been successfully checked in run ${successfulDuplicateRun.htmlUrl}`); exitSuccess({ shouldSkip: true, reason: 'skip_after_successful_duplicate', @@ -182,8 +93,9 @@ function main() { }); } } - if (context.concurrentSkipping !== 'never') { - const concurrentRun = detectConcurrentRuns(context); + // Skip on concurrent runs. + if (this.inputs.concurrentSkipping !== 'never') { + const concurrentRun = this.detectConcurrentRuns(); if (concurrentRun) { exitSuccess({ shouldSkip: true, @@ -192,10 +104,11 @@ function main() { }); } } - if (context.paths.length >= 1 || - context.pathsIgnore.length >= 1 || - Object.keys(context.pathsFilter).length >= 1) { - const { changedFiles, pathsResult } = yield backtracePathSkipping(context); + // Skip on path matches. + if (this.inputs.paths.length >= 1 || + this.inputs.pathsIgnore.length >= 1 || + Object.keys(this.inputs.pathsFilter).length >= 1) { + const { changedFiles, pathsResult } = yield this.backtracePathSkipping(); exitSuccess({ shouldSkip: pathsResult.global.should_skip === 'unknown' ? false @@ -206,300 +119,356 @@ function main() { pathsResult }); } + // Do not skip otherwise. core.info('Do not skip execution because we did not find a transferable run'); exitSuccess({ shouldSkip: false, reason: 'no_transferable_run' }); - } - catch (e) { - if (e instanceof Error) { - core.error(e); - logFatal(e.message); + }); + } + cancelOutdatedRuns() { + return __awaiter(this, void 0, void 0, function* () { + const cancelVictims = this.context.olderRuns.filter(run => { + // Only cancel runs which are not yet completed. + if (run.status === 'completed') { + return false; + } + // Only cancel runs from same branch and repo (ignore pull request runs from remote repositories) + // and not with same tree hash. + // See https://github.com/fkirc/skip-duplicate-actions/pull/177. + return (run.treeHash !== this.context.currentRun.treeHash && + run.branch === this.context.currentRun.branch && + run.repo === this.context.currentRun.repo); + }); + if (!cancelVictims.length) { + return core.info('Did not find other workflow runs to be cancelled'); } - } - }); -} -function cancelOutdatedRuns(context) { - return __awaiter(this, void 0, void 0, function* () { - const currentRun = context.currentRun; - const cancelVictims = context.olderRuns.filter(run => { - if (run.status === 'completed') { - return false; + for (const victim of cancelVictims) { + try { + const res = yield this.context.octokit.rest.actions.cancelWorkflowRun(Object.assign(Object.assign({}, this.context.repo), { run_id: victim.id })); + core.info(`Cancelled run ${victim.htmlUrl} with response code ${res.status}`); + } + catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.warning(error); + } + core.warning(`Failed to cancel ${victim.htmlUrl}`); + } } - return (run.treeHash !== currentRun.treeHash && - run.branch === currentRun.branch && - run.repo === currentRun.repo); }); - if (!cancelVictims.length) { - return core.info('Did not find other workflow runs to be cancelled'); + } + findSuccessfulDuplicateRun(treeHash) { + return this.context.olderRuns.find(run => run.treeHash === treeHash && + run.status === 'completed' && + run.conclusion === 'success'); + } + detectConcurrentRuns() { + const concurrentRuns = this.context.allRuns.filter(run => run.status !== 'completed'); + if (!concurrentRuns.length) { + core.info('Did not find any concurrent workflow runs'); + return; } - for (const victim of cancelVictims) { - yield cancelWorkflowRun(victim, context); + if (this.inputs.concurrentSkipping === 'always') { + core.info(`Skip execution because another instance of the same workflow is already running in ${concurrentRuns[0].htmlUrl}`); + return concurrentRuns[0]; } - }); -} -function cancelWorkflowRun(run, context) { - return __awaiter(this, void 0, void 0, function* () { - try { - const res = yield context.octokit.rest.actions.cancelWorkflowRun({ - owner: context.repoOwner, - repo: context.repoName, - run_id: run.runId - }); - core.info(`Cancelled run ${run.html_url} with response code ${res.status}`); - } - catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.warning(e); + else if (this.inputs.concurrentSkipping === 'outdated_runs') { + const newerRun = concurrentRuns.find(run => new Date(run.createdAt).getTime() > + new Date(this.context.currentRun.createdAt).getTime()); + if (newerRun) { + core.info(`Skip execution because a newer instance of the same workflow is running in ${newerRun.htmlUrl}`); + return newerRun; } - core.warning(`Failed to cancel ${run.html_url}`); - } - }); -} -function detectSuccessfulDuplicateRuns(context) { - const duplicateRuns = context.olderRuns.filter(run => run.treeHash === context.currentRun.treeHash); - const successfulDuplicate = duplicateRuns.find(run => { - return run.status === 'completed' && run.conclusion === 'success'; - }); - return successfulDuplicate; -} -function detectConcurrentRuns(context) { - const concurrentRuns = context.allRuns.filter(run => { - if (run.status === 'completed') { - return false; - } - if (run.runId === context.currentRun.runId) { - return false; - } - return true; - }); - if (!concurrentRuns.length) { - core.info(`Did not find any concurrent workflow runs`); - return; - } - if (context.concurrentSkipping === 'always') { - core.info(`Skip execution because another instance of the same workflow is already running in ${concurrentRuns[0].html_url}`); - return concurrentRuns[0]; - } - else if (context.concurrentSkipping === 'outdated_runs') { - const newerRun = concurrentRuns.find(run => new Date(run.createdAt).getTime() > - new Date(context.currentRun.createdAt).getTime()); - if (newerRun) { - core.info(`Skip execution because a newer instance of the same workflow is running in ${newerRun.html_url}`); - return newerRun; } - } - else if (context.concurrentSkipping === 'same_content') { - const concurrentDuplicate = concurrentRuns.find(run => run.treeHash === context.currentRun.treeHash); - if (concurrentDuplicate) { - core.info(`Skip execution because the exact same files are concurrently checked in run ${concurrentDuplicate.html_url}`); - return concurrentDuplicate; + else if (this.inputs.concurrentSkipping === 'same_content') { + const concurrentDuplicate = concurrentRuns.find(run => run.treeHash === this.context.currentRun.treeHash); + if (concurrentDuplicate) { + core.info(`Skip execution because the exact same files are concurrently checked in run ${concurrentDuplicate.htmlUrl}`); + return concurrentDuplicate; + } } - } - else if (context.concurrentSkipping === 'same_content_newer') { - const concurrentIsOlder = concurrentRuns.find(run => run.treeHash === context.currentRun.treeHash && - run.runNumber < context.currentRun.runNumber); - if (concurrentIsOlder) { - core.info(`Skip execution because the exact same files are concurrently checked in older run ${concurrentIsOlder.html_url}`); - return concurrentIsOlder; + else if (this.inputs.concurrentSkipping === 'same_content_newer') { + const concurrentIsOlder = concurrentRuns.find(run => run.treeHash === this.context.currentRun.treeHash && + run.runNumber < this.context.currentRun.runNumber); + if (concurrentIsOlder) { + core.info(`Skip execution because the exact same files are concurrently checked in older run ${concurrentIsOlder.htmlUrl}`); + return concurrentIsOlder; + } } + core.info(`Did not find any concurrent workflow runs that justify skipping`); } - core.info(`Did not find any concurrent workflow runs that justify skipping`); -} -function backtracePathSkipping(context) { - var _a, _b; - return __awaiter(this, void 0, void 0, function* () { - let commit; - let iterSha = context.currentRun.commitHash; - let distanceToHEAD = 0; - const allChangedFiles = []; - const pathsFilter = Object.assign(Object.assign({}, context.pathsFilter), { global: { - paths: context.paths, - paths_ignore: context.pathsIgnore, - backtracking: true - } }); - const pathsResult = {}; - for (const name of Object.keys(pathsFilter)) { - pathsResult[name] = { should_skip: 'unknown', backtrack_count: 0 }; - } - do { - commit = yield fetchCommitDetails(iterSha, context); - if (!commit) { - break; + backtracePathSkipping() { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + let commit; + let iterSha = this.context.currentRun.commitHash; + let distanceToHEAD = 0; + const allChangedFiles = []; + const pathsFilter = Object.assign(Object.assign({}, this.inputs.pathsFilter), { global: { + paths: this.inputs.paths, + paths_ignore: this.inputs.pathsIgnore, + backtracking: true + } }); + const pathsResult = {}; + for (const name of Object.keys(pathsFilter)) { + pathsResult[name] = { should_skip: 'unknown', backtrack_count: 0 }; } - iterSha = ((_a = commit.parents) === null || _a === void 0 ? void 0 : _a.length) ? (_b = commit.parents[0]) === null || _b === void 0 ? void 0 : _b.sha : null; - const changedFiles = commit.files - ? commit.files.map(f => f.filename).filter(f => typeof f === 'string') - : []; - allChangedFiles.push(changedFiles); - const successfulRun = (distanceToHEAD >= 1 && - findSuccessfulRun(commit.commit.tree.sha, context.olderRuns)) || - undefined; - for (const [name, values] of Object.entries(pathsResult)) { - // Only process paths where status is not determined yet. - if (values.should_skip !== 'unknown') - continue; - // Skip if paths were ignorable or skippable until now and there is a successful run on the current commit. - if (successfulRun) { - pathsResult[name].should_skip = true; - pathsResult[name].skipped_by = successfulRun; - pathsResult[name].backtrack_count = distanceToHEAD; - core.info(`Skip '${name}' because all changes since ${successfulRun.html_url} are in ignored or skipped paths`); - continue; - } - // Check if backtracking limit has been reached. - if ((pathsFilter[name].backtracking === false && distanceToHEAD === 1) || - pathsFilter[name].backtracking === distanceToHEAD) { - pathsResult[name].should_skip = false; - pathsResult[name].backtrack_count = distanceToHEAD; - core.info(`Stop backtracking for '${name}' because the defined limit has been reached`); - continue; - } - // Ignorable if all changed files match against ignored paths. - if (isCommitPathsIgnored(changedFiles, pathsFilter[name].paths_ignore)) { - core.info(`Commit ${commit.html_url} is path-ignored for '${name}': All of '${changedFiles}' match against patterns '${pathsFilter[name].paths_ignore}'`); - continue; + do { + commit = yield this.fetchCommitDetails(iterSha); + if (!commit) { + break; } - // Skippable if none of the changed files matches against paths. - if (pathsFilter[name].paths.length >= 1) { - const matches = getCommitPathsMatches(changedFiles, pathsFilter[name].paths); - if (matches.length === 0) { - core.info(`Commit ${commit.html_url} is path-skipped for '${name}': None of '${changedFiles}' matches against patterns '${pathsFilter[name].paths}'`); + iterSha = ((_a = commit.parents) === null || _a === void 0 ? void 0 : _a.length) ? (_b = commit.parents[0]) === null || _b === void 0 ? void 0 : _b.sha : null; + const changedFiles = commit.files + ? commit.files.map(f => f.filename).filter(f => typeof f === 'string') + : []; + allChangedFiles.push(changedFiles); + const successfulRun = (distanceToHEAD >= 1 && + this.findSuccessfulDuplicateRun(commit.commit.tree.sha)) || + undefined; + for (const [name, values] of Object.entries(pathsResult)) { + // Only process paths where status has not yet been determined. + if (values.should_skip !== 'unknown') + continue; + // Skip if paths were ignorable or skippable until now and there is a successful run for the current commit. + if (successfulRun) { + pathsResult[name].should_skip = true; + pathsResult[name].skipped_by = successfulRun; + pathsResult[name].backtrack_count = distanceToHEAD; + core.info(`Skip '${name}' because all changes since run ${successfulRun.htmlUrl} are in ignored or skipped paths`); continue; } - else { - pathsResult[name].matched_files = matches; + // Check if backtracking limit has been reached. + if ((pathsFilter[name].backtracking === false && distanceToHEAD === 1) || + pathsFilter[name].backtracking === distanceToHEAD) { + pathsResult[name].should_skip = false; + pathsResult[name].backtrack_count = distanceToHEAD; + core.info(`Stop backtracking for '${name}' because the defined limit has been reached`); + continue; + } + // Ignorable if all changed files match against ignored paths. + if (this.isCommitPathsIgnored(changedFiles, pathsFilter[name].paths_ignore)) { + core.info(`Commit ${commit.html_url} is path-ignored for '${name}': All of '${changedFiles}' match against patterns '${pathsFilter[name].paths_ignore}'`); + continue; + } + // Skippable if none of the changed files matches against paths. + if (pathsFilter[name].paths.length >= 1) { + const matches = this.getCommitPathsMatches(changedFiles, pathsFilter[name].paths); + if (matches.length === 0) { + core.info(`Commit ${commit.html_url} is path-skipped for '${name}': None of '${changedFiles}' matches against patterns '${pathsFilter[name].paths}'`); + continue; + } + else { + pathsResult[name].matched_files = matches; + } } + // Not ignorable or skippable. + pathsResult[name].should_skip = false; + pathsResult[name].backtrack_count = distanceToHEAD; + core.info(`Stop backtracking for '${name}' at commit ${commit.html_url} because '${changedFiles}' are not skippable against paths '${pathsFilter[name].paths}' or paths_ignore '${pathsFilter[name].paths_ignore}'`); } - // Not ignorable or skippable. - pathsResult[name].should_skip = false; - pathsResult[name].backtrack_count = distanceToHEAD; - core.info(`Stop backtracking for '${name}' at commit ${commit.html_url} because '${changedFiles}' are not skippable against paths '${pathsFilter[name].paths}' or paths_ignore '${pathsFilter[name].paths_ignore}'`); + // Should be never reached in practice; we expect that this loop aborts after 1-3 iterations. + if (distanceToHEAD++ >= 50) { + core.warning('Aborted commit-backtracing due to bad performance - Did you push an excessive number of ignored-path commits?'); + break; + } + } while (Object.keys(pathsResult).some(path => pathsResult[path].should_skip === 'unknown')); + return { changedFiles: allChangedFiles, pathsResult }; + }); + } + isCommitPathsIgnored(changedFiles, pathsIgnore) { + if (pathsIgnore.length === 0) { + return false; + } + const notIgnoredPaths = micromatch_1.default.not(changedFiles, pathsIgnore, this.globOptions); + return notIgnoredPaths.length === 0; + } + getCommitPathsMatches(changedFiles, paths) { + const matches = (0, micromatch_1.default)(changedFiles, paths, this.globOptions); + return matches; + } + fetchCommitDetails(sha) { + return __awaiter(this, void 0, void 0, function* () { + if (!sha) { + return null; } - // Should be never reached in practice; we expect that this loop aborts after 1-3 iterations. - if (distanceToHEAD++ >= 50) { - core.warning('Aborted commit-backtracing due to bad performance - Did you push an excessive number of ignored-path commits?'); - break; + try { + const res = yield this.context.octokit.rest.repos.getCommit(Object.assign(Object.assign({}, this.context.repo), { ref: sha })); + return res.data; } - } while (Object.keys(pathsResult).some(path => pathsResult[path].should_skip === 'unknown')); - return { changedFiles: allChangedFiles, pathsResult }; - }); -} -function findSuccessfulRun(treeHash, olderRuns) { - const matchingRuns = olderRuns.filter(run => run.treeHash === treeHash); - const successfulRun = matchingRuns.find(run => { - return run.status === 'completed' && run.conclusion === 'success'; - }); - return successfulRun; -} -const globOptions = { - dot: true // Match dotfiles. Otherwise dotfiles are ignored unless a "." is explicitly defined in the pattern. -}; -function isCommitPathsIgnored(changedFiles, pathsIgnore) { - if (pathsIgnore.length === 0) { - return false; + catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.warning(error); + } + core.warning(`Failed to retrieve commit ${sha}`); + return null; + } + }); } - const notIgnoredPaths = micromatch_1.default.not(changedFiles, pathsIgnore, globOptions); - return notIgnoredPaths.length === 0; -} -function getCommitPathsMatches(changedFiles, paths) { - const matches = (0, micromatch_1.default)(changedFiles, paths, globOptions); - return matches; } -function fetchCommitDetails(sha, context) { +function main() { + var _a, _b, _c; return __awaiter(this, void 0, void 0, function* () { - if (!sha) { - return null; - } - try { - const res = yield context.octokit.rest.repos.getCommit({ - owner: context.repoOwner, - repo: context.repoName, - ref: sha - }); - return res.data; + // Get and validate inputs. + const token = core.getInput('github_token', { required: true }); + const inputs = { + paths: getStringArrayInput('paths'), + pathsIgnore: getStringArrayInput('paths_ignore'), + pathsFilter: getPathsFilterInput('paths_filter'), + doNotSkip: getDoNotSkipInput('do_not_skip'), + concurrentSkipping: getConcurrentSkippingInput('concurrent_skipping'), + cancelOthers: (_a = core.getBooleanInput('cancel_others')) !== null && _a !== void 0 ? _a : false, + skipAfterSuccessfulDuplicates: (_b = core.getBooleanInput('skip_after_successful_duplicate')) !== null && _b !== void 0 ? _b : true + }; + const repo = github.context.repo; + const octokit = github.getOctokit(token); + // Get and parse the current workflow run. + const { data: workflowRun } = yield octokit.rest.actions.getWorkflowRun(Object.assign(Object.assign({}, repo), { run_id: github.context.runId })); + const treeHash = (_c = workflowRun.head_commit) === null || _c === void 0 ? void 0 : _c.tree_id; + if (!treeHash) { + exitFail(` + Could not find the tree hash of run ${workflowRun.id} (Workflow ID: ${workflowRun.workflow_id}, + Name: ${workflowRun.name}, Head Branch: ${workflowRun.head_branch}, Head SHA: ${workflowRun.head_sha}). + This might be a run associated with a headless or removed commit. + `); } - catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.warning(e); + const currentRun = mapWorkflowRun(workflowRun, treeHash); + // Fetch list of runs for current workflow. + const { data: { workflow_runs: workflowRuns } } = yield octokit.rest.actions.listWorkflowRuns(Object.assign(Object.assign({}, repo), { workflow_id: currentRun.workflowId, per_page: 100 })); + // Check and map all runs. + const allRuns = workflowRuns.reduce((result, run) => { + // Filter out current run and runs that lack 'head_commit' (most likely runs associated with a headless or removed commit). + // See https://github.com/fkirc/skip-duplicate-actions/pull/178. + if (run.id !== currentRun.id && run.head_commit) { + result.push(mapWorkflowRun(run, run.head_commit.tree_id)); } - core.warning(`Failed to retrieve commit ${sha}`); - return null; - } + return result; + }, []); + // List with older workflow runs only (used to prevent some nasty race conditions and edge cases). + const olderRuns = allRuns.filter(run => { + return (new Date(run.createdAt).getTime() < + new Date(currentRun.createdAt).getTime()); + }); + const skipDuplicateActions = new SkipDuplicateActions(inputs, { + repo, + octokit, + currentRun, + allRuns, + olderRuns + }); + yield skipDuplicateActions.run(); }); } +function mapWorkflowRun(run, treeHash) { + var _a, _b; + return { + id: run.id, + runNumber: run.run_number, + event: run.event, + treeHash, + commitHash: run.head_sha, + status: run.status, + conclusion: run.conclusion, + htmlUrl: run.html_url, + branch: run.head_branch, + // Wrong type: 'head_repository' can be null (probably when repo has been removed) + repo: (_b = (_a = run.head_repository) === null || _a === void 0 ? void 0 : _a.full_name) !== null && _b !== void 0 ? _b : null, + workflowId: run.workflow_id, + createdAt: run.created_at + }; +} +/** Set all outputs and exit the action. */ function exitSuccess(args) { core.setOutput('should_skip', args.shouldSkip); core.setOutput('reason', args.reason); core.setOutput('skipped_by', args.skippedBy || {}); core.setOutput('changed_files', args.changedFiles || []); core.setOutput('paths_result', args.pathsResult || {}); - return process.exit(0); -} -function formatCliOptions(options) { - return `${options.map(o => `"${o}"`).join(', ')}`; + process.exit(0); } -function getConcurrentSkippingInput(name) { - const rawInput = core.getInput(name, { required: true }); - if (rawInput.toLowerCase() === 'false') { - return 'never'; // Backwards-compat - } - else if (rawInput.toLowerCase() === 'true') { - return 'same_content'; // Backwards-compat - } - const options = getConcurrentSkippingOptions(); - if (options.includes(rawInput)) { - return rawInput; - } - else { - logFatal(`'${name}' must be one of ${formatCliOptions(options)}`); +/** Immediately terminate the action with failing exit code. */ +function exitFail(error) { + if (error instanceof Error || typeof error == 'string') { + core.error(error); } + process.exit(1); } -function getBooleanInput(name, defaultValue) { - const rawInput = core.getInput(name, { required: false }); +function getStringArrayInput(name) { + const rawInput = core.getInput(name); if (!rawInput) { - return defaultValue; + return []; } - if (defaultValue) { - return rawInput.toLowerCase() !== 'false'; + try { + const array = JSON.parse(rawInput); + if (!Array.isArray(array)) { + exitFail(`Input '${rawInput}' is not a JSON-array`); + } + for (const element of array) { + if (typeof element !== 'string') { + exitFail(`Element '${element}' of input '${rawInput}' is not a string`); + } + } + return array; } - else { - return rawInput.toLowerCase() === 'true'; + catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.error(error); + } + exitFail(`Input '${rawInput}' is not a valid JSON`); } } -function getStringArrayInput(name) { - const rawInput = core.getInput(name, { required: false }); +function getDoNotSkipInput(name) { + const rawInput = core.getInput(name); if (!rawInput) { return []; } try { const array = JSON.parse(rawInput); if (!Array.isArray(array)) { - logFatal(`Input '${rawInput}' is not a JSON-array`); + exitFail(`Input '${rawInput}' is not a JSON-array`); } - for (const e of array) { - if (typeof e !== 'string') { - logFatal(`Element '${e}' of input '${rawInput}' is not a string`); + for (const element of array) { + if (!workflowRunTriggerOptions.includes(element)) { + exitFail(`Elements in '${name}' must be one of ${workflowRunTriggerOptions + .map(option => `"${option}"`) + .join(', ')}`); } } return array; } - catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.error(e); + catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.error(error); } - logFatal(`Input '${rawInput}' is not a valid JSON`); + exitFail(`Input '${rawInput}' is not a valid JSON`); + } +} +function getConcurrentSkippingInput(name) { + const rawInput = core.getInput(name, { required: true }); + if (rawInput.toLowerCase() === 'false') { + return 'never'; // Backwards-compat + } + else if (rawInput.toLowerCase() === 'true') { + return 'same_content'; // Backwards-compat + } + if (concurrentSkippingOptions.includes(rawInput)) { + return rawInput; + } + else { + exitFail(`'${name}' must be one of ${concurrentSkippingOptions + .map(option => `"${option}"`) + .join(', ')}`); } } function getPathsFilterInput(name) { - const rawInput = core.getInput(name, { required: false }); + const rawInput = core.getInput(name); if (!rawInput) { return {}; } try { const input = js_yaml_1.default.load(rawInput); - // Assign default values + // Assign default values to each entry const pathsFilter = {}; for (const [key, value] of Object.entries(input)) { pathsFilter[key] = { @@ -510,17 +479,13 @@ function getPathsFilterInput(name) { } return pathsFilter; } - catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.error(e); + catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.error(error); } - logFatal(`Input '${rawInput}' is invalid`); + exitFail(`Input '${rawInput}' is invalid`); } } -function logFatal(msg) { - core.setFailed(msg); - return process.exit(1); -} main(); @@ -2561,7 +2526,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); var universalUserAgent = __nccwpck_require__(5030); var beforeAfterHook = __nccwpck_require__(3682); var request = __nccwpck_require__(6234); -var graphql = __nccwpck_require__(8467); +var graphql = __nccwpck_require__(6442); var authToken = __nccwpck_require__(334); function _objectWithoutPropertiesLoose(source, excluded) { @@ -2732,6 +2697,132 @@ exports.Octokit = Octokit; //# sourceMappingURL=index.js.map +/***/ }), + +/***/ 6442: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ value: true })); + +var request = __nccwpck_require__(6234); +var universalUserAgent = __nccwpck_require__(5030); + +const VERSION = "4.8.0"; + +function _buildMessageForResponseErrors(data) { + return `Request failed due to following response errors:\n` + data.errors.map(e => ` - ${e.message}`).join("\n"); +} + +class GraphqlResponseError extends Error { + constructor(request, headers, response) { + super(_buildMessageForResponseErrors(response)); + this.request = request; + this.headers = headers; + this.response = response; + this.name = "GraphqlResponseError"; // Expose the errors and response data in their shorthand properties. + + this.errors = response.errors; + this.data = response.data; // Maintains proper stack trace (only available on V8) + + /* istanbul ignore next */ + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } + +} + +const NON_VARIABLE_OPTIONS = ["method", "baseUrl", "url", "headers", "request", "query", "mediaType"]; +const FORBIDDEN_VARIABLE_OPTIONS = ["query", "method", "url"]; +const GHES_V3_SUFFIX_REGEX = /\/api\/v3\/?$/; +function graphql(request, query, options) { + if (options) { + if (typeof query === "string" && "query" in options) { + return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`)); + } + + for (const key in options) { + if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key)) continue; + return Promise.reject(new Error(`[@octokit/graphql] "${key}" cannot be used as variable name`)); + } + } + + const parsedOptions = typeof query === "string" ? Object.assign({ + query + }, options) : query; + const requestOptions = Object.keys(parsedOptions).reduce((result, key) => { + if (NON_VARIABLE_OPTIONS.includes(key)) { + result[key] = parsedOptions[key]; + return result; + } + + if (!result.variables) { + result.variables = {}; + } + + result.variables[key] = parsedOptions[key]; + return result; + }, {}); // workaround for GitHub Enterprise baseUrl set with /api/v3 suffix + // https://github.com/octokit/auth-app.js/issues/111#issuecomment-657610451 + + const baseUrl = parsedOptions.baseUrl || request.endpoint.DEFAULTS.baseUrl; + + if (GHES_V3_SUFFIX_REGEX.test(baseUrl)) { + requestOptions.url = baseUrl.replace(GHES_V3_SUFFIX_REGEX, "/api/graphql"); + } + + return request(requestOptions).then(response => { + if (response.data.errors) { + const headers = {}; + + for (const key of Object.keys(response.headers)) { + headers[key] = response.headers[key]; + } + + throw new GraphqlResponseError(requestOptions, headers, response.data); + } + + return response.data.data; + }); +} + +function withDefaults(request$1, newDefaults) { + const newRequest = request$1.defaults(newDefaults); + + const newApi = (query, options) => { + return graphql(newRequest, query, options); + }; + + return Object.assign(newApi, { + defaults: withDefaults.bind(null, newRequest), + endpoint: request.request.endpoint + }); +} + +const graphql$1 = withDefaults(request.request, { + headers: { + "user-agent": `octokit-graphql.js/${VERSION} ${universalUserAgent.getUserAgent()}` + }, + method: "POST", + url: "/graphql" +}); +function withCustomRequest(customRequest) { + return withDefaults(customRequest, { + method: "POST", + url: "/graphql" + }); +} + +exports.GraphqlResponseError = GraphqlResponseError; +exports.graphql = graphql$1; +exports.withCustomRequest = withCustomRequest; +//# sourceMappingURL=index.js.map + + /***/ }), /***/ 9440: @@ -3130,132 +3221,6 @@ exports.endpoint = endpoint; //# sourceMappingURL=index.js.map -/***/ }), - -/***/ 8467: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { - -"use strict"; - - -Object.defineProperty(exports, "__esModule", ({ value: true })); - -var request = __nccwpck_require__(6234); -var universalUserAgent = __nccwpck_require__(5030); - -const VERSION = "4.8.0"; - -function _buildMessageForResponseErrors(data) { - return `Request failed due to following response errors:\n` + data.errors.map(e => ` - ${e.message}`).join("\n"); -} - -class GraphqlResponseError extends Error { - constructor(request, headers, response) { - super(_buildMessageForResponseErrors(response)); - this.request = request; - this.headers = headers; - this.response = response; - this.name = "GraphqlResponseError"; // Expose the errors and response data in their shorthand properties. - - this.errors = response.errors; - this.data = response.data; // Maintains proper stack trace (only available on V8) - - /* istanbul ignore next */ - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } - } - -} - -const NON_VARIABLE_OPTIONS = ["method", "baseUrl", "url", "headers", "request", "query", "mediaType"]; -const FORBIDDEN_VARIABLE_OPTIONS = ["query", "method", "url"]; -const GHES_V3_SUFFIX_REGEX = /\/api\/v3\/?$/; -function graphql(request, query, options) { - if (options) { - if (typeof query === "string" && "query" in options) { - return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`)); - } - - for (const key in options) { - if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key)) continue; - return Promise.reject(new Error(`[@octokit/graphql] "${key}" cannot be used as variable name`)); - } - } - - const parsedOptions = typeof query === "string" ? Object.assign({ - query - }, options) : query; - const requestOptions = Object.keys(parsedOptions).reduce((result, key) => { - if (NON_VARIABLE_OPTIONS.includes(key)) { - result[key] = parsedOptions[key]; - return result; - } - - if (!result.variables) { - result.variables = {}; - } - - result.variables[key] = parsedOptions[key]; - return result; - }, {}); // workaround for GitHub Enterprise baseUrl set with /api/v3 suffix - // https://github.com/octokit/auth-app.js/issues/111#issuecomment-657610451 - - const baseUrl = parsedOptions.baseUrl || request.endpoint.DEFAULTS.baseUrl; - - if (GHES_V3_SUFFIX_REGEX.test(baseUrl)) { - requestOptions.url = baseUrl.replace(GHES_V3_SUFFIX_REGEX, "/api/graphql"); - } - - return request(requestOptions).then(response => { - if (response.data.errors) { - const headers = {}; - - for (const key of Object.keys(response.headers)) { - headers[key] = response.headers[key]; - } - - throw new GraphqlResponseError(requestOptions, headers, response.data); - } - - return response.data.data; - }); -} - -function withDefaults(request$1, newDefaults) { - const newRequest = request$1.defaults(newDefaults); - - const newApi = (query, options) => { - return graphql(newRequest, query, options); - }; - - return Object.assign(newApi, { - defaults: withDefaults.bind(null, newRequest), - endpoint: request.request.endpoint - }); -} - -const graphql$1 = withDefaults(request.request, { - headers: { - "user-agent": `octokit-graphql.js/${VERSION} ${universalUserAgent.getUserAgent()}` - }, - method: "POST", - url: "/graphql" -}); -function withCustomRequest(customRequest) { - return withDefaults(customRequest, { - method: "POST", - url: "/graphql" - }); -} - -exports.GraphqlResponseError = GraphqlResponseError; -exports.graphql = graphql$1; -exports.withCustomRequest = withCustomRequest; -//# sourceMappingURL=index.js.map - - /***/ }), /***/ 4193: diff --git a/package-lock.json b/package-lock.json index 162e9cb..c05d16c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,12 +66,12 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.0.tgz", - "integrity": "sha512-JyXXoCu1N8GLuKc2ii8y5RGma5FMpFeO2nAQIe0Yzrbq+rQnN+sFj47auLblR5ka6aHNGPDgv8G/iI2Grb0ldQ==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.1.tgz", + "integrity": "sha512-j2vJGnkopRzH+ykJ8h68wrHnEUmtK//E723jjixiAl/PPf6FhqY/vYRcMVlNydRKQjQsTsYEjpx+DZMIvnGk/g==", "dev": true, "dependencies": { - "core-js-pure": "^3.20.2", + "core-js-pure": "^3.25.1", "regenerator-runtime": "^0.13.4" }, "engines": { @@ -201,23 +201,23 @@ "universal-user-agent": "^6.0.0" } }, - "node_modules/@octokit/endpoint": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", - "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "node_modules/@octokit/core/node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", "dependencies": { + "@octokit/request": "^5.6.0", "@octokit/types": "^6.0.3", - "is-plain-object": "^5.0.0", "universal-user-agent": "^6.0.0" } }, - "node_modules/@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", "dependencies": { - "@octokit/request": "^5.6.0", "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", "universal-user-agent": "^6.0.0" } }, @@ -314,22 +314,21 @@ } }, "node_modules/@types/node": { - "version": "16.11.58", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.58.tgz", - "integrity": "sha512-uMVxJ111wpHzkx/vshZFb6Qni3BOMnlWLq7q9jrwej7Yw/KvjsEbpxCCxw+hLKxexFMc8YmpG8J9tnEe/rKsIg==", + "version": "16.11.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.59.tgz", + "integrity": "sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz", - "integrity": "sha512-OwwR8LRwSnI98tdc2z7mJYgY60gf7I9ZfGjN5EjCwwns9bdTuQfAXcsjSB2wSQ/TVNYSGKf4kzVXbNGaZvwiXw==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz", + "integrity": "sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.36.2", - "@typescript-eslint/type-utils": "5.36.2", - "@typescript-eslint/utils": "5.36.2", + "@typescript-eslint/scope-manager": "5.38.0", + "@typescript-eslint/type-utils": "5.38.0", + "@typescript-eslint/utils": "5.38.0", "debug": "^4.3.4", - "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", "regexpp": "^3.2.0", "semver": "^7.3.7", @@ -353,14 +352,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.36.2.tgz", - "integrity": "sha512-qS/Kb0yzy8sR0idFspI9Z6+t7mqk/oRjnAYfewG+VN73opAUvmYL3oPIMmgOX6CnQS6gmVIXGshlb5RY/R22pA==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.0.tgz", + "integrity": "sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.36.2", - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/typescript-estree": "5.36.2", + "@typescript-eslint/scope-manager": "5.38.0", + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/typescript-estree": "5.38.0", "debug": "^4.3.4" }, "engines": { @@ -380,13 +379,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.36.2.tgz", - "integrity": "sha512-cNNP51L8SkIFSfce8B1NSUBTJTu2Ts4nWeWbFrdaqjmn9yKrAaJUBHkyTZc0cL06OFHpb+JZq5AUHROS398Orw==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz", + "integrity": "sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/visitor-keys": "5.36.2" + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/visitor-keys": "5.38.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -397,13 +396,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.36.2.tgz", - "integrity": "sha512-rPQtS5rfijUWLouhy6UmyNquKDPhQjKsaKH0WnY6hl/07lasj8gPaH2UD8xWkePn6SC+jW2i9c2DZVDnL+Dokw==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz", + "integrity": "sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.36.2", - "@typescript-eslint/utils": "5.36.2", + "@typescript-eslint/typescript-estree": "5.38.0", + "@typescript-eslint/utils": "5.38.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -424,9 +423,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.36.2.tgz", - "integrity": "sha512-9OJSvvwuF1L5eS2EQgFUbECb99F0mwq501w0H0EkYULkhFa19Qq7WFbycdw1PexAc929asupbZcgjVIe6OK/XQ==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.0.tgz", + "integrity": "sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -437,13 +436,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.2.tgz", - "integrity": "sha512-8fyH+RfbKc0mTspfuEjlfqA4YywcwQK2Amcf6TDOwaRLg7Vwdu4bZzyvBZp4bjt1RRjQ5MDnOZahxMrt2l5v9w==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz", + "integrity": "sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/visitor-keys": "5.36.2", + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/visitor-keys": "5.38.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -464,15 +463,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.36.2.tgz", - "integrity": "sha512-uNcopWonEITX96v9pefk9DC1bWMdkweeSsewJ6GeC7L6j2t0SJywisgkr9wUTtXk90fi2Eljj90HSHm3OGdGRg==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.0.tgz", + "integrity": "sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.36.2", - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/typescript-estree": "5.36.2", + "@typescript-eslint/scope-manager": "5.38.0", + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/typescript-estree": "5.38.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -488,12 +487,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.2.tgz", - "integrity": "sha512-BtRvSR6dEdrNt7Net2/XDjbYKU5Ml6GqJgVfXT0CxTCJlnIqK7rAGreuWKMT2t8cFUT2Msv5oxw0GMRD7T5J7A==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz", + "integrity": "sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.36.2", + "@typescript-eslint/types": "5.38.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -692,9 +691,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "funding": [ { @@ -707,10 +706,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001370", - "electron-to-chromium": "^1.4.202", + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.5" + "update-browserslist-db": "^1.0.9" }, "bin": { "browserslist": "cli.js" @@ -742,9 +741,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001397", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz", - "integrity": "sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA==", + "version": "1.0.30001406", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001406.tgz", + "integrity": "sha512-bWTlaXUy/rq0BBtYShc/jArYfBPjEV95euvZ8JVtO43oQExEN/WquoqpufFjNu4kSpi5cy5kMbNvzztWDfv1Jg==", "dev": true, "funding": [ { @@ -798,9 +797,9 @@ "dev": true }, "node_modules/core-js-pure": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.1.tgz", - "integrity": "sha512-7Fr74bliUDdeJCBMxkkIuQ4xfxn/SwrVg+HkJUAoNEXVqYLv55l6Af0dJ5Lq2YBUW9yKqSkLXaS5SYPK6MGa/A==", + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.2.tgz", + "integrity": "sha512-ItD7YpW1cUB4jaqFLZXe1AXkyqIxz6GqPnsDV4uF4hVcWh/WAGIqSqw5p0/WdsILM0Xht9s3Koyw05R3K6RtiA==", "dev": true, "hasInstallScript": true, "funding": { @@ -897,9 +896,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.247", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.247.tgz", - "integrity": "sha512-FLs6R4FQE+1JHM0hh3sfdxnYjKvJpHZyhQDjc2qFq/xFvmmRt/TATNToZhrcGUFzpF2XjeiuozrA8lI0PZmYYw==", + "version": "1.4.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.254.tgz", + "integrity": "sha512-Sh/7YsHqQYkA6ZHuHMy24e6TE4eX6KZVsZb9E/DvU1nQRIrH4BflO/4k+83tfdYvDl+MObvlqHPRICzEdC9c6Q==", "dev": true }, "node_modules/emoji-regex": { @@ -1624,12 +1623,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -1640,9 +1633,9 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", "dev": true, "dependencies": { "function-bind": "^1.1.1", @@ -1904,9 +1897,9 @@ } }, "node_modules/is-callable": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.5.tgz", - "integrity": "sha512-ZIWRujF6MvYGkEuHMYtFRkL2wAtFw89EHfKlXrkPkjQZZRWeh9L1q3SV13NIfHnqxugjLvAOkEHx9mb1zcMnEw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.6.tgz", + "integrity": "sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -2925,9 +2918,9 @@ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, "node_modules/update-browserslist-db": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.7.tgz", - "integrity": "sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", + "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", "dev": true, "funding": [ { @@ -3084,12 +3077,12 @@ } }, "@babel/runtime-corejs3": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.0.tgz", - "integrity": "sha512-JyXXoCu1N8GLuKc2ii8y5RGma5FMpFeO2nAQIe0Yzrbq+rQnN+sFj47auLblR5ka6aHNGPDgv8G/iI2Grb0ldQ==", + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.19.1.tgz", + "integrity": "sha512-j2vJGnkopRzH+ykJ8h68wrHnEUmtK//E723jjixiAl/PPf6FhqY/vYRcMVlNydRKQjQsTsYEjpx+DZMIvnGk/g==", "dev": true, "requires": { - "core-js-pure": "^3.20.2", + "core-js-pure": "^3.25.1", "regenerator-runtime": "^0.13.4" } }, @@ -3185,6 +3178,18 @@ "@octokit/types": "^6.0.3", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "requires": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + } } }, "@octokit/endpoint": { @@ -3197,16 +3202,6 @@ "universal-user-agent": "^6.0.0" } }, - "@octokit/graphql": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", - "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", - "requires": { - "@octokit/request": "^5.6.0", - "@octokit/types": "^6.0.3", - "universal-user-agent": "^6.0.0" - } - }, "@octokit/openapi-types": { "version": "12.11.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", @@ -3294,22 +3289,21 @@ } }, "@types/node": { - "version": "16.11.58", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.58.tgz", - "integrity": "sha512-uMVxJ111wpHzkx/vshZFb6Qni3BOMnlWLq7q9jrwej7Yw/KvjsEbpxCCxw+hLKxexFMc8YmpG8J9tnEe/rKsIg==", + "version": "16.11.59", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.59.tgz", + "integrity": "sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw==", "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz", - "integrity": "sha512-OwwR8LRwSnI98tdc2z7mJYgY60gf7I9ZfGjN5EjCwwns9bdTuQfAXcsjSB2wSQ/TVNYSGKf4kzVXbNGaZvwiXw==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz", + "integrity": "sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.36.2", - "@typescript-eslint/type-utils": "5.36.2", - "@typescript-eslint/utils": "5.36.2", + "@typescript-eslint/scope-manager": "5.38.0", + "@typescript-eslint/type-utils": "5.38.0", + "@typescript-eslint/utils": "5.38.0", "debug": "^4.3.4", - "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", "regexpp": "^3.2.0", "semver": "^7.3.7", @@ -3317,53 +3311,53 @@ } }, "@typescript-eslint/parser": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.36.2.tgz", - "integrity": "sha512-qS/Kb0yzy8sR0idFspI9Z6+t7mqk/oRjnAYfewG+VN73opAUvmYL3oPIMmgOX6CnQS6gmVIXGshlb5RY/R22pA==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.0.tgz", + "integrity": "sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.36.2", - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/typescript-estree": "5.36.2", + "@typescript-eslint/scope-manager": "5.38.0", + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/typescript-estree": "5.38.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.36.2.tgz", - "integrity": "sha512-cNNP51L8SkIFSfce8B1NSUBTJTu2Ts4nWeWbFrdaqjmn9yKrAaJUBHkyTZc0cL06OFHpb+JZq5AUHROS398Orw==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz", + "integrity": "sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/visitor-keys": "5.36.2" + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/visitor-keys": "5.38.0" } }, "@typescript-eslint/type-utils": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.36.2.tgz", - "integrity": "sha512-rPQtS5rfijUWLouhy6UmyNquKDPhQjKsaKH0WnY6hl/07lasj8gPaH2UD8xWkePn6SC+jW2i9c2DZVDnL+Dokw==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz", + "integrity": "sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.36.2", - "@typescript-eslint/utils": "5.36.2", + "@typescript-eslint/typescript-estree": "5.38.0", + "@typescript-eslint/utils": "5.38.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.36.2.tgz", - "integrity": "sha512-9OJSvvwuF1L5eS2EQgFUbECb99F0mwq501w0H0EkYULkhFa19Qq7WFbycdw1PexAc929asupbZcgjVIe6OK/XQ==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.0.tgz", + "integrity": "sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.2.tgz", - "integrity": "sha512-8fyH+RfbKc0mTspfuEjlfqA4YywcwQK2Amcf6TDOwaRLg7Vwdu4bZzyvBZp4bjt1RRjQ5MDnOZahxMrt2l5v9w==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz", + "integrity": "sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/visitor-keys": "5.36.2", + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/visitor-keys": "5.38.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3372,26 +3366,26 @@ } }, "@typescript-eslint/utils": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.36.2.tgz", - "integrity": "sha512-uNcopWonEITX96v9pefk9DC1bWMdkweeSsewJ6GeC7L6j2t0SJywisgkr9wUTtXk90fi2Eljj90HSHm3OGdGRg==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.0.tgz", + "integrity": "sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.36.2", - "@typescript-eslint/types": "5.36.2", - "@typescript-eslint/typescript-estree": "5.36.2", + "@typescript-eslint/scope-manager": "5.38.0", + "@typescript-eslint/types": "5.38.0", + "@typescript-eslint/typescript-estree": "5.38.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } }, "@typescript-eslint/visitor-keys": { - "version": "5.36.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.2.tgz", - "integrity": "sha512-BtRvSR6dEdrNt7Net2/XDjbYKU5Ml6GqJgVfXT0CxTCJlnIqK7rAGreuWKMT2t8cFUT2Msv5oxw0GMRD7T5J7A==", + "version": "5.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz", + "integrity": "sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==", "dev": true, "requires": { - "@typescript-eslint/types": "5.36.2", + "@typescript-eslint/types": "5.38.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -3535,15 +3529,15 @@ } }, "browserslist": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz", - "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001370", - "electron-to-chromium": "^1.4.202", + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.5" + "update-browserslist-db": "^1.0.9" } }, "call-bind": { @@ -3563,9 +3557,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001397", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz", - "integrity": "sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA==", + "version": "1.0.30001406", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001406.tgz", + "integrity": "sha512-bWTlaXUy/rq0BBtYShc/jArYfBPjEV95euvZ8JVtO43oQExEN/WquoqpufFjNu4kSpi5cy5kMbNvzztWDfv1Jg==", "dev": true }, "chalk": { @@ -3600,9 +3594,9 @@ "dev": true }, "core-js-pure": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.1.tgz", - "integrity": "sha512-7Fr74bliUDdeJCBMxkkIuQ4xfxn/SwrVg+HkJUAoNEXVqYLv55l6Af0dJ5Lq2YBUW9yKqSkLXaS5SYPK6MGa/A==", + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.25.2.tgz", + "integrity": "sha512-ItD7YpW1cUB4jaqFLZXe1AXkyqIxz6GqPnsDV4uF4hVcWh/WAGIqSqw5p0/WdsILM0Xht9s3Koyw05R3K6RtiA==", "dev": true }, "cross-spawn": { @@ -3671,9 +3665,9 @@ } }, "electron-to-chromium": { - "version": "1.4.247", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.247.tgz", - "integrity": "sha512-FLs6R4FQE+1JHM0hh3sfdxnYjKvJpHZyhQDjc2qFq/xFvmmRt/TATNToZhrcGUFzpF2XjeiuozrA8lI0PZmYYw==", + "version": "1.4.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.254.tgz", + "integrity": "sha512-Sh/7YsHqQYkA6ZHuHMy24e6TE4eX6KZVsZb9E/DvU1nQRIrH4BflO/4k+83tfdYvDl+MObvlqHPRICzEdC9c6Q==", "dev": true }, "emoji-regex": { @@ -4238,12 +4232,6 @@ "functions-have-names": "^1.2.2" } }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true - }, "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -4251,9 +4239,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", - "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", "dev": true, "requires": { "function-bind": "^1.1.1", @@ -4437,9 +4425,9 @@ } }, "is-callable": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.5.tgz", - "integrity": "sha512-ZIWRujF6MvYGkEuHMYtFRkL2wAtFw89EHfKlXrkPkjQZZRWeh9L1q3SV13NIfHnqxugjLvAOkEHx9mb1zcMnEw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.6.tgz", + "integrity": "sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q==", "dev": true }, "is-core-module": { @@ -5143,9 +5131,9 @@ "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, "update-browserslist-db": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.7.tgz", - "integrity": "sha512-iN/XYesmZ2RmmWAiI4Z5rq0YqSiv0brj9Ce9CfhNE4xIW2h+MFxcgkxIzZ+ShkFPUkjU3gQ+3oypadD3RAMtrg==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz", + "integrity": "sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==", "dev": true, "requires": { "escalade": "^3.1.1", diff --git a/src/main.ts b/src/main.ts index 62856d1..68f5cc1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,14 +5,23 @@ import {GitHub} from '@actions/github/lib/utils' import micromatch from 'micromatch' import yaml from 'js-yaml' -type ActionsGetWorkflowRunResponseData = +type ApiWorkflowRun = Endpoints['GET /repos/{owner}/{repo}/actions/runs/{run_id}']['response']['data'] -type ActionsListWorkflowRunsResponseData = - Endpoints['GET /repos/{owner}/{repo}/actions/runs']['response']['data'] -type ReposGetCommitResponseData = +type ApiWorkflowRuns = + Endpoints['GET /repos/{owner}/{repo}/actions/runs']['response']['data']['workflow_runs'][number] +type ApiCommit = Endpoints['GET /repos/{owner}/{repo}/commits/{ref}']['response']['data'] +const workflowRunTriggerOptions = [ + 'pull_request', + 'push', + 'workflow_dispatch', + 'schedule' +] as const +type WorkflowRunTrigger = typeof workflowRunTriggerOptions[number] + type WorkflowRunStatus = 'queued' | 'in_progress' | 'completed' + type WorkflowRunConclusion = | 'success' | 'failure' @@ -21,200 +30,104 @@ type WorkflowRunConclusion = | 'skipped' | 'timed_out' -const concurrentSkippingMap = { - always: null, - same_content: null, - same_content_newer: null, - outdated_runs: null, - never: null -} -function getConcurrentSkippingOptions(): string[] { - return Object.keys(concurrentSkippingMap) -} -type ConcurrentSkippingOption = keyof typeof concurrentSkippingMap - interface WorkflowRun { - event: WRunTrigger + id: number + runNumber: number + event: WorkflowRunTrigger treeHash: string commitHash: string - status: WorkflowRunStatus + status: WorkflowRunStatus | null conclusion: WorkflowRunConclusion | null - html_url: string + htmlUrl: string branch: string | null repo: string | null - runId: number workflowId: number createdAt: string - runNumber: number } -type WRunTrigger = 'pull_request' | 'push' | 'workflow_dispatch' | 'schedule' - -interface PathsFilterEntry { - paths_ignore: string[] +type PathsFilter = Record< + string, + { + paths_ignore: string[] + paths: string[] + backtracking: boolean | number + } +> + +type PathsResult = Record< + string, + { + should_skip: 'unknown' | boolean + backtrack_count: number + skipped_by?: WorkflowRun + matched_files?: string[] + } +> + +const concurrentSkippingOptions = [ + 'always', + 'same_content', + 'same_content_newer', + 'outdated_runs', + 'never' +] as const +type ConcurrentSkipping = typeof concurrentSkippingOptions[number] + +type Inputs = { paths: string[] - backtracking: boolean | number -} -type PathsFilter = Record - -interface WRunContext { - repoOwner: string - repoName: string - currentRun: WorkflowRun - olderRuns: WorkflowRun[] - allRuns: WorkflowRun[] - octokit: InstanceType pathsIgnore: string[] - paths: string[] pathsFilter: PathsFilter - doNotSkip: WRunTrigger[] - concurrentSkipping: ConcurrentSkippingOption -} - -interface PathsResultEntry { - should_skip: 'unknown' | boolean - backtrack_count: number - skipped_by?: WorkflowRun - matched_files?: string[] -} - -type PathsResult = Record - -function parseWorkflowRun(run: ActionsGetWorkflowRunResponseData): WorkflowRun { - const treeHash = run.head_commit?.tree_id - if (!treeHash) { - logFatal(` - Could not find the tree hash of run ${run.id} (workflow: $ {run.workflow_id}, - name: ${run.name}, head_branch: ${run.head_branch}, head_sha: ${run.head_sha}). - You might have a run associated with a headless or removed commit. - `) - } - const workflowId = run.workflow_id - if (!workflowId) { - logFatal(`Could not find the workflow ID of run ${run.id}`) - } - return { - event: run.event as WRunTrigger, - treeHash, - commitHash: run.head_sha, - status: run.status as WorkflowRunStatus, - conclusion: (run.conclusion as WorkflowRunConclusion) ?? null, - html_url: run.html_url, - branch: run.head_branch ?? null, - repo: run.head_repository.full_name ?? null, - runId: run.id, - workflowId, - createdAt: run.created_at, - runNumber: run.run_number - } -} - -function parseAllRuns( - response: ActionsListWorkflowRunsResponseData -): WorkflowRun[] { - return response.workflow_runs - .filter(run => run.head_commit && run.workflow_id) - .map(run => parseWorkflowRun(run)) + doNotSkip: WorkflowRunTrigger[] + concurrentSkipping: ConcurrentSkipping + cancelOthers: boolean + skipAfterSuccessfulDuplicates: boolean } -function parseOlderRuns( - response: ActionsListWorkflowRunsResponseData, +type Context = { + repo: {owner: string; repo: string} + octokit: InstanceType currentRun: WorkflowRun -): WorkflowRun[] { - const olderRuns = response.workflow_runs.filter(run => { - // Only consider older workflow runs to prevent some nasty race conditions and edge cases. - return ( - new Date(run.created_at).getTime() < - new Date(currentRun.createdAt).getTime() - ) - }) - return olderRuns - .filter(run => run.head_commit && run.workflow_id) - .map(run => parseWorkflowRun(run)) + allRuns: WorkflowRun[] + olderRuns: WorkflowRun[] } -async function main(): Promise { - try { - const token = core.getInput('github_token', {required: true}) - if (!token) { - logFatal('Did not find github_token') - } - const repo = github.context.repo - const repoOwner = repo?.owner - if (!repoOwner) { - logFatal('Did not find the repo owner') - } - const repoName = repo?.repo - if (!repoName) { - logFatal('Did not find the repo name') - } - const runId = github.context.runId - if (!runId) { - logFatal('Did not find runId') - } +class SkipDuplicateActions { + inputs: Inputs + context: Context + globOptions = { + dot: true // Match dotfiles. Otherwise dotfiles are ignored unless a "." is explicitly defined in the pattern. + } - let context: Readonly - try { - const octokit = github.getOctokit(token) - const {data: current_run} = await octokit.rest.actions.getWorkflowRun({ - owner: repoOwner, - repo: repoName, - run_id: runId - }) - const currentRun = parseWorkflowRun(current_run) + constructor(inputs: Inputs, context: Context) { + this.inputs = inputs + this.context = context + } - const {data} = await octokit.rest.actions.listWorkflowRuns({ - owner: repoOwner, - repo: repoName, - workflow_id: currentRun.workflowId, - per_page: 100 - }) - context = { - repoOwner, - repoName, - currentRun, - olderRuns: parseOlderRuns(data, currentRun), - allRuns: parseAllRuns(data), - octokit, - pathsIgnore: getStringArrayInput('paths_ignore'), - paths: getStringArrayInput('paths'), - pathsFilter: getPathsFilterInput('paths_filter'), - doNotSkip: getStringArrayInput('do_not_skip') as WRunTrigger[], - concurrentSkipping: getConcurrentSkippingInput('concurrent_skipping') - } - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.warning(e) - } - core.warning('Failed to fetch the required workflow information') - exitSuccess({ - shouldSkip: false, - reason: 'no_workflow_information' - }) + async run(): Promise { + // Cancel outdated runs. + if (this.inputs.cancelOthers) { + await this.cancelOutdatedRuns() } - const cancelOthers = getBooleanInput('cancel_others', false) - if (cancelOthers) { - await cancelOutdatedRuns(context) - } - if (context.doNotSkip.includes(context.currentRun.event)) { + // Abort early if current run has been triggered by an event that should never be skipped. + if (this.inputs.doNotSkip.includes(this.context.currentRun.event)) { core.info( - `Do not skip execution because the workflow was triggered with '${context.currentRun.event}'` + `Do not skip execution because the workflow was triggered with '${this.context.currentRun.event}'` ) exitSuccess({ shouldSkip: false, reason: 'do_not_skip' }) } - const skipAfterSuccessfulDuplicates = getBooleanInput( - 'skip_after_successful_duplicate', - true - ) - if (skipAfterSuccessfulDuplicates) { - const successfulDuplicateRun = detectSuccessfulDuplicateRuns(context) + + // Skip on successful duplicate run. + if (this.inputs.skipAfterSuccessfulDuplicates) { + const successfulDuplicateRun = this.findSuccessfulDuplicateRun( + this.context.currentRun.treeHash + ) if (successfulDuplicateRun) { core.info( - `Skip execution because the exact same files have been successfully checked in run ${successfulDuplicateRun.html_url}` + `Skip execution because the exact same files have been successfully checked in run ${successfulDuplicateRun.htmlUrl}` ) exitSuccess({ shouldSkip: true, @@ -223,8 +136,10 @@ async function main(): Promise { }) } } - if (context.concurrentSkipping !== 'never') { - const concurrentRun = detectConcurrentRuns(context) + + // Skip on concurrent runs. + if (this.inputs.concurrentSkipping !== 'never') { + const concurrentRun = this.detectConcurrentRuns() if (concurrentRun) { exitSuccess({ shouldSkip: true, @@ -233,12 +148,14 @@ async function main(): Promise { }) } } + + // Skip on path matches. if ( - context.paths.length >= 1 || - context.pathsIgnore.length >= 1 || - Object.keys(context.pathsFilter).length >= 1 + this.inputs.paths.length >= 1 || + this.inputs.pathsIgnore.length >= 1 || + Object.keys(this.inputs.pathsFilter).length >= 1 ) { - const {changedFiles, pathsResult} = await backtracePathSkipping(context) + const {changedFiles, pathsResult} = await this.backtracePathSkipping() exitSuccess({ shouldSkip: pathsResult.global.should_skip === 'unknown' @@ -250,6 +167,8 @@ async function main(): Promise { pathsResult }) } + + // Do not skip otherwise. core.info( 'Do not skip execution because we did not find a transferable run' ) @@ -257,294 +176,353 @@ async function main(): Promise { shouldSkip: false, reason: 'no_transferable_run' }) - } catch (e) { - if (e instanceof Error) { - core.error(e) - logFatal(e.message) - } } -} -async function cancelOutdatedRuns(context: WRunContext): Promise { - const currentRun = context.currentRun - const cancelVictims = context.olderRuns.filter(run => { - if (run.status === 'completed') { - return false + async cancelOutdatedRuns(): Promise { + const cancelVictims = this.context.olderRuns.filter(run => { + // Only cancel runs which are not yet completed. + if (run.status === 'completed') { + return false + } + // Only cancel runs from same branch and repo (ignore pull request runs from remote repositories) + // and not with same tree hash. + // See https://github.com/fkirc/skip-duplicate-actions/pull/177. + return ( + run.treeHash !== this.context.currentRun.treeHash && + run.branch === this.context.currentRun.branch && + run.repo === this.context.currentRun.repo + ) + }) + if (!cancelVictims.length) { + return core.info('Did not find other workflow runs to be cancelled') + } + for (const victim of cancelVictims) { + try { + const res = await this.context.octokit.rest.actions.cancelWorkflowRun({ + ...this.context.repo, + run_id: victim.id + }) + core.info( + `Cancelled run ${victim.htmlUrl} with response code ${res.status}` + ) + } catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.warning(error) + } + core.warning(`Failed to cancel ${victim.htmlUrl}`) + } } - return ( - run.treeHash !== currentRun.treeHash && - run.branch === currentRun.branch && - run.repo === currentRun.repo - ) - }) - if (!cancelVictims.length) { - return core.info('Did not find other workflow runs to be cancelled') - } - for (const victim of cancelVictims) { - await cancelWorkflowRun(victim, context) } -} -async function cancelWorkflowRun( - run: WorkflowRun, - context: WRunContext -): Promise { - try { - const res = await context.octokit.rest.actions.cancelWorkflowRun({ - owner: context.repoOwner, - repo: context.repoName, - run_id: run.runId - }) - core.info(`Cancelled run ${run.html_url} with response code ${res.status}`) - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.warning(e) - } - core.warning(`Failed to cancel ${run.html_url}`) + findSuccessfulDuplicateRun(treeHash: string): WorkflowRun | undefined { + return this.context.olderRuns.find( + run => + run.treeHash === treeHash && + run.status === 'completed' && + run.conclusion === 'success' + ) } -} -function detectSuccessfulDuplicateRuns( - context: WRunContext -): WorkflowRun | undefined { - const duplicateRuns = context.olderRuns.filter( - run => run.treeHash === context.currentRun.treeHash - ) - const successfulDuplicate = duplicateRuns.find(run => { - return run.status === 'completed' && run.conclusion === 'success' - }) - return successfulDuplicate -} + detectConcurrentRuns(): WorkflowRun | undefined { + const concurrentRuns = this.context.allRuns.filter( + run => run.status !== 'completed' + ) -function detectConcurrentRuns(context: WRunContext): WorkflowRun | undefined { - const concurrentRuns: WorkflowRun[] = context.allRuns.filter(run => { - if (run.status === 'completed') { - return false - } - if (run.runId === context.currentRun.runId) { - return false + if (!concurrentRuns.length) { + core.info('Did not find any concurrent workflow runs') + return } - return true - }) - - if (!concurrentRuns.length) { - core.info(`Did not find any concurrent workflow runs`) - return - } - if (context.concurrentSkipping === 'always') { - core.info( - `Skip execution because another instance of the same workflow is already running in ${concurrentRuns[0].html_url}` - ) - return concurrentRuns[0] - } else if (context.concurrentSkipping === 'outdated_runs') { - const newerRun = concurrentRuns.find( - run => - new Date(run.createdAt).getTime() > - new Date(context.currentRun.createdAt).getTime() - ) - if (newerRun) { + if (this.inputs.concurrentSkipping === 'always') { core.info( - `Skip execution because a newer instance of the same workflow is running in ${newerRun.html_url}` + `Skip execution because another instance of the same workflow is already running in ${concurrentRuns[0].htmlUrl}` ) - return newerRun - } - } else if (context.concurrentSkipping === 'same_content') { - const concurrentDuplicate = concurrentRuns.find( - run => run.treeHash === context.currentRun.treeHash - ) - if (concurrentDuplicate) { - core.info( - `Skip execution because the exact same files are concurrently checked in run ${concurrentDuplicate.html_url}` + return concurrentRuns[0] + } else if (this.inputs.concurrentSkipping === 'outdated_runs') { + const newerRun = concurrentRuns.find( + run => + new Date(run.createdAt).getTime() > + new Date(this.context.currentRun.createdAt).getTime() ) - return concurrentDuplicate - } - } else if (context.concurrentSkipping === 'same_content_newer') { - const concurrentIsOlder = concurrentRuns.find( - run => - run.treeHash === context.currentRun.treeHash && - run.runNumber < context.currentRun.runNumber - ) - if (concurrentIsOlder) { - core.info( - `Skip execution because the exact same files are concurrently checked in older run ${concurrentIsOlder.html_url}` + if (newerRun) { + core.info( + `Skip execution because a newer instance of the same workflow is running in ${newerRun.htmlUrl}` + ) + return newerRun + } + } else if (this.inputs.concurrentSkipping === 'same_content') { + const concurrentDuplicate = concurrentRuns.find( + run => run.treeHash === this.context.currentRun.treeHash ) - return concurrentIsOlder + if (concurrentDuplicate) { + core.info( + `Skip execution because the exact same files are concurrently checked in run ${concurrentDuplicate.htmlUrl}` + ) + return concurrentDuplicate + } + } else if (this.inputs.concurrentSkipping === 'same_content_newer') { + const concurrentIsOlder = concurrentRuns.find( + run => + run.treeHash === this.context.currentRun.treeHash && + run.runNumber < this.context.currentRun.runNumber + ) + if (concurrentIsOlder) { + core.info( + `Skip execution because the exact same files are concurrently checked in older run ${concurrentIsOlder.htmlUrl}` + ) + return concurrentIsOlder + } } + core.info(`Did not find any concurrent workflow runs that justify skipping`) } - core.info(`Did not find any concurrent workflow runs that justify skipping`) -} -async function backtracePathSkipping( - context: WRunContext -): Promise<{changedFiles: string[][]; pathsResult: PathsResult}> { - let commit: ReposGetCommitResponseData | null - let iterSha: string | null = context.currentRun.commitHash - let distanceToHEAD = 0 - const allChangedFiles: string[][] = [] - - const pathsFilter: PathsFilter = { - ...context.pathsFilter, - global: { - paths: context.paths, - paths_ignore: context.pathsIgnore, - backtracking: true + async backtracePathSkipping(): Promise<{ + changedFiles: string[][] + pathsResult: PathsResult + }> { + let commit: ApiCommit | null + let iterSha: string | null = this.context.currentRun.commitHash + let distanceToHEAD = 0 + const allChangedFiles: string[][] = [] + + const pathsFilter: PathsFilter = { + ...this.inputs.pathsFilter, + global: { + paths: this.inputs.paths, + paths_ignore: this.inputs.pathsIgnore, + backtracking: true + } } - } - - const pathsResult: PathsResult = {} - for (const name of Object.keys(pathsFilter)) { - pathsResult[name] = {should_skip: 'unknown', backtrack_count: 0} - } - do { - commit = await fetchCommitDetails(iterSha, context) - if (!commit) { - break + const pathsResult: PathsResult = {} + for (const name of Object.keys(pathsFilter)) { + pathsResult[name] = {should_skip: 'unknown', backtrack_count: 0} } - iterSha = commit.parents?.length ? commit.parents[0]?.sha : null - const changedFiles = commit.files - ? commit.files.map(f => f.filename).filter(f => typeof f === 'string') - : [] - allChangedFiles.push(changedFiles) - - const successfulRun = - (distanceToHEAD >= 1 && - findSuccessfulRun(commit.commit.tree.sha, context.olderRuns)) || - undefined - - for (const [name, values] of Object.entries(pathsResult)) { - // Only process paths where status is not determined yet. - if (values.should_skip !== 'unknown') continue - - // Skip if paths were ignorable or skippable until now and there is a successful run on the current commit. - if (successfulRun) { - pathsResult[name].should_skip = true - pathsResult[name].skipped_by = successfulRun - pathsResult[name].backtrack_count = distanceToHEAD - core.info( - `Skip '${name}' because all changes since ${successfulRun.html_url} are in ignored or skipped paths` - ) - continue - } - // Check if backtracking limit has been reached. - if ( - (pathsFilter[name].backtracking === false && distanceToHEAD === 1) || - pathsFilter[name].backtracking === distanceToHEAD - ) { - pathsResult[name].should_skip = false - pathsResult[name].backtrack_count = distanceToHEAD - core.info( - `Stop backtracking for '${name}' because the defined limit has been reached` - ) - continue + do { + commit = await this.fetchCommitDetails(iterSha) + if (!commit) { + break } + iterSha = commit.parents?.length ? commit.parents[0]?.sha : null + const changedFiles = commit.files + ? commit.files.map(f => f.filename).filter(f => typeof f === 'string') + : [] + allChangedFiles.push(changedFiles) + + const successfulRun = + (distanceToHEAD >= 1 && + this.findSuccessfulDuplicateRun(commit.commit.tree.sha)) || + undefined + + for (const [name, values] of Object.entries(pathsResult)) { + // Only process paths where status has not yet been determined. + if (values.should_skip !== 'unknown') continue + + // Skip if paths were ignorable or skippable until now and there is a successful run for the current commit. + if (successfulRun) { + pathsResult[name].should_skip = true + pathsResult[name].skipped_by = successfulRun + pathsResult[name].backtrack_count = distanceToHEAD + core.info( + `Skip '${name}' because all changes since run ${successfulRun.htmlUrl} are in ignored or skipped paths` + ) + continue + } - // Ignorable if all changed files match against ignored paths. - if (isCommitPathsIgnored(changedFiles, pathsFilter[name].paths_ignore)) { - core.info( - `Commit ${commit.html_url} is path-ignored for '${name}': All of '${changedFiles}' match against patterns '${pathsFilter[name].paths_ignore}'` - ) - continue - } + // Check if backtracking limit has been reached. + if ( + (pathsFilter[name].backtracking === false && distanceToHEAD === 1) || + pathsFilter[name].backtracking === distanceToHEAD + ) { + pathsResult[name].should_skip = false + pathsResult[name].backtrack_count = distanceToHEAD + core.info( + `Stop backtracking for '${name}' because the defined limit has been reached` + ) + continue + } - // Skippable if none of the changed files matches against paths. - if (pathsFilter[name].paths.length >= 1) { - const matches = getCommitPathsMatches( - changedFiles, - pathsFilter[name].paths - ) - if (matches.length === 0) { + // Ignorable if all changed files match against ignored paths. + if ( + this.isCommitPathsIgnored( + changedFiles, + pathsFilter[name].paths_ignore + ) + ) { core.info( - `Commit ${commit.html_url} is path-skipped for '${name}': None of '${changedFiles}' matches against patterns '${pathsFilter[name].paths}'` + `Commit ${commit.html_url} is path-ignored for '${name}': All of '${changedFiles}' match against patterns '${pathsFilter[name].paths_ignore}'` ) continue - } else { - pathsResult[name].matched_files = matches } + + // Skippable if none of the changed files matches against paths. + if (pathsFilter[name].paths.length >= 1) { + const matches = this.getCommitPathsMatches( + changedFiles, + pathsFilter[name].paths + ) + if (matches.length === 0) { + core.info( + `Commit ${commit.html_url} is path-skipped for '${name}': None of '${changedFiles}' matches against patterns '${pathsFilter[name].paths}'` + ) + continue + } else { + pathsResult[name].matched_files = matches + } + } + + // Not ignorable or skippable. + pathsResult[name].should_skip = false + pathsResult[name].backtrack_count = distanceToHEAD + core.info( + `Stop backtracking for '${name}' at commit ${commit.html_url} because '${changedFiles}' are not skippable against paths '${pathsFilter[name].paths}' or paths_ignore '${pathsFilter[name].paths_ignore}'` + ) } - // Not ignorable or skippable. - pathsResult[name].should_skip = false - pathsResult[name].backtrack_count = distanceToHEAD - core.info( - `Stop backtracking for '${name}' at commit ${commit.html_url} because '${changedFiles}' are not skippable against paths '${pathsFilter[name].paths}' or paths_ignore '${pathsFilter[name].paths_ignore}'` + // Should be never reached in practice; we expect that this loop aborts after 1-3 iterations. + if (distanceToHEAD++ >= 50) { + core.warning( + 'Aborted commit-backtracing due to bad performance - Did you push an excessive number of ignored-path commits?' + ) + break + } + } while ( + Object.keys(pathsResult).some( + path => pathsResult[path].should_skip === 'unknown' ) - } + ) - // Should be never reached in practice; we expect that this loop aborts after 1-3 iterations. - if (distanceToHEAD++ >= 50) { - core.warning( - 'Aborted commit-backtracing due to bad performance - Did you push an excessive number of ignored-path commits?' - ) - break + return {changedFiles: allChangedFiles, pathsResult} + } + + isCommitPathsIgnored(changedFiles: string[], pathsIgnore: string[]): boolean { + if (pathsIgnore.length === 0) { + return false } - } while ( - Object.keys(pathsResult).some( - path => pathsResult[path].should_skip === 'unknown' + const notIgnoredPaths = micromatch.not( + changedFiles, + pathsIgnore, + this.globOptions ) - ) - - return {changedFiles: allChangedFiles, pathsResult} -} + return notIgnoredPaths.length === 0 + } -function findSuccessfulRun( - treeHash: string, - olderRuns: WorkflowRun[] -): WorkflowRun | undefined { - const matchingRuns = olderRuns.filter(run => run.treeHash === treeHash) - const successfulRun = matchingRuns.find(run => { - return run.status === 'completed' && run.conclusion === 'success' - }) - return successfulRun -} + getCommitPathsMatches(changedFiles: string[], paths: string[]): string[] { + const matches = micromatch(changedFiles, paths, this.globOptions) + return matches + } -const globOptions = { - dot: true // Match dotfiles. Otherwise dotfiles are ignored unless a "." is explicitly defined in the pattern. + async fetchCommitDetails(sha: string | null): Promise { + if (!sha) { + return null + } + try { + const res = await this.context.octokit.rest.repos.getCommit({ + ...this.context.repo, + ref: sha + }) + return res.data + } catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.warning(error) + } + core.warning(`Failed to retrieve commit ${sha}`) + return null + } + } } -function isCommitPathsIgnored( - changedFiles: string[], - pathsIgnore: string[] -): boolean { - if (pathsIgnore.length === 0) { - return false +async function main(): Promise { + // Get and validate inputs. + const token = core.getInput('github_token', {required: true}) + const inputs = { + paths: getStringArrayInput('paths'), + pathsIgnore: getStringArrayInput('paths_ignore'), + pathsFilter: getPathsFilterInput('paths_filter'), + doNotSkip: getDoNotSkipInput('do_not_skip'), + concurrentSkipping: getConcurrentSkippingInput('concurrent_skipping'), + cancelOthers: core.getBooleanInput('cancel_others') ?? false, + skipAfterSuccessfulDuplicates: + core.getBooleanInput('skip_after_successful_duplicate') ?? true } - const notIgnoredPaths = micromatch.not(changedFiles, pathsIgnore, globOptions) - return notIgnoredPaths.length === 0 -} -function getCommitPathsMatches( - changedFiles: string[], - paths: string[] -): string[] { - const matches = micromatch(changedFiles, paths, globOptions) - return matches -} + const repo = github.context.repo + const octokit = github.getOctokit(token) -async function fetchCommitDetails( - sha: string | null, - context: WRunContext -): Promise { - if (!sha) { - return null + // Get and parse the current workflow run. + const {data: workflowRun} = await octokit.rest.actions.getWorkflowRun({ + ...repo, + run_id: github.context.runId + }) + const treeHash = workflowRun.head_commit?.tree_id + if (!treeHash) { + exitFail(` + Could not find the tree hash of run ${workflowRun.id} (Workflow ID: ${workflowRun.workflow_id}, + Name: ${workflowRun.name}, Head Branch: ${workflowRun.head_branch}, Head SHA: ${workflowRun.head_sha}). + This might be a run associated with a headless or removed commit. + `) } - try { - const res = await context.octokit.rest.repos.getCommit({ - owner: context.repoOwner, - repo: context.repoName, - ref: sha - }) - return res.data - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.warning(e) + const currentRun = mapWorkflowRun(workflowRun, treeHash) + + // Fetch list of runs for current workflow. + const { + data: {workflow_runs: workflowRuns} + } = await octokit.rest.actions.listWorkflowRuns({ + ...repo, + workflow_id: currentRun.workflowId, + per_page: 100 + }) + + // Check and map all runs. + const allRuns = workflowRuns.reduce((result: WorkflowRun[], run) => { + // Filter out current run and runs that lack 'head_commit' (most likely runs associated with a headless or removed commit). + // See https://github.com/fkirc/skip-duplicate-actions/pull/178. + if (run.id !== currentRun.id && run.head_commit) { + result.push(mapWorkflowRun(run, run.head_commit.tree_id)) } - core.warning(`Failed to retrieve commit ${sha}`) - return null + return result + }, []) + + // List with older workflow runs only (used to prevent some nasty race conditions and edge cases). + const olderRuns = allRuns.filter(run => { + return ( + new Date(run.createdAt).getTime() < + new Date(currentRun.createdAt).getTime() + ) + }) + + const skipDuplicateActions = new SkipDuplicateActions(inputs, { + repo, + octokit, + currentRun, + allRuns, + olderRuns + }) + await skipDuplicateActions.run() +} + +function mapWorkflowRun( + run: ApiWorkflowRun | ApiWorkflowRuns, + treeHash: string +): WorkflowRun { + return { + id: run.id, + runNumber: run.run_number, + event: run.event as WorkflowRunTrigger, + treeHash, + commitHash: run.head_sha, + status: run.status as WorkflowRunStatus, + conclusion: run.conclusion as WorkflowRunConclusion, + htmlUrl: run.html_url, + branch: run.head_branch, + // Wrong type: 'head_repository' can be null (probably when repo has been removed) + repo: run.head_repository?.full_name ?? null, + workflowId: run.workflow_id, + createdAt: run.created_at } } +/** Set all outputs and exit the action. */ function exitSuccess(args: { shouldSkip: boolean reason: string @@ -557,74 +535,98 @@ function exitSuccess(args: { core.setOutput('skipped_by', args.skippedBy || {}) core.setOutput('changed_files', args.changedFiles || []) core.setOutput('paths_result', args.pathsResult || {}) - return process.exit(0) + process.exit(0) } -function formatCliOptions(options: string[]): string { - return `${options.map(o => `"${o}"`).join(', ')}` -} -function getConcurrentSkippingInput(name: string): ConcurrentSkippingOption { - const rawInput = core.getInput(name, {required: true}) - if (rawInput.toLowerCase() === 'false') { - return 'never' // Backwards-compat - } else if (rawInput.toLowerCase() === 'true') { - return 'same_content' // Backwards-compat - } - const options = getConcurrentSkippingOptions() - if (options.includes(rawInput)) { - return rawInput as ConcurrentSkippingOption - } else { - logFatal(`'${name}' must be one of ${formatCliOptions(options)}`) +/** Immediately terminate the action with failing exit code. */ +function exitFail(error: unknown): never { + if (error instanceof Error || typeof error == 'string') { + core.error(error) } + process.exit(1) } -function getBooleanInput(name: string, defaultValue: boolean): boolean { - const rawInput = core.getInput(name, {required: false}) +function getStringArrayInput(name: string): string[] { + const rawInput = core.getInput(name) if (!rawInput) { - return defaultValue + return [] } - if (defaultValue) { - return rawInput.toLowerCase() !== 'false' - } else { - return rawInput.toLowerCase() === 'true' + try { + const array = JSON.parse(rawInput) + if (!Array.isArray(array)) { + exitFail(`Input '${rawInput}' is not a JSON-array`) + } + for (const element of array) { + if (typeof element !== 'string') { + exitFail(`Element '${element}' of input '${rawInput}' is not a string`) + } + } + return array + } catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.error(error) + } + exitFail(`Input '${rawInput}' is not a valid JSON`) } } -function getStringArrayInput(name: string): string[] { - const rawInput = core.getInput(name, {required: false}) +function getDoNotSkipInput(name: string): WorkflowRunTrigger[] { + const rawInput = core.getInput(name) if (!rawInput) { return [] } try { const array = JSON.parse(rawInput) if (!Array.isArray(array)) { - logFatal(`Input '${rawInput}' is not a JSON-array`) + exitFail(`Input '${rawInput}' is not a JSON-array`) } - for (const e of array) { - if (typeof e !== 'string') { - logFatal(`Element '${e}' of input '${rawInput}' is not a string`) + for (const element of array) { + if (!workflowRunTriggerOptions.includes(element as WorkflowRunTrigger)) { + exitFail( + `Elements in '${name}' must be one of ${workflowRunTriggerOptions + .map(option => `"${option}"`) + .join(', ')}` + ) } } return array - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.error(e) + } catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.error(error) } - logFatal(`Input '${rawInput}' is not a valid JSON`) + exitFail(`Input '${rawInput}' is not a valid JSON`) + } +} + +function getConcurrentSkippingInput(name: string): ConcurrentSkipping { + const rawInput = core.getInput(name, {required: true}) + if (rawInput.toLowerCase() === 'false') { + return 'never' // Backwards-compat + } else if (rawInput.toLowerCase() === 'true') { + return 'same_content' // Backwards-compat + } + if (concurrentSkippingOptions.includes(rawInput as ConcurrentSkipping)) { + return rawInput as ConcurrentSkipping + } else { + exitFail( + `'${name}' must be one of ${concurrentSkippingOptions + .map(option => `"${option}"`) + .join(', ')}` + ) } } function getPathsFilterInput(name: string): PathsFilter { - const rawInput = core.getInput(name, {required: false}) + const rawInput = core.getInput(name) if (!rawInput) { return {} } try { const input = yaml.load(rawInput) - // Assign default values + // Assign default values to each entry const pathsFilter: PathsFilter = {} for (const [key, value] of Object.entries( - input as Record> + input as Record> )) { pathsFilter[key] = { paths: value.paths || [], @@ -633,17 +635,12 @@ function getPathsFilterInput(name: string): PathsFilter { } } return pathsFilter - } catch (e) { - if (e instanceof Error || typeof e === 'string') { - core.error(e) + } catch (error) { + if (error instanceof Error || typeof error === 'string') { + core.error(error) } - logFatal(`Input '${rawInput}' is invalid`) + exitFail(`Input '${rawInput}' is invalid`) } } -function logFatal(msg: string): never { - core.setFailed(msg) - return process.exit(1) -} - main() diff --git a/tsconfig.json b/tsconfig.json index aaf5824..31ce49b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "compilerOptions": { - "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "outDir": "./lib" /* Redirect output structure to the directory. */, + "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ }, "exclude": ["node_modules"] }