You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2020/12/27 13:52:55 UTC
[airflow-cancel-workflow-runs] 39/44: Adds 'allDuplicates' mode of
cancelling (#10)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow-cancel-workflow-runs.git
commit 99869d37d982384d18c79539b67df94f17557cbe
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sat Oct 31 18:18:25 2020 +0100
Adds 'allDuplicates' mode of cancelling (#10)
---
README.md | 81 +-
action.yml | 17 +-
dist/index.js | 4723 ++++++++-------------------------------------------------
src/main.ts | 1325 +++++++++++-----
4 files changed, 1673 insertions(+), 4473 deletions(-)
diff --git a/README.md b/README.md
index 0a13160..e81c66b 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,8 @@
- [Inputs and outputs](#inputs-and-outputs)
- [Inputs](#inputs)
- [Outputs](#outputs)
-- [Examples](#examples)
+ - [Most often used canceling example](#most-often-used-canceling-example)
+- [More Examples](#more-examples)
- [Repositories that use Pull Requests from forks](#repositories-that-use-pull-requests-from-forks)
- [Cancel duplicate runs for the source workflow](#cancel-duplicate-runs-for-the-source-workflow)
- [Cancel duplicate jobs for triggered workflow](#cancel-duplicate-jobs-for-triggered-workflow)
@@ -98,10 +99,6 @@ If you want a comprehensive solution, you should use the action as follows:
The `workflow_run` should be responsible for all canceling actions. The examples below show
the possible ways the action can be utilized.
-The previous version of this action (v1) used `schedule` events to cancel previous runs. With
-the announcement of pull_request_target by GitHub, the v2 version discourages using this pattern
-and `schedule` events are no longer needed.
-
# Inputs and outputs
## Inputs
@@ -122,12 +119,13 @@ and `schedule` events are no longer needed.
The job cancel modes work as follows:
-| Cancel Mode | No `sourceRunId` specified | The `sourceRunId` set to `${{ github.event.workflow_run.id }}` |
-|--------------|------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
-| `duplicates` | Cancels past, duplicate runs from the same repo/branch as current run. | Cancels past, duplicate runs for the same repo/branch as the source run. |
-| `self` | Cancels self run. | Cancel the `sourceRunId` run. |
-| `failedJobs` | Cancels all runs of own workflow that have matching jobs that failed. | Cancels all runs of the `sourceRunId` workflow that have matching jobs that failed. |
-| `namedJobs` | Cancels all runs of own workflow that have matching jobs. | Cancels all runs of the `sourceRunId` workflow that have matching jobs. |
+| Cancel Mode | No `sourceRunId` specified | The `sourceRunId` set to `${{ github.event.workflow_run.id }}` |
+|-----------------|------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
+| `duplicates` | Cancels duplicate runs from the same repo/branch as current run. | Cancels duplicate runs for the same repo/branch as the source run. |
+| `allDuplicates` | Cancels duplicate runs from all running workflows. | Cancels duplicate runs from all running workflows. |
+| `self` | Cancels self run. | Cancel the `sourceRunId` run. |
+| `failedJobs` | Cancels all runs of own workflow that have matching jobs that failed. | Cancels all runs of the `sourceRunId` workflow that have matching jobs that failed. |
+| `namedJobs` | Cancels all runs of own workflow that have matching jobs. | Cancels all runs of the `sourceRunId` workflow that have matching jobs. |
## Outputs
@@ -143,7 +141,38 @@ The job cancel modes work as follows:
| `sourceEvent` | Current event: ``${{ github.event }}`` | Event of the run that triggered this `workflow_run` |
| `cancelledRuns` | JSON-stringified array of cancelled run ids. | JSON-stringified array of cancelled run ids. |
-# Examples
+## Most often used canceling example
+
+The most common canceling example is that you want to cancel all duplicates appearing in your build queue.
+As of 4.1 version of the Action this can be realised by single workflow run that can cancel all duplicates
+for all running workflows. It is resistant to temporary queues - as it can cancel also the future, queued
+workflows that have duplicated, fresher (also queued workflows and this is recommended for everyone.
+
+The below example is a "workflow_run" type of event. The workflow_run event always has "write" access that allows
+it to cancel other workflows - even if they are coming from pull request.
+
+```yaml
+name: Cancelling Duplicates
+on:
+ workflow_run:
+ workflows: ['CI']
+ types: ['requested']
+
+jobs:
+ cancel-duplicate-workflow-runs:
+ name: "Cancel duplicate workflow runs"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: potiuk/cancel-workflow-runs@v4_1
+ name: "Cancel duplicate workflow runs"
+ with:
+ cancelMode: allDuplicates
+ token: ${{ secrets.GITHUB_TOKEN }}
+ sourceRunId: ${{ github.event.workflow_run.id }}
+```
+
+
+# More Examples
Note that you can combine the steps below in several steps of the same job. The examples here are showing
one step per case for clarity.
@@ -202,7 +231,7 @@ jobs:
name: "Cancel duplicate workflow runs"
runs-on: ubuntu-latest
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Cancel duplicate workflow runs"
with:
cancelMode: duplicates
@@ -263,7 +292,7 @@ jobs:
sourceHeadSha: ${{ steps.cancel.outputs.sourceHeadSha }}
sourceEvent: ${{ steps.cancel.outputs.sourceEvent }}
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
id: cancel
name: "Cancel duplicate CI runs"
with:
@@ -276,7 +305,7 @@ jobs:
that you will not see in the list of checks.
You can checks the status of those images in:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Cancel duplicate Cancelling runs"
with:
cancelMode: namedJobs
@@ -326,7 +355,7 @@ on:
runs-on: ubuntu-latest
steps:
- name: "Cancel the self CI workflow run"
- uses: potiuk/cancel-workflow-runs@v2
+ uses: potiuk/cancel-workflow-runs@v4_1
with:
cancelMode: self
notifyPRCancel: true
@@ -355,7 +384,7 @@ on:
runs-on: ubuntu-latest
steps:
- name: "Cancel the self Cancelling workflow run"
- uses: potiuk/cancel-workflow-runs@v2
+ uses: potiuk/cancel-workflow-runs@v4_1
with:
cancelMode: self
notifyPRCancel: true
@@ -388,7 +417,7 @@ jobs:
name: "Fail fast CI runs"
runs-on: ubuntu-latest
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Fail fast CI runs"
with:
cancelMode: failedJobs
@@ -429,7 +458,7 @@ jobs:
name: "Fail fast CI runs"
runs-on: ubuntu-latest
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Fail fast CI. Source run: ${{ github.event.workflow_run.id }}"
id: cancel-failed
with:
@@ -452,7 +481,7 @@ jobs:
echo "::set-output name=matching-regexp::${REGEXP}"
- name: "Cancel triggered 'Cancelling' runs for the cancelled failed runs"
if: steps.cancel-failed.outputs.cancelledRuns != '[]'
- uses: potiuk/cancel-workflow-runs@master
+ uses: potiuk/cancel-workflow-runs@v4_1
with:
cancelMode: namedJobs
token: ${{ secrets.GITHUB_TOKEN }}
@@ -487,7 +516,7 @@ jobs:
name: "Fail fast Canceling runs"
runs-on: ubuntu-latest
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Fail fast Canceling runs"
with:
cancelMode: failedJobs
@@ -512,7 +541,7 @@ on:
runs-on: ubuntu-latest
steps:
- name: "Cancel the self CI workflow run"
- uses: potiuk/cancel-workflow-runs@v2
+ uses: potiuk/cancel-workflow-runs@v4_1
with:
cancelMode: duplicates
cancelFutureDuplicates: true
@@ -553,7 +582,7 @@ jobs:
name: "Cancel duplicate workflow runs"
runs-on: ubuntu-latest
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Cancel duplicate workflow runs"
with:
cancelMode: duplicates
@@ -577,7 +606,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Cancel the self workflow run"
- uses: potiuk/cancel-workflow-runs@v2
+ uses: potiuk/cancel-workflow-runs@v4_1
with:
cancelMode: self
token: ${{ secrets.GITHUB_TOKEN }}
@@ -603,7 +632,7 @@ jobs:
name: "Cancel failed runs"
runs-on: ubuntu-latest
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Cancel failed runs"
with:
cancelMode: failedJobs
@@ -635,7 +664,7 @@ jobs:
name: "Cancel the self workflow run"
runs-on: ubuntu-latest
steps:
- - uses: potiuk/cancel-workflow-runs@v2
+ - uses: potiuk/cancel-workflow-runs@v4_1
name: "Cancel past CI runs"
with:
cancelMode: namedJobs
diff --git a/action.yml b/action.yml
index aaec312..541094a 100644
--- a/action.yml
+++ b/action.yml
@@ -29,12 +29,17 @@ inputs:
cancelMode:
description: |
The mode of cancel. One of:
- * `duplicates` - cancels past, duplicate runs from the same repo/branch as local run or
- sourceId workflow. This is the default mode when cancelMode is not specified.
- * `self` - cancels self run - either own run if sourceRunId is not set, or
- the source run that triggered the `workflow_run'
- * `failedJobs` - cancels all runs that failed in jobs matching one of the regexps
- * `namedJobs` - cancels runs where names of some jobs match some of regexps
+ * `duplicates` - cancels duplicate runs from the same repo/branch as local run or
+ sourceId workflow. This is the default mode when cancelMode is not specified.
+ * `allDuplicates` - cancels duplicate runs from all workflows. It is more aggressive version of
+ duplicate canceling - as it cancels all duplicates. It is helpful in case
+ of long queues of builds - as it is enough that one of the workflows that
+ cancel duplicates is executed, it can effectively clean-up the queue in this
+ case for all the future, queued runs.
+ * `self` - cancels self run - either own run if sourceRunId is not set, or
+ the source run that triggered the `workflow_run'
+ * `failedJobs` - cancels all runs that failed in jobs matching one of the regexps
+ * `namedJobs` - cancels runs where names of some jobs match some of regexps
required: false
cancelFutureDuplicates:
description: |
diff --git a/dist/index.js b/dist/index.js
index 69bd164..6f047aa 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1465,76 +1465,138 @@ var __importStar = (this && this.__importStar) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
const github = __importStar(__webpack_require__(469));
const core = __importStar(__webpack_require__(393));
-const treemap = __importStar(__webpack_require__(706));
-const CANCELLABLE_RUNS = [
+/**
+ Those are the cancellable event types tht we know about
+ */
+const CANCELLABLE_EVENT_TYPES = [
'push',
'pull_request',
'workflow_run',
'schedule',
'workflow_dispatch'
];
+/**
+ * Those are different modes for cancelling
+ */
var CancelMode;
(function (CancelMode) {
CancelMode["DUPLICATES"] = "duplicates";
+ CancelMode["ALL_DUPLICATES"] = "allDuplicates";
CancelMode["SELF"] = "self";
CancelMode["FAILED_JOBS"] = "failedJobs";
CancelMode["NAMED_JOBS"] = "namedJobs";
})(CancelMode || (CancelMode = {}));
-function createListRunsQueryOtherRuns(octokit, owner, repo, status, workflowId, headBranch, eventName) {
+/**
+ * Converts the source of a run object into a string that can be used as map key in maps where we keep
+ * arrays of runs per source group
+ * @param sourceGroup the object identifying the source of the run (Pull Request, Master Push)
+ * @returns the unique string id for the source group
+ */
+function getSourceGroupId(sourceGroup) {
+ return `:${sourceGroup.workflowId}:${sourceGroup.headRepo}:${sourceGroup.headBranch}:${sourceGroup.eventName}`;
+}
+/**
+ * Creates query parameters selecting all runs that share the same source group as we have. This can
+ * be used to select duplicates of my own run.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param mySourceGroup - source group of the originating run
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQueryRunsSameSource(repositoryInfo, status, mySourceGroup) {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
- workflow_id: workflowId,
+ workflow_id: mySourceGroup.workflowId,
status,
- branch: headBranch,
- event: eventName
+ branch: mySourceGroup.headBranch,
+ event: mySourceGroup.eventName
};
- return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+ return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request);
}
-function createListRunsQueryMyOwnRun(octokit, owner, repo, status, workflowId, runId) {
+/**
+ * Creates query parameters selecting only specific run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @param runId - run Id to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQuerySpecificRunId(repositoryInfo, status, workflowId, runId) {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
workflow_id: workflowId,
status,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId.toString()
};
- return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+ return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request);
}
-function createListRunsQueryAllRuns(octokit, owner, repo, status, workflowId) {
+/**
+ * Creates query parameters selecting all run Ids for specified workflow Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQueryAllRuns(repositoryInfo, status, workflowId) {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
workflow_id: workflowId,
status
};
- return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+ return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request);
}
-function createJobsForWorkflowRunQuery(octokit, owner, repo, runId) {
+/**
+ * Creates query parameters selecting all jobs for specified run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return query parameters merged with the listJobsForWorkflowRun criteria
+ */
+function createJobsForWorkflowRunQuery(repositoryInfo, runId) {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
};
- return octokit.actions.listJobsForWorkflowRun.endpoint.merge(request);
+ return repositoryInfo.octokit.actions.listJobsForWorkflowRun.endpoint.merge(request);
}
-function matchInArray(s, regexps) {
+/**
+ * Returns true if the string matches any of the regexps in array of regexps
+ * @param stringToMatch string to match
+ * @param regexps array of regexp to match the string against
+ * @return true if there is a match
+ */
+function matchInArray(stringToMatch, regexps) {
for (const regexp of regexps) {
- if (s.match(regexp)) {
+ if (stringToMatch.match(regexp)) {
return true;
}
}
return false;
}
-function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfFailed) {
+/**
+ * Returns true if the runId specified has jobs matching the regexp and optionally checks if those
+ * jobs are failed.
+ * * If checkIfFailed is False, it returns true if any of the job name for the run match any of the regexps
+ * * Id checkIfFailed is True, it returns true if any of the matching jobs have status 'failed'
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @param jobNameRegexps - array of job name regexps
+ * @param checkIfFailed - whether to check the 'failed' status of matched jobs
+ * @return true if there is a match
+ */
+function jobsMatchingNames(repositoryInfo, runId, jobNameRegexps, checkIfFailed) {
var e_1, _a;
return __awaiter(this, void 0, void 0, function* () {
- const listJobs = createJobsForWorkflowRunQuery(octokit, owner, repo, runId);
+ const listJobs = createJobsForWorkflowRunQuery(repositoryInfo, runId);
if (checkIfFailed) {
core.info(`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps} that failed\n`);
}
@@ -1542,7 +1604,7 @@ function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfF
core.info(`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps}\n`);
}
try {
- for (var _b = __asyncValues(octokit.paginate.iterator(listJobs)), _c; _c = yield _b.next(), !_c.done;) {
+ for (var _b = __asyncValues(repositoryInfo.octokit.paginate.iterator(listJobs)), _c; _c = yield _b.next(), !_c.done;) {
const item = _c.value;
for (const job of item.data.jobs) {
core.info(` The job name: ${job.name}, Conclusion: ${job.conclusion}`);
@@ -1550,11 +1612,13 @@ function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfF
if (checkIfFailed) {
// Only fail the build if one of the matching jobs fail
if (job.conclusion === 'failure') {
- core.info(` The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed. Cancelling run.`);
+ core.info(` The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed.` +
+ ` Cancelling run.`);
return true;
}
else {
- core.info(` The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. So far, so good.`);
+ core.info(` The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. ` +
+ ` So far, so good.`);
}
}
else {
@@ -1576,30 +1640,52 @@ function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfF
return false;
});
}
-function getWorkflowId(octokit, runId, owner, repo) {
+/**
+ * Retrieves workflowId from the workflow URL.
+ * @param workflowUrl workflow URL to retrieve the ID from
+ * @return numerical workflow id
+ */
+function retrieveWorkflowIdFromUrl(workflowUrl) {
+ const workflowIdString = workflowUrl.split('/').pop() || '';
+ if (!(workflowIdString.length > 0)) {
+ throw new Error('Could not resolve workflow');
+ }
+ return parseInt(workflowIdString);
+}
+/**
+ * Returns workflowId of the runId specified
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return workflow ID for the run id
+ */
+function getWorkflowId(repositoryInfo, runId) {
return __awaiter(this, void 0, void 0, function* () {
- const reply = yield octokit.actions.getWorkflowRun({
- owner,
- repo,
+ const reply = yield repositoryInfo.octokit.actions.getWorkflowRun({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
});
core.info(`The source run ${runId} is in ${reply.data.workflow_url} workflow`);
- const workflowIdString = reply.data.workflow_url.split('/').pop() || '';
- if (!(workflowIdString.length > 0)) {
- throw new Error('Could not resolve workflow');
- }
- return parseInt(workflowIdString);
+ return retrieveWorkflowIdFromUrl(reply.data.workflow_url);
});
}
-function getWorkflowRuns(octokit, statusValues, cancelMode, createListRunQuery) {
+/**
+ * Returns workflow runs matching the callable adding query criteria
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - array of string status values for runs that we are interested at
+ * @param cancelMode - which cancel mode the query is about
+ * @param createListRunQuery - what is the callable criteria selection
+ * @return map of workflow run items indexed by the workflow run number
+ */
+function getWorkflowRuns(repositoryInfo, statusValues, cancelMode, createListRunQuery) {
var e_2, _a;
return __awaiter(this, void 0, void 0, function* () {
- const workflowRuns = new treemap.TreeMap();
+ const workflowRuns = new Map();
for (const status of statusValues) {
const listRuns = yield createListRunQuery(status);
try {
- for (var _b = __asyncValues(octokit.paginate.iterator(listRuns)), _c; _c = yield _b.next(), !_c.done;) {
+ for (var _b = __asyncValues(repositoryInfo.octokit.paginate.iterator(listRuns)), _c; _c = yield _b.next(), !_c.done;) {
const item = _c.value;
// There is some sort of bug where the pagination URLs point to a
// different endpoint URL which trips up the resulting representation
@@ -1622,14 +1708,114 @@ function getWorkflowRuns(octokit, statusValues, cancelMode, createListRunQuery)
return workflowRuns;
});
}
-function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNamesRegexps, skipEventTypes) {
+/**
+ * True if the request is candidate for cancelling in case of duplicate deletion
+ * @param runItem item to check
+ * @param headRepo head Repo that we are checking against
+ * @param cancelFutureDuplicates whether future duplicates are being cancelled
+ * @param sourceRunId the source Run Id that originates the request
+ * @return true if we determine that the run Id should be cancelled
+ */
+function isCandidateForCancellingDuplicate(runItem, headRepo, cancelFutureDuplicates, sourceRunId) {
+ const runHeadRepo = runItem.head_repository.full_name;
+ if (headRepo !== undefined && runHeadRepo !== headRepo) {
+ core.info(`\nThe run ${runItem.id} is from a different ` +
+ `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`);
+ return false;
+ }
+ if (cancelFutureDuplicates) {
+ core.info(`\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`);
+ return true;
+ }
+ else {
+ if (runItem.id === sourceRunId) {
+ core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`);
+ return false;
+ }
+ else if (runItem.id > sourceRunId) {
+ core.info(`\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`);
+ return false;
+ }
+ core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`);
+ return true;
+ }
+}
+/**
+ * Should the run is candidate for cancelling in SELF cancelling mode?
+ * @param runItem run item
+ * @param sourceRunId source run id
+ * @return true if the run item is self
+ */
+function isCandidateForCancellingSelf(runItem, sourceRunId) {
+ if (runItem.id === sourceRunId) {
+ core.info(`\nReturning the "source" run: ${runItem.id}.\n`);
+ return true;
+ }
+ else {
+ return false;
+ }
+}
+/**
+ * Should the run is candidate for cancelling in naming job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains jobs with names matching the pattern
+ */
+function isCandidateForCancellingNamedJobs(repositoryInfo, runItem, jobNamesRegexps) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Cancel all jobs that have failed jobs (no matter when started)
+ if (yield jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, false)) {
+ core.info(`\nSome jobs have matching names in ${runItem.id} . Returning it.\n`);
+ return true;
+ }
+ else {
+ core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`);
+ return false;
+ }
+ });
+}
+/**
+ * Should the run is candidate for cancelling in failed job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains failed jobs with names matching the pattern
+ */
+function isCandidateForCancellingFailedJobs(repositoryInfo, runItem, jobNamesRegexps) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Cancel all jobs that have failed jobs (no matter when started)
+ if (yield jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, true)) {
+ core.info(`\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`);
+ return true;
+ }
+ else {
+ core.info(`\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`);
+ return false;
+ }
+ });
+}
+/**
+ * Determines whether the run is candidate to be cancelled depending on the mode used
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run item
+ * @param headRepo - head repository
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - what is the source run id
+ * @param jobNamesRegexps - what are the regexps for job names
+ * @param skipEventTypes - which events should be skipped
+ * @return true if the run id is candidate for cancelling
+ */
+function isCandidateForCancelling(repositoryInfo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNamesRegexps, skipEventTypes) {
return __awaiter(this, void 0, void 0, function* () {
if ('completed' === runItem.status.toString()) {
core.info(`\nThe run ${runItem.id} is completed. Not cancelling it.\n`);
return false;
}
- if (!CANCELLABLE_RUNS.includes(runItem.event.toString())) {
- core.info(`\nThe run ${runItem.id} is (${runItem.event} event - not in ${CANCELLABLE_RUNS}). Not cancelling it.\n`);
+ if (!CANCELLABLE_EVENT_TYPES.includes(runItem.event.toString())) {
+ core.info(`\nThe run ${runItem.id} is (${runItem.event} event - not ` +
+ `in ${CANCELLABLE_EVENT_TYPES}). Not cancelling it.\n`);
return false;
}
if (skipEventTypes.includes(runItem.event.toString())) {
@@ -1638,72 +1824,38 @@ function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode,
return false;
}
if (cancelMode === CancelMode.FAILED_JOBS) {
- // Cancel all jobs that have failed jobs (no matter when started)
- if (yield jobsMatchingNames(octokit, owner, repo, runItem.id, jobNamesRegexps, true)) {
- core.info(`\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`);
- return true;
- }
- else {
- core.info(`\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`);
- return false;
- }
+ return yield isCandidateForCancellingFailedJobs(repositoryInfo, runItem, jobNamesRegexps);
}
else if (cancelMode === CancelMode.NAMED_JOBS) {
- // Cancel all jobs that have failed jobs (no matter when started)
- if (yield jobsMatchingNames(octokit, owner, repo, runItem.id, jobNamesRegexps, false)) {
- core.info(`\nSome jobs have matching names in ${runItem.id} . Returning it.\n`);
- return true;
- }
- else {
- core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`);
- return false;
- }
+ return yield isCandidateForCancellingNamedJobs(repositoryInfo, runItem, jobNamesRegexps);
}
else if (cancelMode === CancelMode.SELF) {
- if (runItem.id === sourceRunId) {
- core.info(`\nReturning the "source" run: ${runItem.id}.\n`);
- return true;
- }
- else {
- return false;
- }
+ return isCandidateForCancellingSelf(runItem, sourceRunId);
}
else if (cancelMode === CancelMode.DUPLICATES) {
- const runHeadRepo = runItem.head_repository.full_name;
- if (headRepo !== undefined && runHeadRepo !== headRepo) {
- core.info(`\nThe run ${runItem.id} is from a different ` +
- `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`);
- return false;
- }
- if (cancelFutureDuplicates) {
- core.info(`\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`);
- return true;
- }
- else {
- if (runItem.id === sourceRunId) {
- core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`);
- return false;
- }
- else if (runItem.id > sourceRunId) {
- core.info(`\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`);
- return false;
- }
- core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`);
- return true;
- }
+ return isCandidateForCancellingDuplicate(runItem, headRepo, cancelFutureDuplicates, sourceRunId);
+ }
+ else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+ core.info(`Returning candidate ${runItem.id} for "all_duplicates" cancelling.`);
+ return true;
}
else {
throw Error(`\nWrong cancel mode ${cancelMode}! This should never happen.\n`);
}
});
}
-function cancelRun(octokit, owner, repo, runId) {
+/**
+ * Cancels the specified workflow run.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run Id to cancel
+ */
+function cancelRun(repositoryInfo, runId) {
return __awaiter(this, void 0, void 0, function* () {
let reply;
try {
- reply = yield octokit.actions.cancelWorkflowRun({
- owner,
- repo,
+ reply = yield repositoryInfo.octokit.actions.cancelWorkflowRun({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
});
@@ -1714,122 +1866,288 @@ function cancelRun(octokit, owner, repo, runId) {
}
});
}
-function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
+/**
+ * Returns map of workflow run items matching the criteria specified group by workflow run id
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - status values we want to check
+ * @param cancelMode - cancel mode to use
+ * @param sourceWorkflowId - source workflow id
+ * @param sourceRunId - source run id
+ * @param sourceEventName - name of the source event
+ * @param mySourceGroup - source of the run that originated it
+ * @return map of the run items matching grouped by workflow run id
+ */
+function getWorkflowRunsMatchingCriteria(repositoryInfo, statusValues, cancelMode, sourceWorkflowId, sourceRunId, sourceEventName, mySourceGroup) {
return __awaiter(this, void 0, void 0, function* () {
- const statusValues = ['queued', 'in_progress'];
- const workflowRuns = yield getWorkflowRuns(octokit, statusValues, cancelMode, function (status) {
+ return yield getWorkflowRuns(repositoryInfo, statusValues, cancelMode, function (status) {
if (cancelMode === CancelMode.SELF) {
- core.info(`\nFinding runs for my own run: Owner: ${owner}, Repo: ${repo}, ` +
+ core.info(`\nFinding runs for my own run: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
`Workflow ID:${sourceWorkflowId}, Source Run id: ${sourceRunId}\n`);
- return createListRunsQueryMyOwnRun(octokit, owner, repo, status, sourceWorkflowId, sourceRunId);
+ return createListRunsQuerySpecificRunId(repositoryInfo, status, sourceWorkflowId, sourceRunId);
}
else if (cancelMode === CancelMode.FAILED_JOBS ||
- cancelMode === CancelMode.NAMED_JOBS) {
- core.info(`\nFinding runs for all runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
- `Workflow ID:${sourceWorkflowId}\n`);
- return createListRunsQueryAllRuns(octokit, owner, repo, status, sourceWorkflowId);
+ cancelMode === CancelMode.NAMED_JOBS ||
+ cancelMode === CancelMode.ALL_DUPLICATES) {
+ core.info(`\nFinding runs for all runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+ `Status: ${status} Workflow ID:${sourceWorkflowId}\n`);
+ return createListRunsQueryAllRuns(repositoryInfo, status, sourceWorkflowId);
}
else if (cancelMode === CancelMode.DUPLICATES) {
- core.info(`\nFinding duplicate runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
- `Workflow ID:${sourceWorkflowId}, Head Branch: ${headBranch},` +
+ core.info(`\nFinding duplicate runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+ `Status: ${status} Workflow ID:${sourceWorkflowId}, Head Branch: ${mySourceGroup.headBranch},` +
`Event name: ${sourceEventName}\n`);
- return createListRunsQueryOtherRuns(octokit, owner, repo, status, sourceWorkflowId, headBranch, sourceEventName);
+ return createListRunsQueryRunsSameSource(repositoryInfo, status, mySourceGroup);
}
else {
throw Error(`\nWrong cancel mode ${cancelMode}! This should never happen.\n`);
}
});
- const workflowsToCancel = [];
- const pullRequestToNotify = [];
+ });
+}
+/**
+ * Finds pull request matching its headRepo, headBranch and headSha
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository from which Pull Request comes
+ * @param headBranch - head branch from which Pull Request comes
+ * @param headSha - sha for the head of the incoming Pull request
+ */
+function findPullRequest(repositoryInfo, headRepo, headBranch, headSha) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Finds Pull request for this workflow run
+ core.info(`\nFinding PR request id for: owner: ${repositoryInfo.owner}, Repo:${repositoryInfo.repo},` +
+ ` Head:${headRepo}:${headBranch}.\n`);
+ const pullRequests = yield repositoryInfo.octokit.pulls.list({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
+ head: `${headRepo}:${headBranch}`
+ });
+ for (const pullRequest of pullRequests.data) {
+ core.info(`\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`);
+ if (pullRequest.head.sha === headSha) {
+ core.info(`\nFound PR: ${pullRequest.number}\n`);
+ return pullRequest;
+ }
+ }
+ core.info(`\nCould not find the PR for this build :(\n`);
+ return null;
+ });
+}
+/**
+ * Finds pull request id for the run item.
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run Item that the pull request should be found for
+ * @return pull request number to notify (or undefined if not found)
+ */
+function findPullRequestForRunItem(repositoryInfo, runItem) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const pullRequest = yield findPullRequest(repositoryInfo, runItem.head_repository.owner.login, runItem.head_branch, runItem.head_sha);
+ if (pullRequest) {
+ return pullRequest.number;
+ }
+ return undefined;
+ });
+}
+/**
+ * Maps found workflow runs into groups - filters out the workflows that are not eligible for canceling
+ * (depends on cancel Mode) and assigns each workflow to groups - where workflow runs from the
+ * same group are put together in one array - in a map indexed by the source group id.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository the event comes from
+ * @param cancelMode - cancel mode to use
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - source run id for the run
+ * @param jobNameRegexps - regexps for job names
+ * @param skipEventTypes - array of event names to skip
+ * @param workflowRuns - map of workflow runs found
+ * @parm map where key is the source group id and value is array of workflow run item candidates to cancel
+ */
+function filterAndMapWorkflowRunsToGroups(repositoryInfo, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes, workflowRuns) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const mapOfWorkflowRunCandidates = new Map();
for (const [key, runItem] of workflowRuns) {
core.info(`\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status},` +
` Created at ${runItem.created_at}\n`);
- if (yield shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes)) {
- if (notifyPRCancel && runItem.event === 'pull_request') {
- const pullRequest = yield findPullRequest(octokit, owner, repo, runItem.head_repository.owner.login, runItem.head_branch, runItem.head_sha);
- if (pullRequest) {
- pullRequestToNotify.push(pullRequest.number);
- }
+ if (yield isCandidateForCancelling(repositoryInfo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes)) {
+ const candidateSourceGroup = {
+ workflowId: retrieveWorkflowIdFromUrl(runItem.workflow_url),
+ headBranch: runItem.head_branch,
+ headRepo: runItem.head_repository.full_name,
+ eventName: runItem.event
+ };
+ const sourceGroupId = getSourceGroupId(candidateSourceGroup);
+ let workflowRunArray = mapOfWorkflowRunCandidates.get(sourceGroupId);
+ if (workflowRunArray === undefined) {
+ workflowRunArray = [];
+ mapOfWorkflowRunCandidates.set(sourceGroupId, workflowRunArray);
}
- workflowsToCancel.push([runItem.id, runItem.created_at]);
- }
- }
- // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
- const sortedRunTuplesToCancel = workflowsToCancel.sort((runTuple1, runTuple2) => runTuple2[1].localeCompare(runTuple1[1]));
- if (sortedRunTuplesToCancel.length > 0) {
- if (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) {
- core.info(`\nSkipping the first run (${sortedRunTuplesToCancel[0]}) of all the matching ` +
- `duplicates - this one we are going to leave in peace!\n`);
- sortedRunTuplesToCancel.shift();
- }
- if (sortedRunTuplesToCancel.length === 0) {
- core.info(`\nNo duplicates to cancel!\n`);
- return sortedRunTuplesToCancel.map(runTuple => runTuple[0]);
- }
- core.info('\n###### Cancelling runs starting from the oldest ##########\n' +
- `\n Runs to cancel: ${sortedRunTuplesToCancel.length}\n` +
- `\n PRs to notify: ${pullRequestToNotify.length}\n`);
- for (const runTuple of sortedRunTuplesToCancel) {
- core.info(`\nCancelling run: ${runTuple}.\n`);
- yield cancelRun(octokit, owner, repo, runTuple[0]);
- }
- for (const pullRequestNumber of pullRequestToNotify) {
- const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`;
- yield addCommentToPullRequest(octokit, owner, repo, pullRequestNumber, `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`);
+ core.info(`The candidate ${runItem.id} has been added to ${sourceGroupId} group of candidates`);
+ workflowRunArray.push(runItem);
}
- core.info('\n###### Finished cancelling runs ##########\n');
}
- else {
- core.info('\n###### There are no runs to cancel! ##########\n');
- }
- return sortedRunTuplesToCancel.map(runTuple => runTuple[0]);
+ return mapOfWorkflowRunCandidates;
});
}
-function getRequiredEnv(key) {
- const value = process.env[key];
- if (value === undefined) {
- const message = `${key} was not defined.`;
- throw new Error(message);
- }
- return value;
-}
-function addCommentToPullRequest(octokit, owner, repo, pullRequestNumber, comment) {
+/**
+ * Add specified comment to Pull Request
+ * @param repositoryInfo - information about the repository used
+ * @param pullRequestNumber - number of pull request
+ * @param comment - comment to add
+ */
+function addCommentToPullRequest(repositoryInfo, pullRequestNumber, comment) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`\nNotifying PR: ${pullRequestNumber} with '${comment}'.\n`);
- yield octokit.issues.createComment({
- owner,
- repo,
+ yield repositoryInfo.octokit.issues.createComment({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
body: comment
});
});
}
-function findPullRequest(octokit, owner, repo, headRepo, headBranch, headSha) {
+/**
+ * Notifies PR about cancelling
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - my own run id
+ * @param pullRequestNumber - number of pull request
+ * @param reason reason for canceling
+ */
+function notifyPR(repositoryInfo, selfRunId, pullRequestNumber, reason) {
return __awaiter(this, void 0, void 0, function* () {
- // Finds Pull request for this workflow run
- core.info(`\nFinding PR request id for: owner: ${owner}, Repo:${repo}, Head:${headRepo}:${headBranch}.\n`);
- const pullRequests = yield octokit.pulls.list({
- owner,
- repo,
- head: `${headRepo}:${headBranch}`
- });
- for (const pullRequest of pullRequests.data) {
- core.info(`\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`);
- if (pullRequest.head.sha === headSha) {
- core.info(`\nFound PR: ${pullRequest.number}\n`);
- return pullRequest;
+ const selfWorkflowRunUrl = `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+ `/actions/runs/${selfRunId}`;
+ yield addCommentToPullRequest(repositoryInfo, pullRequestNumber, `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`);
+ });
+}
+/**
+ * Cancels all runs in the specified group of runs.
+ * @param repositoryInfo - information about the repository used
+ * @param sortedRunItems - items sorted in descending order (descending order by created_at)
+ * @param notifyPRCancel - whether to notify the PR when cancelling
+ * @param selfRunId - what is the self run id
+ * @param sourceGroupId - what is the source group id
+ * @param reason - reason for canceling
+ */
+function cancelAllRunsInTheSourceGroup(repositoryInfo, sortedRunItems, notifyPRCancel, selfRunId, sourceGroupId, reason) {
+ return __awaiter(this, void 0, void 0, function* () {
+ core.info(`\n###### Cancelling runs for ${sourceGroupId} starting from the most recent ##########\n` +
+ `\n Number of runs to cancel: ${sortedRunItems.length}\n`);
+ const cancelledRuns = [];
+ for (const runItem of sortedRunItems) {
+ if (notifyPRCancel && runItem.event === 'pull_request') {
+ const pullRequestNumber = yield findPullRequestForRunItem(repositoryInfo, runItem);
+ if (pullRequestNumber !== undefined) {
+ core.info(`\nNotifying PR: ${pullRequestNumber} (runItem: ${runItem}) with: ${reason}\n`);
+ yield notifyPR(repositoryInfo, selfRunId, pullRequestNumber, reason);
+ }
}
+ core.info(`\nCancelling run: ${runItem}.\n`);
+ yield cancelRun(repositoryInfo, runItem.id);
+ cancelledRuns.push(runItem.id);
}
- core.info(`\nCould not find the PR for this build :(\n`);
- return null;
+ core.info(`\n###### Finished cancelling runs for ${sourceGroupId} ##########\n`);
+ return cancelledRuns;
});
}
-function getOrigin(octokit, runId, owner, repo) {
+/**
+ * Cancels found runs in a smart way. It takes all the found runs group by the source group, sorts them
+ * descending according to create date in each of the source groups and cancels them in that order -
+ * optionally skipping the first found run in each source group in case of duplicates.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param mapOfWorkflowRunsCandidatesToCancel map of all workflow run candidates
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param notifyPRCancel - whether to notify PRs with comments
+ * @param selfRunId - self run Id
+ * @param reason - reason for canceling
+ */
+function cancelTheRunsPerGroup(repositoryInfo, mapOfWorkflowRunsCandidatesToCancel, cancelMode, cancelFutureDuplicates, notifyPRCancel, selfRunId, reason) {
return __awaiter(this, void 0, void 0, function* () {
- const reply = yield octokit.actions.getWorkflowRun({
- owner,
- repo,
+ const cancelledRuns = [];
+ for (const [sourceGroupId, candidatesArray] of mapOfWorkflowRunsCandidatesToCancel) {
+ // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
+ const sortedRunItems = candidatesArray.sort((runItem1, runItem2) => runItem2.created_at.localeCompare(runItem1.created_at));
+ if (sortedRunItems.length > 0) {
+ if ((cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) ||
+ cancelMode === CancelMode.ALL_DUPLICATES) {
+ core.info(`\nSkipping the first run (${sortedRunItems[0].id}) of all the matching ` +
+ `duplicates for '${sourceGroupId}'. This one we are going to leave in peace!\n`);
+ sortedRunItems.shift();
+ }
+ if (sortedRunItems.length === 0) {
+ core.info(`\nNo duplicates to cancel for ${sourceGroupId}!\n`);
+ continue;
+ }
+ cancelledRuns.push(...(yield cancelAllRunsInTheSourceGroup(repositoryInfo, sortedRunItems, notifyPRCancel, selfRunId, sourceGroupId, reason)));
+ }
+ else {
+ core.info(`\n###### There are no runs to cancel for ${sourceGroupId} ##########\n`);
+ }
+ }
+ return cancelledRuns;
+ });
+}
+/**
+ * Find and cancels runs based on the criteria chosen.
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ * (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ * @param reason - reason for cancelling
+ * @return array of canceled workflow run ids
+ */
+function findAndCancelRuns(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const statusValues = ['queued', 'in_progress'];
+ const mySourceGroup = {
+ headBranch,
+ headRepo,
+ eventName: sourceEventName,
+ workflowId: sourceWorkflowId
+ };
+ const workflowRuns = yield getWorkflowRunsMatchingCriteria(repositoryInfo, statusValues, cancelMode, sourceWorkflowId, sourceRunId, sourceEventName, mySourceGroup);
+ const mapOfWorkflowRunsCandidatesToCancel = yield filterAndMapWorkflowRunsToGroups(repositoryInfo, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes, workflowRuns);
+ return yield cancelTheRunsPerGroup(repositoryInfo, mapOfWorkflowRunsCandidatesToCancel, cancelMode, cancelFutureDuplicates, notifyPRCancel, selfRunId, reason);
+ });
+}
+/**
+ * Returns environment variable that is required - throws error if it is not defined.
+ * @param key key for the env variable
+ * @return value of the env variable
+ */
+function getRequiredEnv(key) {
+ const value = process.env[key];
+ if (value === undefined) {
+ const message = `${key} was not defined.`;
+ throw new Error(message);
+ }
+ return value;
+}
+/**
+ * Gets origin of the runId - if this is a workflow run, it returns the information about the source run
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run id of the run to check
+ * @return information about the triggering workflow
+ */
+function getOrigin(repositoryInfo, runId) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const reply = yield repositoryInfo.octokit.actions.getWorkflowRun({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
});
@@ -1839,22 +2157,44 @@ function getOrigin(octokit, runId, owner, repo) {
`Event: ${sourceRun.event}, Head sha: ${sourceRun.head_sha}, url: ${sourceRun.url}`);
let pullRequest = null;
if (sourceRun.event === 'pull_request') {
- pullRequest = yield findPullRequest(octokit, owner, repo, sourceRun.head_repository.owner.login, sourceRun.head_branch, sourceRun.head_sha);
- }
- return [
- reply.data.head_repository.full_name,
- reply.data.head_branch,
- reply.data.event,
- reply.data.head_sha,
- pullRequest ? pullRequest.merge_commit_sha : '',
- pullRequest
- ];
+ pullRequest = yield findPullRequest(repositoryInfo, sourceRun.head_repository.owner.login, sourceRun.head_branch, sourceRun.head_sha);
+ }
+ return {
+ headRepo: reply.data.head_repository.full_name,
+ headBranch: reply.data.head_branch,
+ headSha: reply.data.head_sha,
+ mergeCommitSha: pullRequest ? pullRequest.merge_commit_sha : null,
+ pullRequest: pullRequest ? pullRequest : null,
+ eventName: reply.data.event
+ };
});
}
-function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates) {
+/**
+ * Performs the actual cancelling.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ * (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRCancelMessage - message to send when cancelling the PR (overrides default message
+ * generated automatically)
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ */
+function performCancelJob(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates) {
return __awaiter(this, void 0, void 0, function* () {
core.info('\n###################################################################################\n');
- core.info(`All parameters: owner: ${owner}, repo: ${repo}, run id: ${sourceRunId}, ` +
+ core.info(`All parameters: owner: ${repositoryInfo.owner}, repo: ${repositoryInfo.repo}, run id: ${sourceRunId}, ` +
`head repo ${headRepo}, headBranch: ${headBranch}, ` +
`sourceEventName: ${sourceEventName}, cancelMode: ${cancelMode}, jobNames: ${jobNameRegexps}`);
core.info('\n###################################################################################\n');
@@ -1874,20 +2214,139 @@ function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, own
reason = `It has jobs matching ${jobNameRegexps}.`;
}
else if (cancelMode === CancelMode.DUPLICATES) {
- core.info(`# Cancel duplicate runs started before ${sourceRunId} for workflow ${sourceWorkflowId}.`);
- reason = `It in earlier duplicate of ${sourceWorkflowId} run.`;
+ core.info(`# Cancel duplicate runs for workflow ${sourceWorkflowId} for same triggering branch as own run Id.`);
+ reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`;
+ }
+ else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+ core.info(`# Cancel all duplicates runs started for workflow ${sourceWorkflowId}.`);
+ reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`;
}
else {
throw Error(`Wrong cancel mode ${cancelMode}! This should never happen.`);
}
core.info('\n###################################################################################\n');
- return yield findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
+ return yield findAndCancelRuns(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
});
}
+/**
+ * Retrieves information about source workflow Id. Either from the current event or from the workflow
+ * nme specified. If the file name is not specified, the workflow to act on is either set to self event
+ * or in case of workflow_run event - to the workflow id that triggered the 'workflow_run' event.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param workflowFileName - optional workflow file name
+ * @param sourceRunId - source run id of the workfow
+ * @param selfRunId - self run id
+ * @return workflow id that is associate with the workflow we are going to act on.
+ */
+function retrieveWorkflowId(repositoryInfo, workflowFileName, sourceRunId, selfRunId) {
+ return __awaiter(this, void 0, void 0, function* () {
+ let sourceWorkflowId;
+ if (workflowFileName) {
+ sourceWorkflowId = workflowFileName;
+ core.info(`\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`);
+ }
+ else {
+ sourceWorkflowId = yield getWorkflowId(repositoryInfo, sourceRunId);
+ if (sourceRunId === selfRunId) {
+ core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`);
+ }
+ else {
+ core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`);
+ }
+ }
+ return sourceWorkflowId;
+ });
+}
+/**
+ * Sets output but also prints the output value in the logs.
+ *
+ * @param name name of the output
+ * @param value value of the output
+ */
function verboseOutput(name, value) {
core.info(`Setting output: ${name}: ${value}`);
core.setOutput(name, value);
}
+/**
+ * Performs sanity check of the parameters passed. Some of the parameter combinations do not work so they
+ * are verified and in case od unexpected combination found, approrpriate error is raised.
+ *
+ * @param eventName - name of the event to act on
+ * @param sourceRunId - run id of the triggering event
+ * @param selfRunId - our own run id
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether future duplicate cancelling is enabled
+ * @param jobNameRegexps - array of regular expression of job names
+ */
+function performSanityChecks(eventName, sourceRunId, selfRunId, cancelMode, cancelFutureDuplicates, jobNameRegexps) {
+ if (eventName === 'workflow_run' &&
+ sourceRunId === selfRunId &&
+ cancelMode === CancelMode.DUPLICATES) {
+ throw Error(`You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
+ 'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
+ 'See the docs for details.');
+ }
+ if (jobNameRegexps.length > 0 &&
+ [
+ CancelMode.DUPLICATES,
+ CancelMode.SELF,
+ CancelMode.ALL_DUPLICATES
+ ].includes(cancelMode)) {
+ throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`);
+ }
+ if (cancelMode === CancelMode.ALL_DUPLICATES && !cancelFutureDuplicates) {
+ throw Error(`The ${cancelMode} cancelMode has to have cancelFutureDuplicates set to true.`);
+ }
+}
+/**
+ * Produces basic outputs for the action. This does not include cancelled workflow run id - those are
+ * set after cancelling is done.
+ *
+ * @param triggeringWorkflowInfo
+ */
+function produceBasicOutputs(triggeringWorkflowInfo) {
+ verboseOutput('sourceHeadRepo', triggeringWorkflowInfo.headRepo);
+ verboseOutput('sourceHeadBranch', triggeringWorkflowInfo.headBranch);
+ verboseOutput('sourceHeadSha', triggeringWorkflowInfo.headSha);
+ verboseOutput('sourceEvent', triggeringWorkflowInfo.eventName);
+ verboseOutput('pullRequestNumber', triggeringWorkflowInfo.pullRequest
+ ? triggeringWorkflowInfo.pullRequest.number.toString()
+ : '');
+ verboseOutput('mergeCommitSha', triggeringWorkflowInfo.mergeCommitSha
+ ? triggeringWorkflowInfo.mergeCommitSha
+ : '');
+ verboseOutput('targetCommitSha', triggeringWorkflowInfo.mergeCommitSha
+ ? triggeringWorkflowInfo.mergeCommitSha
+ : triggeringWorkflowInfo.headSha);
+}
+/**
+ * Notifies the PR that the action has started.
+ *
+ * @param repositoryInfo information about the repository
+ * @param triggeringWorkflowInfo information about the triggering workflow
+ * @param sourceRunId run id of the source workflow
+ * @param selfRunId self run id
+ * @param notifyPRMessageStart whether to notify about the start of the action
+ */
+function notifyActionStart(repositoryInfo, triggeringWorkflowInfo, sourceRunId, selfRunId, notifyPRMessageStart) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (notifyPRMessageStart && triggeringWorkflowInfo.pullRequest) {
+ const selfWorkflowRunUrl = `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+ `/actions/runs/${selfRunId}`;
+ yield repositoryInfo.octokit.issues.createComment({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
+ // eslint-disable-next-line @typescript-eslint/camelcase
+ issue_number: triggeringWorkflowInfo.pullRequest.number,
+ body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
+ });
+ }
+ });
+}
+/**
+ * Main run method that does everything :)
+ */
function run() {
return __awaiter(this, void 0, void 0, function* () {
const token = core.getInput('token', { required: true });
@@ -1911,55 +2370,23 @@ function run() {
: [];
const workflowFileName = core.getInput('workflowFileName');
const [owner, repo] = repository.split('/');
+ const repositoryInfo = {
+ octokit,
+ owner,
+ repo
+ };
core.info(`\nGetting workflow id for source run id: ${sourceRunId}, owner: ${owner}, repo: ${repo},` +
` skipEventTypes: ${skipEventTypes}\n`);
- let sourceWorkflowId;
- if (workflowFileName) {
- sourceWorkflowId = workflowFileName;
- core.info(`\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`);
- }
- else {
- sourceWorkflowId = yield getWorkflowId(octokit, sourceRunId, owner, repo);
- if (sourceRunId === selfRunId) {
- core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`);
- }
- else {
- core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`);
- }
- if (eventName === 'workflow_run' && sourceRunId === selfRunId) {
- if (cancelMode === CancelMode.DUPLICATES)
- throw Error(`You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
- 'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
- 'See the docs for details.');
- }
- }
+ const sourceWorkflowId = yield retrieveWorkflowId(repositoryInfo, workflowFileName, sourceRunId, selfRunId);
+ performSanityChecks(eventName, sourceRunId, selfRunId, cancelMode, cancelFutureDuplicates, jobNameRegexps);
core.info(`Repository: ${repository}, Owner: ${owner}, Repo: ${repo}, ` +
`Event name: ${eventName}, CancelMode: ${cancelMode}, ` +
`sourceWorkflowId: ${sourceWorkflowId}, sourceRunId: ${sourceRunId}, selfRunId: ${selfRunId}, ` +
`jobNames: ${jobNameRegexps}`);
- if (jobNameRegexps.length > 0 &&
- [CancelMode.DUPLICATES, CancelMode.SELF].includes(cancelMode)) {
- throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`);
- }
- const [headRepo, headBranch, sourceEventName, headSha, mergeCommitSha, pullRequest] = yield getOrigin(octokit, sourceRunId, owner, repo);
- verboseOutput('sourceHeadRepo', headRepo);
- verboseOutput('sourceHeadBranch', headBranch);
- verboseOutput('sourceHeadSha', headSha);
- verboseOutput('sourceEvent', sourceEventName);
- verboseOutput('pullRequestNumber', pullRequest ? pullRequest.number.toString() : '');
- verboseOutput('mergeCommitSha', mergeCommitSha);
- verboseOutput('targetCommitSha', pullRequest ? mergeCommitSha : headSha);
- const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`;
- if (notifyPRMessageStart && pullRequest) {
- yield octokit.issues.createComment({
- owner,
- repo,
- // eslint-disable-next-line @typescript-eslint/camelcase
- issue_number: pullRequest.number,
- body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
- });
- }
- const cancelledRuns = yield performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates);
+ const triggeringWorkflowInfo = yield getOrigin(repositoryInfo, sourceRunId);
+ produceBasicOutputs(triggeringWorkflowInfo);
+ yield notifyActionStart(repositoryInfo, triggeringWorkflowInfo, sourceRunId, selfRunId, notifyPRMessageStart);
+ const cancelledRuns = yield performCancelJob(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, triggeringWorkflowInfo.headRepo, triggeringWorkflowInfo.headBranch, triggeringWorkflowInfo.eventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates);
verboseOutput('cancelledRuns', JSON.stringify(cancelledRuns));
});
}
@@ -9118,3836 +9545,6 @@ module.exports = (promise, onFinally) => {
/***/ }),
-/***/ 706:
-/***/ (function(module) {
-
-(function webpackUniversalModuleDefinition(root, factory) {
- if(true)
- module.exports = factory();
- else { var i, a; }
-})(typeof self !== 'undefined' ? self : this, function() {
-return /******/ (function(modules) { // webpackBootstrap
-/******/ // The module cache
-/******/ var installedModules = {};
-/******/
-/******/ // The require function
-/******/ function __webpack_require__(moduleId) {
-/******/
-/******/ // Check if module is in cache
-/******/ if(installedModules[moduleId]) {
-/******/ return installedModules[moduleId].exports;
-/******/ }
-/******/ // Create a new module (and put it into the cache)
-/******/ var module = installedModules[moduleId] = {
-/******/ i: moduleId,
-/******/ l: false,
-/******/ exports: {}
-/******/ };
-/******/
-/******/ // Execute the module function
-/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
-/******/ // Flag the module as loaded
-/******/ module.l = true;
-/******/
-/******/ // Return the exports of the module
-/******/ return module.exports;
-/******/ }
-/******/
-/******/
-/******/ // expose the modules object (__webpack_modules__)
-/******/ __webpack_require__.m = modules;
-/******/
-/******/ // expose the module cache
-/******/ __webpack_require__.c = installedModules;
-/******/
-/******/ // define getter function for harmony exports
-/******/ __webpack_require__.d = function(exports, name, getter) {
-/******/ if(!__webpack_require__.o(exports, name)) {
-/******/ Object.defineProperty(exports, name, {
-/******/ configurable: false,
-/******/ enumerable: true,
-/******/ get: getter
-/******/ });
-/******/ }
-/******/ };
-/******/
-/******/ // getDefaultExport function for compatibility with non-harmony modules
-/******/ __webpack_require__.n = function(module) {
-/******/ var getter = module && module.__esModule ?
-/******/ function getDefault() { return module['default']; } :
-/******/ function getModuleExports() { return module; };
-/******/ __webpack_require__.d(getter, 'a', getter);
-/******/ return getter;
-/******/ };
-/******/
-/******/ // Object.prototype.hasOwnProperty.call
-/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
-/******/
-/******/ // __webpack_public_path__
-/******/ __webpack_require__.p = "";
-/******/
-/******/ // Load entry module and return exports
-/******/ return __webpack_require__(__webpack_require__.s = 5);
-/******/ })
-/************************************************************************/
-/******/ ([
-/* 0 */
-/***/ (function(module, exports, __nested_webpack_require_2850__) {
-
-"use strict";
-
-
-/**
- * @private
- */
-const RED = 1;
-/**
- * @private
- */
-const BLACK = 2;
-
-/**
- * @private
- * A node for a red-black tree
- */
-class TreeNode {
-
- /**
- * Default constructor
- */
- constructor() {
- /** left child */
- this.left = null;
- /** right child */
- this.right = null;
- /** parent node */
- this.parent = null;
- /** key object (additional 'value' data member is added in map-like classes) */
- this.key = null;
- /** by default new node is red */
- this.color = RED;
- }
-
- /**
- * @returns parent of parent
- */
- grandparent() {
- let p = this.parent;
- if (p === null) {
- return null;
- } // No parent means no grandparent
- return p.parent;
- }
-
- /**
- * @returns the other child of the same parent
- */
- sibling() {
- let p = this.parent;
- if (p === null) {
- return null;
- } // No parent means no sibling
- if (this === p.left) {
- return p.right;
- }
- else {
- return p.left;
- }
- }
-
- /**
- * @returns another child of the grandparent
- */
- uncle() {
- let p = this.parent;
- if (p === null) {
- return null;
- } // No parent means no uncle
- let g = p.parent;
- if (g === null) {
- return null;
- } // No grandparent means no uncle
- return p.sibling();
- }
-}
-
-module.exports = {
- TreeNode: TreeNode,
- BLACK: BLACK,
- RED: RED
-};
-
-/***/ }),
-/* 1 */
-/***/ (function(module, exports) {
-
-/**
- * Used by sets
- * @private
- */
-class KeyOnlyPolicy {
- /**
- * Returns key data from the specified node
- * @param {*} n
- */
- fetch(n) {
- return n.key;
- }
-
- /**
- * Copies key data from one node to another
- * @param {*} dst
- * @param {*} src
- */
- copy(dst, src) {
- dst.key = src.key;
- }
-
- /**
- * @returns string representation of the key
- * @param {*} node
- */
- toString(node) {
- return String(node.key);
- }
-}
-
-/**
- * Used by maps
- * @private
- */
-class KeyValuePolicy {
- /**
- * Returns key-value data from the specified node
- * @param {*} n
- */
- fetch(n) {
- return [n.key, n.value];
- }
-
- /**
- * Copies key-value data from one node to another
- * @param {*} dst
- * @param {*} src
- */
- copy(dst, src) {
- dst.key = src.key;
- dst.value = src.value;
- }
-
- /**
- * @returns string representation of key-value pair
- * @param {*} node
- */
- toString(node) {
- return String(node.key) + ':' + String(node.value);
- }
-}
-
-/**
- * Used for iteration through values of a map
- * @private
- */
-class ValueOnlyPolicy {
- /**
- * Returns data from the specified node
- * @param {*} n
- */
- fetch(n) {
- return n.value;
- }
-
- /**
- * Copies value data from one node to another
- * @param {*} dst
- * @param {*} src
- */
- copy(dst, src) {
- dst.value = src.value;
- }
-
- /**
- * @returns string representation of node's value
- * @param {*} node
- */
- toString(node) {
- return String(node.value);
- }
-}
-
-module.exports = {
- KeyOnlyPolicy: KeyOnlyPolicy,
- ValueOnlyPolicy: ValueOnlyPolicy,
- KeyValuePolicy: KeyValuePolicy
-};
-
-/***/ }),
-/* 2 */
-/***/ (function(module, exports, __nested_webpack_require_6477__) {
-
-"use strict";
-
-
-/** @ignore */
-const {TreeNode, RED, BLACK} = __nested_webpack_require_6477__(0);
-/** @ignore */
-const {JsIterator, JsReverseIterator} = __nested_webpack_require_6477__(3);
-/** @ignore */
-const {Iterator, ReverseIterator} = __nested_webpack_require_6477__(4);
-/** @ignore */
-const {KeyOnlyPolicy, ValueOnlyPolicy, KeyValuePolicy} = __nested_webpack_require_6477__(1);
-/** @ignore */
-const {InsertionResult} = __nested_webpack_require_6477__(8);
-
-/** insertion mode of a multimap, nodes with the same keys can be added */
-const INSERT_MULTI = 1;
-/** if a node with the same key already exists then the subsequent attempts are ignored */
-const INSERT_UNIQUE = 2;
-/** if a node with the same key already exists then it's value is replaced on subsequent attempts */
-const INSERT_REPLACE = 3;
-
-/**
- * @private
- * Special node in a tree is created for performance reasons
- */
-class Head {
- /** default constructor */
- constructor() {
- /** node with the smallest key */
- this.leftmost = this;
- /** node with the largest key */
- this.rightmost = this;
- /** root node of the tree */
- this.root = this;
- /** number of nodes in the tree */
- this.size = 0;
- /** extra tag used in debuggin of unit tests */
- this.id = 'HEAD';
- }
-}
-
-/**
- * @private
- * 3-way comparison, similar to strcmp and memcp in C programming language
- * @returns +1 if the value of rhs is greater than lhs
- * -1 if the value of rhs is less than lhs
- * 0 if values are the same
- */
-function compare(lhs, rhs) {
- if (lhs < rhs) {
- return -1;
- }
- else if (lhs === rhs) {
- return 0;
- }
- else {
- return 1;
- }
-}
-
-/**
- * Red-black tree
- * @access private
- */
-class Tree {
- /** default constructor of an empty tree */
- constructor() {
- /** head */
- this.head = new Head();
- /** 3-way comparison function */
- this.compare = compare;
- /** must be an instance of KeyOnlyPolicy for sets, or KeyValuePolicy for maps */
- this.valuePolicy = new KeyOnlyPolicy();
- }
-
- /**
- * Deletes all nodes in the tree
- */
- clear() {
- this.head = new Head();
- }
-
- /**
- * @returns number of nodes in the tree
- */
- size() {
- return this.head.size;
- }
-
- /**
- * @private
- * A wrapper that calls 3-way comparison of node keys
- * @param {*} lhs
- * @param {*} rhs
- */
- compareNodes(lhs, rhs) {
- return this.compare(lhs.key, rhs.key);
- }
-
- /**
- * @private
- * used by rotation operations
- */
- replaceNode(oldNode, newNode) {
- if (oldNode === newNode) {
- return;
- }
- if (oldNode.parent === null) {
- this.head.root = newNode;
- }
- else {
- if (oldNode === oldNode.parent.left) {
- oldNode.parent.left = newNode;
- }
- else {
- oldNode.parent.right = newNode;
- }
- }
-
- if (!this.isLeaf(newNode)) {
- newNode.parent = oldNode.parent;
- }
- }
-
- /**
- * Rebalances tree as described below
-
- X Y
- / \ / \
- Y c right rotate --> a X
- / \ <-- left rotate / \
- a b b c
- * @private
- */
- rotateLeft(node) {
- let right = node.right;
- if (this.isLeaf(right)) {
- throw new Error('rotateLeft can\'t be performed. The tree is corrupted');
- }
- this.replaceNode(node, right);
-
- node.right = right.left;
- if (right.left !== null) {
- right.left.parent = node;
- }
-
- right.left = node;
- node.parent = right;
- }
-
- /**
- * Rebalances tree as described in rotateLeft
- * @param {*} node - parent node
- */
- rotateRight(node) {
- let left = node.left;
- if (this.isLeaf(left)) {
- throw new Error('rotateRight can\'t be performed. The tree is corrupted');
- }
- this.replaceNode(node, left);
-
- node.left = left.right;
- if (left.right !== null) {
- left.right.parent = node;
- }
-
- left.right = node;
- node.parent = left;
- }
-
- /**
- * @returns true - for null pointers and head node; false - for all other nodes
- * @param {*} node
- */
- isLeaf(node) {
- if (node === null || node === this.head) {
- return true;
- }
- return false;
- }
-
- /**
- * Leaf nodes are considered 'black'. All real nodes contain 'color' data member
- * @param {*} node
- */
- fetchColor(node) {
- if (this.isLeaf(node)) {
- return BLACK;
- }
- else {
- return node.color;
- }
- }
-
- /**
- * Tests a node for 'blackness'.
- * @param {*} node
- */
- isBlack(node) {
- return (this.fetchColor(node) === BLACK);
- }
-
- /**
- * Tests node for 'redness'.
- * @param {*} node
- */
- isRed(node) {
- return (this.fetchColor(node) === RED);
- }
-
- /* ===========================
- INSERT
- =========================== */
- /**
- * A node will be inserted into the tree even if nodes with the same key already exist
- * @param {*} node
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- */
- insertMulti(node) {
- return this.insertNode(node, INSERT_MULTI);
- }
-
- /**
- * The node is inserted into the tree only if nodes with the same key do not exist there
- * @param {*} node
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- */
- insertUnique(node) {
- return this.insertNode(node, INSERT_UNIQUE);
- }
-
- /**
- * The node is inserted. If a node with the same key exists it's value will be replaced by the value of the new node
- * @param {*} node
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- */
- insertOrReplace(node) {
- return this.insertNode(node, INSERT_REPLACE);
- }
-
- /**
- * @private
- * Inserts node. Updates head node. Rebalances tree.
- * @param {*} n - node
- * @param {*} mode - one of INSERT_MULTI, INSERT_UNIQUE, INSERT_REPLACE
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- */
- insertNode(n, mode = INSERT_MULTI) {
- let res = this.insertNodeInternal(this.head.root, n, mode);
- if (res.wasAdded) {
- if (this.head.size === 0) {
- this.head.root = n;
- this.head.leftmost = n;
- this.head.rightmost = n;
-
- n.left = this.head;
- n.right = this.head;
- }
- else if (this.head.leftmost.left === n) {
- this.head.leftmost = n;
- n.left = this.head;
- }
- else if (this.head.rightmost.right === n) {
- this.head.rightmost = n;
- n.right = this.head;
- }
- this.insertRepairTree(n);
- this.head.size = this.head.size + 1;
- }
- return res;
- }
-
- /**
- * @private
- * Inserts node according to the mode
- * @param {*} root - root node of the tree
- * @param {*} n - node to be inserted
- * @param {*} mode - one of INSERT_MULTI, INSERT_UNIQUE, INSERT_REPLACE
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- */
- insertNodeInternal(root, n, mode) {
- // recursively descend the tree until a leaf is found
- let x = root;
- let y = null;
- let rc = -1;
- // find matching node
- while (!this.isLeaf(x)) {
- y = x;
- rc = this.compareNodes(n, y);
- if (rc < 0) {
- x = y.left;
- }
- else if (rc > 0) {
- x = y.right;
- }
- else {
- // node with the same key value
- switch (mode) {
- case INSERT_UNIQUE:
- // it's a duplicate
- return new InsertionResult(false, false, undefined);
- case INSERT_REPLACE:
- this.valuePolicy.copy(y, n);
- return new InsertionResult(false, true, new Iterator(y, this));
- default:
- // INSERT_MULTI
- x = y.right;
- }
- }
- }
- if (this.isLeaf(y)) {
- n.parent = null;
- n.left = this.head;
- n.right = this.head;
- }
- else {
- n.parent = y;
- if (rc < 0) {
- y.left = n;
- }
- else {
- y.right = n;
- }
- }
- return new InsertionResult(true, false, new Iterator(n, this));
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
- * @param {*} n - node
- */
- insertRepairTree(n) {
- if (n.parent === null) {
- this.repairCase1(n);
- }
- else if (this.isBlack(n.parent)) {
- /* insert_case2(n);
- // do nothing */
- }
- else if (this.isRed(n.uncle())) {
- this.repairCase3(n);
- }
- else {
- this.repairCase4(n);
- }
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
- * @param {*} n - node
- */
- repairCase1(n) {
- n.color = BLACK;
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
- * @param {*} n - node
- */
- repairCase3(n) {
- n.parent.color = BLACK;
- n.uncle().color = BLACK;
- n.grandparent().color = RED;
- this.insertRepairTree(n.grandparent());
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
- * @param {*} n - node
- */
- repairCase4(n) {
- let p = n.parent;
- let g = n.grandparent();
-
- let nr = null;
- if ((g.left !== null)
- && (n === g.left.right)) {
- this.rotateLeft(p);
- n = n.left;
- }
- else if ((g.right !== null)
- && (n === g.right.left)) {
- this.rotateRight(p);
- n = n.right;
- }
-
- p = n.parent;
- g = n.grandparent();
- if (n === p.left) {
- this.rotateRight(g);
- }
- else {
- this.rotateLeft(g);
- }
-
- p.color = BLACK;
- g.color = RED;
- }
-
- /**
- * @returns the node with the highest key for the subtree of the specified root node
- * @param {*} node - root node of the subtree to be evaluated
- */
- fetchMaximum(node) {
- while (!this.isLeaf(node.right)) {
- node = node.right;
- }
-
- return node;
- }
-
- /**
- * @returns the node with the lowest key for the subtree of the specified root node
- * @param {*} node - root node of the subtree to be evaluated
- */
- fetchMinimum(node) {
- while (!this.isLeaf(node.left)) {
- node = node.left;
- }
-
- return node;
- }
-
- /* ===========================
- ERASE
- =========================== */
- /**
- * Removes node from the tree
- * @param {*} node
- */
- erase(node) {
- if (this.isLeaf(node)) {
- return;
- }
-
- this.eraseInternal(node);
- let h = this.head;
- h.size = h.size - 1;
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
- * @param {*} node - node
- */
- eraseInternal(node) {
- if (!this.isLeaf(node.left)
- && !this.isLeaf(node.right)) {
- let pred = this.fetchMaximum(node.left);
-
- this.valuePolicy.copy(node, pred);
- node = pred;
- }
-
- let child = (this.isLeaf(node.right)) ? node.left : node.right;
-
- if (this.isBlack(node)) {
- this.eraseCase1(node);
- }
- this.replaceNode(node, child);
- if (this.head.size === 2) {
- if (!this.isLeaf(child)) {
- // Root node must be BLACK
- child.color = BLACK;
- }
- }
-
- let h = this.head;
- if (this.isLeaf(child)) {
- /* The node didn't have children and it was removed
- the head needs to update leftmost, rightmost pointers */
- if (h.leftmost === node) {
- let p = node.parent;
- if (p !== null) {
- h.leftmost = p;
- p.left = h;
- }
- else {
- h.leftmost = h;
- }
- }
- if (h.rightmost === node) {
- let p = node.parent;
- if (p !== null) {
- h.rightmost = p;
- p.right = h;
- }
- else {
- h.rightmost = h;
- }
- }
- }
- else {
- // the node had a child. Now node is removed. Any references should point to the child now
- if (h.leftmost === node) {
- h.leftmost = child;
- child.left = h;
- }
- if (h.rightmost === node) {
- h.rightmost = child;
- child.right = h;
- }
- }
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
- * @param {*} node
- */
- eraseCase1(node) {
- if (node.parent === null) {
- return;
- }
- else {
- this.eraseCase2(node);
- }
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
- * @param {*} node
- */
- eraseCase2(node) {
- let s = node.sibling();
-
- if (this.isRed(s)) {
- node.parent.color = RED;
- s.color = BLACK;
-
- if (node === node.parent.left) {
- this.rotateLeft(node.parent);
- }
- else {
- this.rotateRight(node.parent);
- }
- }
- this.eraseCase3(node);
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
- * @param {*} node
- */
- eraseCase3(node) {
- let s = node.sibling();
- let p = node.parent;
- if (this.isBlack(p)
- && this.isBlack(s)
- && this.isBlack(s.left)
- && this.isBlack(s.right)) {
-
- s.color = RED;
- this.eraseCase1(p);
- }
- else {
- this.eraseCase4(node);
- }
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
- * @param {*} node
- */
- eraseCase4(node) {
- let s = node.sibling();
- let p = node.parent;
- if (this.isRed(p)
- && this.isBlack(s)
- && this.isBlack(s.left)
- && this.isBlack(s.right)) {
-
- s.color = RED;
- p.color = BLACK;
- }
- else {
- this.eraseCase5(node);
- }
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
- * @param {*} node
- */
- eraseCase5(node) {
- let s = node.sibling();
- let p = node.parent;
- /* The check below is unnecessary
- due to case 2 (even though case 2 changed the sibling to a sibling's child,
- the sibling's child can't be red, since no red parent can have a red child). */
- /* if ((!this.isLeaf(s))
- && this.isBlack(s)) { */
-
- /* the following statements just force the red to be on the left of the left of the parent,
- or right of the right, so case six will rotate correctly. */
- if (node === p.left
- && this.isRed(s.left)
- && this.isBlack(s.right)) {
-
- s.color = RED;
- s.left.color = BLACK;
- this.rotateRight(s);
- }
- else if (node === p.right
- && this.isBlack(s.left)
- && this.isRed(s.right)) {
-
- s.color = RED;
- s.right.color = BLACK;
- this.rotateLeft(s);
- }
- //}
- this.eraseCase6(node);
- }
-
- /**
- * @private
- * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
- * @param {*} node
- */
- eraseCase6(node) {
- let s = node.sibling();
- let p = node.parent;
- s.color = this.fetchColor(p);
- p.color = BLACK;
-
- if (node === p.left) {
- s.right.color = BLACK;
- this.rotateLeft(p);
- }
- else {
- s.left.color = BLACK;
- this.rotateRight(p);
- }
- }
-
- /* ===========================
- SEARCH BY KEY
- =========================== */
- /**
- * @returns an iterator pointin to a node with matching key value. If node is not found then end() iterator is returned.
- * @param {*} k - key value
- */
- find(k) {
- let y = this.head;
- let x = y.root;
- while (!this.isLeaf(x)) {
- let rc = this.compare(x.key, k);
- if (rc > 0) {
- y = x;
- x = x.left;
- }
- else if (rc < 0) {
- y = x;
- x = x.right;
- }
- else {
- return new Iterator(x, this);
- }
- }
- return new Iterator(this.head, this);
- }
-
- /**
- * @returns an iterator pointing to the first node in the tree that is not less than
- * (i.e. greater or equal to) the specified key value, or end() if no such node is found.
- * @param {*} k - key value
- */
- lowerBound(k) {
- let y = this.head;
- let x = y.root;
- while (!this.isLeaf(x)) {
- let rc = this.compare(x.key, k);
- if (rc >= 0) {
- y = x;
- x = x.left;
- }
- else {
- x = x.right;
- }
- }
- return new Iterator(y, this);
- }
-
- /**
- * @returns an iterator pointing to the first node in the tree that is greater than
- * the specified key value, or end() if no such node is found.
- * @param {*} k - key value
- */
- upperBound(k) {
- let y = this.head;
- let x = y.root;
- while (!this.isLeaf(x)) {
- let rc = this.compare(x.key, k);
- if (rc > 0) {
- y = x;
- x = x.left;
- }
- else {
- x = x.right;
- }
- }
- return new Iterator(y, this);
- }
-
- /* ===========================
- ITERATORS
- =========================== */
-
- /**
- * @returns iterator pointing to the node with the lowest key
- */
- begin() {
- return new Iterator(this.head.leftmost, this);
- }
-
- /**
- * @returns iterator pointing to the node following the node with the highest key
- */
- end() {
- return new Iterator(this.head, this);
- }
-
- /**
- * @returns iterator pointing to the node with the highest key
- */
- rbegin() {
- return new ReverseIterator(this.head.rightmost, this);
- }
-
- /**
- * @returns iterator pointing to the node preceding the node with the lowest key
- */
- rend() {
- return new ReverseIterator(this.head, this);
- }
-
- /**
- * @private
- * provides support for ES6 forward iteration
- */
- jsBegin() {
- return this.head.leftmost;
- }
-
- /**
- * @private
- * provides support for ES6 forward iteration
- */
- jsEnd() {
- return this.head;
- }
-
- /**
- * @private
- * provides support for ES6 reverse iteration
- */
- jsRbegin() {
- return this.head.rightmost;
- }
-
- /**
- * @private
- * provides support for ES6 forward iteration
- */
- jsRend() {
- return this.head;
- }
-
- /**
- * @returns node following the specified node in ascending order of their keys
- * @param {*} n - node
- */
- next(n) {
- if (n === this.head) {
- return this.head.leftmost;
- }
- if (n.right === this.head) {
- return this.head;
- }
- if (n.right !== null) {
- let res = this.fetchMinimum(n.right);
- return res;
- }
- else {
- while (n.parent.left !== n) {
- n = n.parent;
- }
- return n.parent;
- }
- }
-
- /**
- * @returns node preceding the specified node in ascending order of their keys
- * @param {*} n - node
- */
- prev(n) {
- if (n === this.head) {
- return this.head.rightmost;
- }
- if (n.left === this.head) {
- return this.head;
- }
- if (n.left !== null) {
- let res = this.fetchMaximum(n.left);
- return res;
- }
- else {
- while (n.parent.right !== n) {
- n = n.parent;
- }
- return n.parent;
- }
- }
-
- /**
- * ES6 forward iteration
- */
- [Symbol.iterator]() {
- return new JsIterator(this);
- }
-
- /**
- * ES6 reverse iteration
- */
- backward() {
- return new JsReverseIterator(this);
- }
-
- /**
- * @returns a new JsIterator object that contains the [key, value] pairs for each element in the order of the keys.
- */
- entries() {
- return new JsIterator(this);
- }
-
- /**
- * @returns a new JsIterator object that contains the keys for each element in the order of the keys.
- */
- keys() {
- return new JsIterator(this, new KeyOnlyPolicy());
- }
-
- /**
- * @returns a new JsIterator object that contains the values for each element in the order of the keys.
- */
- values() {
- return new JsIterator(this, new ValueOnlyPolicy());
- }
-
- /**
- * @returns first element of the container, or undefined if container is empty
- */
- first() {
- if (this.size() === 0) {
- return undefined;
- }
- else {
- let it = this.begin();
- return this.valuePolicy.fetch(it.node);
- }
- }
-
- /**
- * @returns last element of the container, or undefined if container is empty
- */
- last() {
- if (this.size() === 0) {
- return undefined;
- }
- else {
- let it = this.rbegin();
- return this.valuePolicy.fetch(it.node);
- }
- }
-
- /**
- * @returns String representation of the container
- */
- toString() {
- let parts = [];
- for (let it = this.begin(); !it.equals(this.end()); it.next()) {
- // convert each key-value pair
- parts.push(this.valuePolicy.toString(it.node));
- }
- return '{' + parts.join(',') + '}';
- }
-
- /**
- * @returns String tag of this class
- */
- get [Symbol.toStringTag]() {
- return 'Tree';
- }
-
- /**
- * @returns constructor object for this class
- */
- static get [Symbol.species]() {
- return Tree;
- }
-
-}
-
-module.exports = {
- Tree: Tree,
- compare: compare
-};
-
-
-/***/ }),
-/* 3 */
-/***/ (function(module, exports, __nested_webpack_require_31214__) {
-
-"use strict";
-
-
-/* Containers are expected to support the following methods:
- jsBegin() - returns the very first node
- jsEnd() - returns the node beyond the last one
- next(node) - returns the next node
- prev(node) - returns the previous node
- valuePolicy - an instance of KeyOnlyPolicy, or KeyValuePolicy */
-/**
- * ES6-style forward iterator.
- *
- * @example
- * let m = new TreeMap();
- * ...
- * for (let [key, value] of m) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- * // iterate values
- * for (let value of m.values()) {
- * console.log(`value: ${value}`);
- * }
- */
-class JsIterator {
- /**
- * @param {*} container
- */
- constructor(container, valuePolicy = container.valuePolicy) {
- /**
- * @private
- * Internal reference to a container
- */
- this.container = container;
- /**
- * @private
- * valuePolicy implements what members of the node will be returned: key, value, or key and value
- */
- this.valuePolicy = valuePolicy;
- /**
- * @private
- * current node
- */
- this.node = container.jsBegin();
- }
-
- /**
- * As documented in ES6 iteration protocol. It can be used for manual iteration.
- * Iterators are documented here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
- *
- * @example
- * let m = new TreeMap();
- * ...
- * let jsIt = m.entries();
- * while (true) {
- * let res = it.next();
- * if (res.done) {
- * break;
- * }
- * console.log(`key: ${res.value[0]}, value: ${res.value[1]`});
- * }
- */
- next() {
- let res = {};
- res.done = (this.node === this.container.jsEnd());
- if (!res.done) {
- res.value = this.valuePolicy.fetch(this.node);
- this.node = this.container.next(this.node);
- }
- return res;
- }
-
- /**
- * Support for ES6 for-of loops.
- * @returns {JsIterator}
- */
- [Symbol.iterator]() {
- return this;
- }
-
- /**
- * A reverse iterator for the same container.
- * @returns {JsReverseIterator}
- * @example
- * let m = new TreeMap();
- * ...
- * // iterate all key-value pairs in reverse order
- * for (let [key, value] of m.backwards()) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- backwards() {
- // eslint-disable-next-line no-use-before-define
- return new JsReverseIterator(this.container, this.valuePolicy);
- }
-}
-
-/* Containers are expected to support the following methods:
- jsRbegin() - returns the very first node in reverse order (e.g. the very last node)
- jsrEnd() - returns the node beyond the last one in reverse order (e.g. the node before the first one)
- next(node) - returns the next node
- prev(node) - returns the previous node
- valuePolicy - an instance of KeyOnlyPolicy, or KeyValuePolicy */
-/**
- * ES6-style backward iterator
- * @example
- * let m = new TreeMap();
- * ...
- * // iterate all key-value pairs in reverse order
- * for (let [key, value] of m.backwards()) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- * // iterate keys in reverse order
- * for (let key of m.keys().backwards()) {
- * console.log(`key: ${key}`);
- * }
- */
-class JsReverseIterator {
- /**
- * @param {*} container
- */
- constructor(container, valuePolicy = container.valuePolicy) {
- /**
- * @private
- * Internal reference to a container
- */
- this.container = container;
- /**
- * @private
- * valuePolicy implements what members of the node will be returned: key, value, or key and value
- */
- this.valuePolicy = valuePolicy;
- /**
- * @private
- * current node
- */
- this.node = container.jsRbegin();
- }
-
- /**
- * As documented in ES6 iteration protocol. It can be used for manual iteration.
- * Iterators are documented here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
- *
- * @example
- * let m = new TreeMap();
- * ...
- * let jsIt = m.entries().backwards();
- * while (true) {
- * let res = it.next();
- * if (res.done) {
- * break;
- * }
- * console.log(`key: ${res.value[0]}, value: ${res.value[1]`});
- * }
- */
- next() {
- let res = {};
- res.done = (this.node === this.container.jsRend());
- if (!res.done) {
- res.value = this.valuePolicy.fetch(this.node);
- this.node = this.container.prev(this.node);
- }
- return res;
- }
-
- /**
- * Support for ES6 for-of loops.
- * @returns {JsReverseIterator}
- */
- [Symbol.iterator]() {
- return this;
- }
-
- /**
- * A forward iterator for the same container
- * @returns {JsIterator}
- * @example
- * let m = new TreeMap();
- * ...
- * // iterate all key-value pairs in direct order
- * for (let [key, value] of m.backwards().backwards()) {
- * console.log(`key: ${key}, value: ${value}`);
- */
- backwards() {
- return new JsIterator(this.container, this.valuePolicy);
- }
-}
-
-module.exports = {
- JsIterator: JsIterator,
- JsReverseIterator: JsReverseIterator
-};
-
-/***/ }),
-/* 4 */
-/***/ (function(module, exports, __nested_webpack_require_36806__) {
-
-"use strict";
-
-/**
- * Base class for STL-like iterators. It references a node (or index) and a container.
- * Navigation is achieved by calling container's prev() and next() methods.
- */
-class BaseIterator {
- /**
- * @param {*} node - current node
- * @param {*} container - container
- */
- constructor(node, container) {
- /**
- * @private
- * __n - internal node reference
- */
- this.__n = node;
- /**
- * @private
- * __c - internal container reference
- */
- this.__c = container;
- }
-
- /**
- * Two iterators are considered to be equal if they point to the same node of the same container
- * @param {BaseIterator} rhs - object on the 'right-hand side' of .eq. operator
- * @returns {boolean}
- */
- equals(rhs) {
- let lhsClass = this.constructor.name;
- let rhsClass = rhs.constructor.name;
- if (lhsClass !== rhsClass) {
- throw new Error(`Can't compare an instance of ${lhsClass} with an instance of ${rhsClass}`);
- }
- if (this.__c !== rhs.__c) {
- throw new Error('Iterators belong to different containers');
- }
- return this.__n === rhs.__n;
- }
-
- /**
- * @private
- * @returns current node
- */
- get node() {
- return this.__n;
- }
-
- /**
- * @private
- * @returns key of the current node
- */
- get key() {
- return this.__n.key;
- }
-
- /**
- * @private
- * @returns value of the current node
- */
- get value() {
- return this.__n.value;
- }
-
- /**
- * @private
- * @returns container that holds current node
- */
- get container() {
- return this.__c;
- }
-}
-
-/**
- * STL-like forward iterator. It's more verbose than ES6 iterators, but allows iteration over any part of the container
- *
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
-class Iterator extends BaseIterator {
- /**
- * There are 3 ways to construct an iterator:
- *
- * 1. Using a node and a container
- * 2. Copy constructor / clone
- * 3. Copy constructor / clone from ReverseIterator instance
- * @param {*} args
- *
- * @example
- * // Using a node and a container
- * let it = new Iterator(node, container);
- *
- * // Copy constructor / clone
- * let it1 = new Iterator(node, container);
- * let it2 = new Iterator(it1);
- *
- * // Copy constructor / clone from ReverseIterator instance
- * let it1 = new ReverseIterator(node, container);
- * let it2 = new Iterator(it1);
- */
- constructor(...args) {
- if (args.length === 2) {
- let [node, container] = args;
- super(node, container);
- }
- else if (args.length === 1) {
- let [obj] = args;
- let className = obj.constructor.name;
- if (className === Iterator.name) {
- super(obj.__n, obj.__c);
- }
- // eslint-disable-next-line no-use-before-define
- else if (className === ReverseIterator.name) {
- let c = obj.__c;
- super(c.next(obj.__n), c);
- }
- else {
- throw new Error(`Can't create an Iterator from ${className}`);
- }
- }
- else {
- throw new Error('Can\'t create an Iterator with provided parameters');
- }
- }
-
- /**
- * Replaces node reference with the reference of the next node in the container.
- * Can be used for manual iteration over a range of key-value pairs.
- * @example
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * let from = t.lowerBound(0);
- * let to = t.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- next() {
- /**
- * __n and __c are defined in the base class
- */
- this.__n = this.__c.next(this.__n);
- }
-
- /**
- * Replaces node reference with the reference of the previous node in the container
- * Can be used for manual reverse iteration over a range of key-value pairs.
- * @example
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * let from = t.lowerBound(0);
- * let to = t.upperBound(50);
- * let it = to;
- * while (!it.equals(from)) {
- * it.prev();
- * console.log(it.key);
- * }
- */
- prev() {
- this.__n = this.__c.prev(this.__n);
- }
-}
-
-/**
- * STL-like backward iterator. Can be used to traverse container or a range in the reverse order.
- * It's more verbose than ES6 iterators, but allows iteration over any part of the container
- *
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
-class ReverseIterator extends BaseIterator {
- /**
- * There are 3 ways to construct a reverse iterator:
- *
- * 1. Using a node and a container
- * 2. Copy constructor / clone
- * 3. Copy constructor / clone from forward Iterator instance
- * @param {*} args
- *
- * @example
- * // Using a node and a container
- * let it = new ReverseIterator(node, container);
- *
- * // Copy constructor / clone
- * let it1 = new ReverseIterator(node, container);
- * let it2 = new ReverseIterator(it1);
- *
- * // Copy constructor / clone from forward Iterator instance
- * let it1 = new Iterator(node, container);
- * let it2 = new ReverseIterator(it1);
- */
- constructor(...args) {
- if (args.length === 2) {
- let [node, container] = args;
- super(node, container);
- }
- else if (args.length === 1) {
- let [obj] = args;
- let className = obj.constructor.name;
- if (className === ReverseIterator.name) {
- super(obj.__n, obj.__c);
- }
- else if (className === Iterator.name) {
- let c = obj.__c;
- super(c.prev(obj.__n), c);
- }
- else {
- throw new Error(`Can't create an ReverseIterator from ${className}`);
- }
- }
- else {
- throw new Error('Can\'t create a Reverse Iterator with provided parameters');
- }
- }
-
- /**
- * Replaces node reference with the reference of the previous node in the container, because it works in reverse order
- * Can be used for manual reverse iteration over a range of key-value pairs.
- * @example
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * let from = new ReverseIterator(t.upperBound(50));
- * let to = new ReverseIterator(t.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- next() {
- /**
- * __n and __c are defined in the base class
- */
- this.__n = this.__c.prev(this.__n);
- }
-
- /**
- * Replaces node reference with the reference of the next node in the container, because it works in reverse order
- * Can be used for manual forward iteration over a range of key-value pairs.
- * @example
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * let from = new ReverseIterator(t.upperBound(50));
- * let to = new ReverseIterator(t.lowerBound(0));
- * let it = to;
- * while (!it.equals(from)) {
- * it.prev();
- * console.log(it.key);
- * }
- */
- prev() {
- this.__n = this.__c.next(this.__n);
- }
-}
-
-module.exports = {
- Iterator: Iterator,
- ReverseIterator: ReverseIterator
-};
-
-/***/ }),
-/* 5 */
-/***/ (function(module, exports, __nested_webpack_require_45031__) {
-
-module.exports = __nested_webpack_require_45031__(6);
-
-
-/***/ }),
-/* 6 */
-/***/ (function(module, exports, __nested_webpack_require_45149__) {
-
-/* This is an entry point to the library.
- It collects all public classes and re-exports them */
-/**@private */
-const {TreeMap} = __nested_webpack_require_45149__(7);
-/**@private */
-const {TreeMultiMap} = __nested_webpack_require_45149__(9);
-/**@private */
-const {TreeSet} = __nested_webpack_require_45149__(10);
-/**@private */
-const {TreeMultiSet} = __nested_webpack_require_45149__(11);
-/**@private */
-const {Iterator, ReverseIterator} = __nested_webpack_require_45149__(4);
-/**@private */
-const {JsIterator, JsReverseIterator} = __nested_webpack_require_45149__(3);
-
-module.exports = {
- Iterator: Iterator,
- ReverseIterator: ReverseIterator,
- JsIterator: JsIterator,
- JsReverseIterator: JsReverseIterator,
- TreeMap: TreeMap,
- TreeMultiMap: TreeMultiMap,
- TreeSet: TreeSet,
- TreeMultiSet: TreeMultiSet,
-};
-
-/***/ }),
-/* 7 */
-/***/ (function(module, exports, __nested_webpack_require_46005__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_46005__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyValuePolicy} = __nested_webpack_require_46005__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_46005__(0);
-
-/**
- * TreeMap is an associative container that stores elements formed
- * by a combination of a key value and a mapped value, following a specific order.
- *
- * In a TreeMap, the key values are generally used to sort and uniquely identify
- * the elements, while the mapped values store the content associated to this key.
- * The types of key and mapped value may differ.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced by their key
- * and not by their absolute position in the container.
- * * **Ordered** - The elements in the container follow a strict order at all times.
- * All inserted elements are given a position in this order.
- * * **Map** - Each element associates a key to a mapped value. Keys are meant
- * to identify the elements whose main content is the mapped value.
- * * **Unique keys** - No two elements in the container can have equivalent keys.
- *
- * @example
- * let map = new TreeMap();
- * // add few values
- * map.set(1, 'a');
- * map.set(2, 'b');
- * // find a value by key
- * let v = map.get(1); // << 'a'
- * // print all key-value pairs
- * for (let [key, value] of map) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
-class TreeMap {
- /*======================================================
- * Methods of ES6 Map
- *======================================================*/
-
- /**
- * Creates an empty, or a pre-initialized map.
- * @param {*} [iterable] Another iterable object whose key-value pairs are added into the newly created map.
- * @example
- * // Create an empty map
- * let map1 = new TreeMap();
- * // Create and initialize map
- * let map2 = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- */
- constructor(iterable) {
- /** Internal tree */
- this.__t = new Tree();
- this.__t.valuePolicy = new KeyValuePolicy();
- if ((iterable !== undefined)
- && (iterable !== null)) {
- if (iterable[Symbol.iterator] !== undefined) {
- // copy contents
- for (let [k, v] of iterable) {
- this.set(k, v);
- }
- }
- else {
- throw new Error('TreeMap constructor accepts only iterable objects');
- }
- }
- }
-
- /**
- * String tag of this class
- * @returns {String}
- * @example
- * Object.prototype.toString.call(new TreeMap()); // "[object TreeMap]"
- */
- get [Symbol.toStringTag]() {
- return 'TreeMap';
- }
-
- /**
- * Allows to create programmatically an instance of the same class
- * @returns constructor object for this class.
- * @example
- * let map = new TreeMap();
- * let constrFunc = Object.getPrototypeOf(map).constructor[Symbol.species];
- * let map2 = new constrFunc();
- */
- static get [Symbol.species]() {
- return TreeMap;
- }
-
- /**
- * Removes all key-value pairs.
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * map.clear();
- * console.log(map.size); // 0
- */
- clear() {
- this.__t.clear();
- }
-
- /**
- * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * map.delete(2);
- * console.log(map.toString()); // {1:A,3:C}
- */
- delete(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- this.__t.erase(it.node);
- }
- }
-
- /**
- * Forward ES6 iterator for all key-value pairs in ascending order of the keys.
- * @returns {JsIterator}
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let [key,value] of map.entries()) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- entries() {
- return this.__t.entries();
- }
-
- /**
- * Iterates all key-value pairs using a callback in ascending order of the keys.
- * Note that ES6 specifies the order of key value parameters in the callback differently from for-of loop.
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * map.forEach(function(value, key, container) {
- * console.log(`key: ${key}, value: ${value}`);
- * });
- */
- forEach(callback) {
- for (let [k, v] of this.__t) {
- callback(v, k, this);
- }
- }
-
- /**
- * Finds value associated with the specified key. If specified key does not exist then undefined is returned.
- * @returns {*}
- * @param {*} key a value of any type that can be compared with a key
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let v = map.get(3); // 'C'
- * * let v = map.get(4); // returns undefined
- */
- get(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- return it.value;
- }
- else {
- return undefined;
- }
- }
-
- /**
- * A boolean indicator whether map contains a key-value pair with the specified key
- * @returns {Boolean}
- * @param {*} key a value of any type that can be compared with a key
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let b = map.get(3); // true
- */
- has(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- return true;
- }
- else {
- return false;
- }
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order of the keys.
- * @returns {JsIterator}
- * @example
- * // iterate all keys
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let k of map.keys()) {
- * console.log(k); // 1, 2, 3
- * }
- * // iterate all keys in reverse order
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let k of map.keys().backward()) {
- * console.log(k); // 3, 2, 1
- * }
- */
- keys() {
- return this.__t.keys();
- }
-
- /**
- * Adds or updates key-value pair to the map.
- * @param {*} key
- * @param {*} value
- * @example
- * let map = new TreeMap();
- * map.set(1, 'A');
- */
- set(key, value) {
- let n = new TreeNode();
- n.key = key;
- n.value = value;
- this.__t.insertOrReplace(n);
- }
-
- /**
- * Number of key-value pairs in the map.
- * @returns {Number}
- */
- get size() {
- return this.__t.size();
- }
-
- /**
- * Forward ES6 iterator for all values in ascending order of the keys.
- * @returns {JsITerator}
- * @example
- * // iterate all values
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let v of map.values()) {
- * console.log(v); // 'A', 'B', 'C'
- * }
- * // iterate all values in reverse order
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let v of map.values().backward()) {
- * console.log(v); // 'C', 'B', 'A'
- * }
- */
- values() {
- return this.__t.values();
- }
-
- /**
- * Forward ES6 iterator for all key-value pairs in ascending order of the keys. The same as entries() method
- * @returns {JsIterator}
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let [key,value] of map) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- [Symbol.iterator]() {
- return this.__t[Symbol.iterator]();
- }
-
- /*======================================================
- * More methods
- *======================================================*/
- /**
- * ES6 reverse iterator for all key-value pairs in descending order of the keys.
- * @returns {JsReverseIterator}
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let [key,value] of map.backwards()) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- backward() {
- return this.__t.backward();
- }
-
- /**
- * Sets custom comparison function if key values are not of primitive types.
- * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
- * +1 if the value of rhs is greater than lhs
- * -1 if the value of rhs is less than lhs
- * 0 if values are the same
- */
- set compareFunc(func) {
- this.clear();
- this.__t.compare = func;
- }
-
- /*======================================================
- * STL-like methods
- *======================================================*/
-
- /**
- * Forward iterator to the first element
- * @returns {Iterator}
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- begin() {
- return this.__t.begin();
- }
-
- /**
- * Forward iterator to the element following the last element
- * @returns {Iterator}
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- end() {
- return this.__t.end();
- }
-
- /**
- * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let m = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * ...
- * let it = m.find(1);
- * if (!it.equals(m.end())) {
- * console.log(`key: ${it.key}, value: ${it.value}`); // 1, 'A'
- * }
- */
- find(key) {
- return this.__t.find(key);
- }
-
- /**
- * Adds key-value pair if such key does not exist in the map
- * @param {*} key
- * @param {*} value
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let m = new TreeMap();
- * let res = m.insertUnique(1, 'A');
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.value}`); // prints A
- * }
- * res = m.insertUnique(1, 'B') // this step has no effect on the map
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // not executed
- * }
- */
- insertUnique(key, value) {
- let n = new TreeNode();
- n.key = key;
- n.value = value;
- return this.__t.insertUnique(n);
- }
-
- /**
- * Adds key-value pair if such key does not exist in the map. Replaces value if such key exists
- * @param {*} key
- * @param {*} value
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let m = new TreeMap();
- * let res = m.insertOrReplace(1, 'A');
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.value}`); // prints A
- * }
- * res = m.insertOrReplace(1, 'B') // replaces value on the existing node
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // prints B
- * }
- */
- insertOrReplace(key, value) {
- let n = new TreeNode();
- n.key = key;
- n.value = value;
- return this.__t.insertOrReplace(n);
- }
-
- /**
- * Removes key-value pair for the specified iterator.
- * @param {Iterator} iterator
- * @example
- * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let it = map.find(2);
- * it.prev();
- * map.erase(it); // removes a node with key 1
- * console.log(map.toString()); // {2:B,3:C}
- */
- erase(iterator) {
- this.__t.erase(iterator.node);
- }
-
- /**
- * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = m.lowerBound(0);
- * let to = m.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(m.upperBound(50));
- * let to = new ReverseIterator(m.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- lowerBound(key) {
- return this.__t.lowerBound(key);
- }
-
- /**
- * Reverse iterator to the last element.
- * @returns {ReverseIterator}
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- rbegin() {
- return this.__t.rbegin();
- }
-
- /**
- * Reverse iterator pointing to before the first element.
- * @returns {ReverseIterator}
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- rend() {
- return this.__t.rend();
- }
-
- /**
- * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = m.lowerBound(0);
- * let to = m.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let m = new TreeMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(m.upperBound(50));
- * let to = new ReverseIterator(m.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- upperBound(key) {
- return this.__t.upperBound(key);
- }
-
- /**
- * @returns first key/value pair of the container, or undefined if container is empty
- * @example
- * let m = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let first = m.first();
- * if (first) {
- * let key = first[0]; // 1
- * let value = first[1]; // 'A'
- * }
- */
- first() {
- return this.__t.first();
- }
-
- /**
- * @returns last key/value pair of the container, or undefined if container is empty
- * @example
- * let m = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let last = m.last();
- * if (last) {
- * let key = last[0]; // 3
- * let value = last[1]; // 'C'
- * }
- */
- last() {
- return this.__t.last();
- }
-
- /**
- * Serializes contents of the map in the form {key1:value1,key2:value2,...}
- * @returns {String}
- */
- toString() {
- return this.__t.toString();
- }
-}
-
-module.exports = {
- TreeMap: TreeMap,
-};
-
-/***/ }),
-/* 8 */
-/***/ (function(module, exports) {
-
-/**
- * An instance of this class reports whether insert operation was successful.
- * if a node was added, or an existing one replaced then an iterator is provided. Otherwise the value of iterator is undefined
- */
-class InsertionResult {
- /**
- * Default constructor
- * @param {Boolean} wasAdded
- * @param {Boolean} wasReplaced
- * @param {Iterator} iterator only provided if the node was added, or replaced
- */
- constructor(wasAdded, wasReplaced, iterator) {
- /**
- * Boolean flag indicating whether an element was added
- */
- this.wasAdded = wasAdded;
- /**
- * Boolean flag indicating whether an existing node was updated
- */
- this.wasReplaced = wasReplaced;
- /**
- * {Iterator} instance pointing to the newly added node
- */
- this.iterator = iterator;
- }
-}
-
-module.exports = {
- InsertionResult: InsertionResult
-};
-
-/***/ }),
-/* 9 */
-/***/ (function(module, exports, __nested_webpack_require_63461__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_63461__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyValuePolicy} = __nested_webpack_require_63461__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_63461__(0);
-
-/**
- * TreeMultiMap is an associative container that stores elements formed by
- * a combination of a key value and a mapped value, following a specific order,
- * and where multiple elements can have equivalent keys.
- *
- * In a TreeMultiMap, the key values are generally used to sort and uniquely
- * identify the elements, while the mapped values store the content
- * associated to this key. The types of key and mapped value may differ.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced
- * by their key and not by their absolute position in the container.
- * * **Ordered** - The elements in the container follow a strict order
- * at all times. All inserted elements are given a position in this order.
- * * **Map** - Each element associates a key to a mapped value. Keys are meant
- * to identify the elements whose main content is the mapped value.
- * * **Multiple equivalent keys** - Multiple elements in the container
- * can have equivalent keys.
- *
- * @example
- * let map = new TreeMultiMap();
- * // add few values
- * map.set(1, 'a');
- * map.set(2, 'b');
- * map.set(2, 'c');
- * // find a value by key
- * let v = map.get(1); // << 'a'
- * find all values for a given key
- * // print all key-value pairs
- * let from = map.lowerBound(2);
- * let to = map.upperBound(2);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
-class TreeMultiMap {
- /*======================================================
- * Methods of ES6 Map
- *======================================================*/
-
- /**
- * Creates an empty, or a pre-initialized map.
- * @param {*} [iterable] Another iterable object whose key-value pairs are added into the newly created map.
- * @example
- * // Create an empty map
- * let map1 = new TreeMultiMap();
- * // Create and initialize map
- * let map2 = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- */
- constructor(iterable) {
- /** Internal tree */
- this.__t = new Tree();
- this.__t.valuePolicy = new KeyValuePolicy();
- if ((iterable !== undefined)
- && (iterable !== null)) {
- if (iterable[Symbol.iterator] !== undefined) {
- // copy contents
- for (let [k, v] of iterable) {
- this.set(k, v);
- }
- }
- else {
- throw new Error('TreeMultiMap constructor accepts only iterable objects');
- }
- }
- }
-
- /**
- * String tag of this class
- * @returns {String}
- * @example
- * Object.prototype.toString.call(new TreeMultiMap()); // "[object TreeMultiMap]"
- */
- get [Symbol.toStringTag]() {
- return 'TreeMultiMap';
- }
-
- /**
- * Allows to create programmatically an instance of the same class
- * @returns constructor object for this class.
- * @example
- * let map = new TreeMultiMap();
- * let constrFunc = Object.getPrototypeOf(map).constructor[Symbol.species];
- * let map2 = new constrFunc();
- */
- static get [Symbol.species]() {
- return TreeMultiMap;
- }
-
- /**
- * Removes all key-value pairs.
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * map.clear();
- * console.log(map.size); // 0
- */
- clear() {
- this.__t.clear();
- }
-
- /**
- * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * map.delete(2);
- * console.log(map.toString()); // {1:A,3:C}
- */
- delete(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- this.__t.erase(it.node);
- }
- }
-
- /**
- * Forward ES6 iterator for all key-value pairs in ascending order of the keys.
- * @returns {JsIterator}
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let [key,value] of map.entries()) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- entries() {
- return this.__t.entries();
- }
-
- /**
- * Iterates all key-value pairs using a callback in ascending order of the keys.
- * Note that ES6 specifies the order of key value parameters in the callback differently from for-of loop.
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * map.forEach(function(value, key, container) {
- * console.log(`key: ${key}, value: ${value}`);
- * });
- */
- forEach(callback) {
- for (let [k, v] of this.__t) {
- callback(v, k, this);
- }
- }
-
- /**
- * Finds value associated with the specified key. If specified key does not exist then undefined is returned.
- * @returns {*}
- * @param {*} key a value of any type that can be compared with a key
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let v = map.get(3); // 'C'
- * * let v = map.get(4); // returns undefined
- */
- get(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- return it.value;
- }
- else {
- return undefined;
- }
- }
-
- /**
- * A boolean indicator whether map contains a key-value pair with the specified key
- * @returns {Boolean}
- * @param {*} key a value of any type that can be compared with a key
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let b = map.get(3); // true
- */
- has(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- return true;
- }
- else {
- return false;
- }
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order of the keys.
- * @returns {JsIterator}
- * @example
- * // iterate all keys
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let k of map.keys()) {
- * console.log(k); // 1, 2, 3
- * }
- * // iterate all keys in reverse order
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let k of map.keys().backward()) {
- * console.log(k); // 3, 2, 1
- * }
- */
- keys() {
- return this.__t.keys();
- }
-
- /**
- * Adds a key-value pair to the map. Multiple key-value pairs with the same key are allowed in TreeMultiMap.
- * @param {*} key
- * @param {*} value
- * @example
- * let map = new TreeMultiMap();
- * map.set(1, 'A');
- * map.set(1, 'B');
- * map.set(2, 'C');
- * for (let k of map.values()) {
- * console.log(k); // A, B, C
- * }
- */
- set(key, value) {
- let n = new TreeNode();
- n.key = key;
- n.value = value;
- this.__t.insertMulti(n);
- }
-
- /**
- * Number of key-value pairs in the map.
- * @returns {Number}
- */
- get size() {
- return this.__t.size();
- }
-
- /**
- * Forward ES6 iterator for all values in ascending order of the keys.
- * @returns {JsITerator}
- * @example
- * // iterate all values
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let v of map.values()) {
- * console.log(v); // 'A', 'B', 'C'
- * }
- * // iterate all values in reverse order
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let v of map.values().backward()) {
- * console.log(v); // 'C', 'B', 'A'
- * }
- */
- values() {
- return this.__t.values();
- }
-
- /**
- * Forward ES6 iterator for all key-value pairs in ascending order of the keys. The same as entries() method
- * @returns {JsIterator}
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let [key,value] of map) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- [Symbol.iterator]() {
- return this.__t[Symbol.iterator]();
- }
-
- /*======================================================
- * More methods
- *======================================================*/
- /**
- * ES6 reverse iterator for all key-value pairs in descending order of the keys.
- * @returns {JsReverseIterator}
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * for (let [key,value] of map.backwards()) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- backward() {
- return this.__t.backward();
- }
-
- /**
- * Sets custom comparison function if key values are not of primitive types.
- * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
- * +1 if the value of rhs is greater than lhs
- * -1 if the value of rhs is less than lhs
- * 0 if values are the same
- */
- set compareFunc(func) {
- this.clear();
- this.__t.compare = func;
- }
-
- /*======================================================
- * STL-like methods
- *======================================================*/
-
- /**
- * Forward iterator to the first element
- * @returns {Iterator}
- * @example
- * let m = new TreeMultiMap();
- * ...
- * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- begin() {
- return this.__t.begin();
- }
-
- /**
- * Forward iterator to the element following the last element
- * @returns {Iterator}
- * @example
- * let m = new TreeMultiMap();
- * ...
- * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- end() {
- return this.__t.end();
- }
-
- /**
- * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let m = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * ...
- * let it = m.find(1);
- * if (!it.equals(m.end())) {
- * console.log(`key: ${it.key}, value: ${it.value}`); // 1, 'A'
- * }
- */
- find(key) {
- return this.__t.find(key);
- }
-
- /**
- * Adds key-value pair if such key does not exist in the map
- * @param {*} key
- * @param {*} value
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let m = new TreeMultiMap();
- * let res = m.insertUnique(1, 'A');
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.value}`); // prints A
- * }
- * res = m.insertUnique(1, 'B') // this step has no effect on the map
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // not executed
- * }
- */
- insertUnique(key, value) {
- let n = new TreeNode();
- n.key = key;
- n.value = value;
- return this.__t.insertUnique(n);
- }
-
- /**
- * Adds key-value pair if such key does not exist in the map. Replaces value if such key exists
- * @param {*} key
- * @param {*} value
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let m = new TreeMultiMap();
- * let res = m.insertOrReplace(1, 'A');
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.value}`); // prints A
- * }
- * res = m.insertOrReplace(1, 'B') // replaces value on the existing node
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // prints B
- * }
- */
- insertOrReplace(key, value) {
- let n = new TreeNode();
- n.key = key;
- n.value = value;
- return this.__t.insertOrReplace(n);
- }
-
- /**
- * Adds key-value pair. If such key already exists in the map then adds another node with the same key and a new value.
- * @param {*} key
- * @param {*} value
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let m = new TreeMultiMap();
- * let res = m.insertMulti(1, 'A');
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.value}`); // prints A
- * }
- * res = m.insertMulti(1, 'B') // adds a new node
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.value}`); // prints B
- * it.prev();
- * console.log(`Previously inserted ${res.iterator.value}`); // prints A
- * }
- */
- insertMulti(key, value) {
- let n = new TreeNode();
- n.key = key;
- n.value = value;
- return this.__t.insertMulti(n);
- }
-
- /**
- * Removes key-value pair for the specified iterator.
- * @param {Iterator} iterator
- * @example
- * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let it = map.find(2);
- * it.prev();
- * map.erase(it); // removes a node with key 1
- * console.log(map.toString()); // {2:B,3:C}
- */
- erase(iterator) {
- this.__t.erase(iterator.node);
- }
-
- /**
- * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let m = new TreeMultiMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = m.lowerBound(0);
- * let to = m.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let m = new TreeMultiMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(m.upperBound(50));
- * let to = new ReverseIterator(m.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- lowerBound(key) {
- return this.__t.lowerBound(key);
- }
-
- /**
- * Reverse iterator to the last element.
- * @returns {ReverseIterator}
- * @example
- * let m = new TreeMultiMap();
- * ...
- * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- rbegin() {
- return this.__t.rbegin();
- }
-
- /**
- * Reverse iterator pointing to before the first element.
- * @returns {ReverseIterator}
- * @example
- * let m = new TreeMultiMap();
- * ...
- * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
- * console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
- rend() {
- return this.__t.rend();
- }
-
- /**
- * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let m = new TreeMultiMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = m.lowerBound(0);
- * let to = m.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let m = new TreeMultiMap();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(m.upperBound(50));
- * let to = new ReverseIterator(m.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- upperBound(key) {
- return this.__t.upperBound(key);
- }
-
- /**
- * @returns first key/value pair of the container, or undefined if container is empty
- * @example
- * let m = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let first = m.first();
- * if (first) {
- * let key = first[0]; // 1
- * let value = first[1]; // 'A'
- * }
- */
- first() {
- return this.__t.first();
- }
-
- /**
- * @returns last key/value pair of the container, or undefined if container is empty
- * @example
- * let m = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
- * let last = m.last();
- * if (last) {
- * let key = last[0]; // 3
- * let value = last[1]; // 'C'
- * }
- */
- last() {
- return this.__t.last();
- }
-
- /**
- * Serializes contents of the map in the form {key1:value1,key2:value2,...}
- * @returns {String}
- */
- toString() {
- return this.__t.toString();
- }
-}
-
-module.exports = {
- TreeMultiMap: TreeMultiMap,
-};
-
-/***/ }),
-/* 10 */
-/***/ (function(module, exports, __nested_webpack_require_81431__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_81431__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyOnlyPolicy} = __nested_webpack_require_81431__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_81431__(0);
-
-/**
- * TreeSet is a container that stores unique elements following a specific order.
- *
- * In a TreeSet, the value of an element also identifies it (the value is itself the key),
- * and each value must be unique. The value of the elements in a TreeSet cannot be modified
- * once in the container (the elements are immutable), but they can be inserted or removed
- * from the container.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced by their key and
- * not by their absolute position in the container.</li>
- * * **Ordered** - The elements in the container follow a strict order at all times.
- * All inserted elements are given a position in this order.</li>
- * * **Set** - The value of an element is also the key used to identify it.</li>
- * * **Unique keys** - No two elements in the container can have equivalent keys.</li>
- *
- *
- * @example
- * let set = new TreeSet();
- * // add few values
- * set.add(1);
- * set.add(2);
- * // check whether key exists
- * let flag = set.has(1); // << true
- * // print all keys
- * for (let key of set) {
- * console.log(`key: ${key}`);
- * }
- */
-class TreeSet {
- /*======================================================
- * Methods of ES6 Set
- *======================================================*/
-
- /**
- * Creates an empty, or a pre-initialized set.
- * @param {*} [iterable] Another iterable object whose values are added into the newly created set.
- * @example
- * // Create an empty set
- * let set = new TreeSet();
- * // Create and initialize set
- * let set2 = new TreeSet([1, 2, 3]);
- */
- constructor(iterable) {
- /** Internal tree */
- this.__t = new Tree();
- this.__t.valuePolicy = new KeyOnlyPolicy();
- if ((iterable !== undefined)
- && (iterable !== null)) {
- if (iterable[Symbol.iterator] !== undefined) {
- // copy contents
- for (let k of iterable) {
- this.add(k);
- }
- }
- else {
- throw new Error('TreeSet constructor accepts only iterable objects');
- }
- }
- }
-
- /**
- * String tag of this class
- * @returns {String}
- * @example
- * Object.prototype.toString.call(new TreeSet()); // "[object TreeSet]"
- */
- get [Symbol.toStringTag]() {
- return 'TreeSet';
- }
-
- /**
- * Allows to create programmatically an instance of the same class
- * @returns constructor object for this class.
- * @example
- * let set = new TreeSet();
- * let constrFunc = Object.getPrototypeOf(set).constructor[Symbol.species];
- * let set2 = new constrFunc();
- */
- static get [Symbol.species]() {
- return TreeSet;
- }
-
- /**
- * Removes all key-value pairs.
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * set.clear();
- * console.log(set.size); // 0
- */
- clear() {
- this.__t.clear();
- }
-
- /**
- * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * set.delete(2);
- * console.log(set.toString()); // {1,3}
- */
- delete(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- this.__t.erase(it.node);
- }
- }
-
- /**
- * Forward ES6 iterator for all values in ascending order.
- * @returns {JsIterator}
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * for (let key of set.entries()) {
- * console.log(`key: ${key}`);
- * }
- */
- entries() {
- return this.__t.entries();
- }
-
- /**
- * Iterates all values using a callback in ascending order.
- * Note that ES6 specifies the order of key parameters in the callback differently from for-of loop.
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * set.forEach(function(value, key, container) {
- * // value is the same as key
- * console.log(`key: ${key}, value: ${value}`);
- * });
- */
- forEach(callback) {
- for (let k of this.__t) {
- callback(k, k, this);
- }
- }
-
- /**
- * A boolean indicator whether set contains the specified key.
- * @returns {Boolean}
- * @param {*} key a value of any type that can be compared with a key
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * let b = set.get(3); // true
- * b = set.get(4); // false
- */
- has(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- return true;
- }
- else {
- return false;
- }
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order.
- * @returns {JsIterator}
- * @example
- * // iterate all keys
- * let set = new TreeSet([1, 2, 3]);
- * for (let k of set.keys()) {
- * console.log(k); // 1, 2, 3
- * }
- * // iterate all keys in reverse order
- * let set = new TreeSet([1, 2, 3]);
- * for (let k of set.keys().backward()) {
- * console.log(k); // 3, 2, 1
- * }
- */
- keys() {
- return this.__t.keys();
- }
-
- /**
- * Adds a key to the set, unless the key already exists.
- * @param {*} key
- * @example
- * let set = new TreeSet();
- * set.add(1);
- */
- add(key) {
- let n = new TreeNode();
- n.key = key;
- this.__t.insertUnique(n);
- }
-
- /**
- * Number of keys in the set.
- * @returns {Number}
- */
- get size() {
- return this.__t.size();
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order. It is the same as keys() method
- * @returns {JsITerator}
- * @example
- * // iterate all values
- * let set = new TreeSet([1, 2, 3]);
- * for (let v of set.values()) {
- * console.log(v); // '1', '2', '3'
- * }
- * // iterate all values in reverse order
- * let set = new TreeSet([1, 2, 3]);
- * for (let v of set.values().backward()) {
- * console.log(v); // '3', '2', '1'
- * }
- */
- values() {
- return this.__t.keys();
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order. The same as entries() method
- * @returns {JsIterator}
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * for (let key of set) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- [Symbol.iterator]() {
- return this.__t[Symbol.iterator]();
- }
-
- /*======================================================
- * More methods
- *======================================================*/
- /**
- * ES6 reverse iterator for all keys in descending order.
- * @returns {JsReverseIterator}
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * for (let key of set.backwards()) {
- * console.log(`key: ${key}`);
- * }
- */
- backward() {
- return this.__t.backward();
- }
-
- /**
- * Sets custom comparison function if key values are not of primitive types.
- * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
- * +1 if the value of rhs is greater than lhs
- * -1 if the value of rhs is less than lhs
- * 0 if values are the same
- */
- set compareFunc(func) {
- this.clear();
- this.__t.compare = func;
- }
-
- /*======================================================
- * STL-like methods
- *======================================================*/
-
- /**
- * Forward iterator to the first element
- * @returns {Iterator}
- * @example
- * let set = new TreeSet();
- * ...
- * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- begin() {
- return this.__t.begin();
- }
-
- /**
- * Forward iterator to the element following the last element
- * @returns {Iterator}
- * @example
- * let set = new TreeSet();
- * ...
- * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- end() {
- return this.__t.end();
- }
-
- /**
- * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * ...
- * let it = set.find(1);
- * if (!it.equals(set.end())) {
- * console.log(`Found key: ${it.key}`); // 1
- * }
- */
- find(key) {
- return this.__t.find(key);
- }
-
- /**
- * Adds a key if it doesn't exist
- * @param {*} key
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let set = new TreeSet();
- * let res = set.insertUnique(1);
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // prints 1
- * }
- * res = set.insertUnique(1); // this step has no effect on the set
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // not executed
- * }
- */
- insertUnique(key) {
- let n = new TreeNode();
- n.key = key;
- return this.__t.insertUnique(n);
- }
-
- /**
- * Adds key-value pair if such key does not exist in the map. Replaces value if such key exists
- * @param {*} key
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let set = new TreeSet();
- * let res = set.insertOrReplace(1);
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // prints 1
- * }
- * res = set.insertOrReplace(1) // returns iterator to the previously added node
- * if (res.wasInserted) {
- * console.log(`Inserted ${res.iterator.key}`); // prints 1
- * }
- */
- insertOrReplace(key) {
- let n = new TreeNode();
- n.key = key;
- return this.__t.insertOrReplace(n);
- }
-
- /**
- * Removes value for the specified iterator.
- * @param {Iterator} iterator
- * @example
- * let set = new TreeSet([1,2,3]);
- * let it = set.find(2);
- * it.prev();
- * set.erase(it); // removes a node with key 1
- * console.log(set.toString()); // {2,3}
- */
- erase(iterator) {
- this.__t.erase(iterator.node);
- }
-
- /**
- * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let set = new TreeSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = set.lowerBound(0);
- * let to = set.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let set = new TreeSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(set.upperBound(50));
- * let to = new ReverseIterator(set.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- lowerBound(key) {
- return this.__t.lowerBound(key);
- }
-
- /**
- * Reverse iterator to the last element.
- * @returns {ReverseIterator}
- * @example
- * let set = new TreeSet();
- * ...
- * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- rbegin() {
- return this.__t.rbegin();
- }
-
- /**
- * Reverse iterator pointing to before the first element.
- * @returns {ReverseIterator}
- * @example
- * let set = new TreeSet();
- * ...
- * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- rend() {
- return this.__t.rend();
- }
-
- /**
- * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let set = new TreeSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = set.lowerBound(0);
- * let to = set.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let set = new TreeSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(set.upperBound(50));
- * let to = new ReverseIterator(set.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- upperBound(key) {
- return this.__t.upperBound(key);
- }
-
- /**
- * @returns first element of the container, or undefined if container is empty
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * let first = set.first(); // 1
- */
- first() {
- return this.__t.first();
- }
-
- /**
- * @returns last element of the container, or undefined if container is empty
- * @example
- * let set = new TreeSet([1, 2, 3]);
- * let last = set.last(); // 3
- */
- last() {
- return this.__t.last();
- }
-
- /**
- * Serializes contents of the set in the form {key1,key2,...}
- * @returns {String}
- */
- toString() {
- return this.__t.toString();
- }
-}
-
-module.exports = {
- TreeSet: TreeSet,
-};
-
-/***/ }),
-/* 11 */
-/***/ (function(module, exports, __nested_webpack_require_96249__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_96249__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyOnlyPolicy} = __nested_webpack_require_96249__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_96249__(0);
-
-/**
- * TreeMultiSet is a container that stores elements following a specific order,
- * and where multiple elements can have equivalent values.
- *
- * In a TreeMultiSet, the value of an element also identifies it
- * (the value is itself the key). The value of the elements in a multiset
- * cannot be modified once in the container (the elements are always immutable),
- * but they can be inserted or removed from the container.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced
- * by their key and not by their absolute position in the container.
- * * **Ordered** - The elements in the container follow a strict order
- * at all times. All inserted elements are given a position in this order.
- * * **Set** - The value of an element is also the key used to identify it.
- * * **Multiple equivalent keys** - Multiple elements in the container
- * can have equivalent keys.
- *
- * @example
- * let set = new TreeMultiSet();
- * // add few values
- * set.add(1);
- * set.add(2);
- * set.add(2);
- * // check whether key exists
- * let flag = set.has(1); // << true
- * // print all keys
- * for (let key of set) {
- * console.log(`key: ${key}`); // 1, 2, 2
- * }
- */
-class TreeMultiSet {
- /*======================================================
- * Methods of ES6 Set
- *======================================================*/
-
- /**
- * Creates an empty, or a pre-initialized set.
- * @param {*} [iterable] Another iterable object whose values are added into the newly created set.
- * @example
- * // Create an empty set
- * let set = new TreeMultiSet();
- * // Create and initialize set
- * let set2 = new TreeMultiSet([1, 2, 3]);
- */
- constructor(iterable) {
- /** Internal tree */
- this.__t = new Tree();
- this.__t.valuePolicy = new KeyOnlyPolicy();
- if ((iterable !== undefined)
- && (iterable !== null)) {
- if (iterable[Symbol.iterator] !== undefined) {
- // copy contents
- for (let k of iterable) {
- this.add(k);
- }
- }
- else {
- throw new Error('TreeMultiSet constructor accepts only iterable objects');
- }
- }
- }
-
- /**
- * String tag of this class
- * @returns {String}
- * @example
- * Object.prototype.toString.call(new TreeMultiSet()); // "[object TreeMultiSet]"
- */
- get [Symbol.toStringTag]() {
- return 'TreeMultiSet';
- }
-
- /**
- * Allows to create programmatically an instance of the same class
- * @returns constructor object for this class.
- * @example
- * let set = new TreeMultiSet();
- * let constrFunc = Object.getPrototypeOf(set).constructor[Symbol.species];
- * let set2 = new constrFunc();
- */
- static get [Symbol.species]() {
- return TreeMultiSet;
- }
-
- /**
- * Removes all key-value pairs.
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * set.clear();
- * console.log(set.size); // 0
- */
- clear() {
- this.__t.clear();
- }
-
- /**
- * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
- * @example
- * let set = new TreeMultiSet([1, 2, 2, 3]);
- * set.delete(2);
- * console.log(set.toString()); // {1,2,3}
- * set.delete(2); / remove the second copy of the key
- * console.log(set.toString()); // {1,3}
- */
- delete(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- this.__t.erase(it.node);
- }
- }
-
- /**
- * Forward ES6 iterator for all values in ascending order.
- * @returns {JsIterator}
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * for (let key of set.entries()) {
- * console.log(`key: ${key}`);
- * }
- */
- entries() {
- return this.__t.entries();
- }
-
- /**
- * Iterates all values using a callback in ascending order.
- * Note that ES6 specifies the order of key parameters in the callback differently from for-of loop.
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * set.forEach(function(value, key, container) {
- * // value is the same as key
- * console.log(`key: ${key}, value: ${value}`);
- * });
- */
- forEach(callback) {
- for (let k of this.__t) {
- callback(k, k, this);
- }
- }
-
- /**
- * A boolean indicator whether set contains the specified key.
- * @returns {Boolean}
- * @param {*} key a value of any type that can be compared with a key
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * let b = set.get(3); // true
- * b = set.get(4); // false
- */
- has(key) {
- let it = this.__t.find(key);
- if (!it.equals(this.__t.end())) {
- return true;
- }
- else {
- return false;
- }
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order.
- * @returns {JsIterator}
- * @example
- * // iterate all keys
- * let set = new TreeMultiSet([1, 2, 3]);
- * for (let k of set.keys()) {
- * console.log(k); // 1, 2, 3
- * }
- * // iterate all keys in reverse order
- * let set = new TreeMultiSet([1, 2, 3]);
- * for (let k of set.keys().backward()) {
- * console.log(k); // 3, 2, 1
- * }
- */
- keys() {
- return this.__t.keys();
- }
-
- /**
- * Adds a key to the set. If the key already exists then another entry with the same value is added.
- * @param {*} key
- * @example
- * let set = new TreeMultiSet();
- * set.add(1);
- * set.add(1);
- * set.add(2);
- * // print all keys
- * for (let key of set) {
- * console.log(`key: ${key}`); // 1, 1, 2
- * }
- */
- add(key) {
- let n = new TreeNode();
- n.key = key;
- this.__t.insertMulti(n);
- }
-
- /**
- * Number of keys in the set.
- * @returns {Number}
- */
- get size() {
- return this.__t.size();
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order. It is the same as keys() method
- * @returns {JsITerator}
- * @example
- * // iterate all values
- * let set = new TreeMultiSet([1, 2, 3]);
- * for (let v of set.values()) {
- * console.log(v); // '1', '2', '3'
- * }
- * // iterate all values in reverse order
- * let set = new TreeMultiSet([1, 2, 3]);
- * for (let v of set.values().backward()) {
- * console.log(v); // '3', '2', '1'
- * }
- */
- values() {
- return this.__t.keys();
- }
-
- /**
- * Forward ES6 iterator for all keys in ascending order. The same as entries() method
- * @returns {JsIterator}
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * for (let key of set) {
- * console.log(`key: ${key}, value: ${value}`);
- * }
- */
- [Symbol.iterator]() {
- return this.__t[Symbol.iterator]();
- }
-
- /*======================================================
- * More methods
- *======================================================*/
- /**
- * ES6 reverse iterator for all keys in descending order.
- * @returns {JsReverseIterator}
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * for (let key of set.backwards()) {
- * console.log(`key: ${key}`);
- * }
- */
- backward() {
- return this.__t.backward();
- }
-
- /**
- * Sets custom comparison function if key values are not of primitive types.
- * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
- * +1 if the value of rhs is greater than lhs
- * -1 if the value of rhs is less than lhs
- * 0 if values are the same
- */
- set compareFunc(func) {
- this.clear();
- this.__t.compare = func;
- }
-
- /*======================================================
- * STL-like methods
- *======================================================*/
-
- /**
- * Forward iterator to the first element
- * @returns {Iterator}
- * @example
- * let set = new TreeMultiSet();
- * ...
- * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- begin() {
- return this.__t.begin();
- }
-
- /**
- * Forward iterator to the element following the last element
- * @returns {Iterator}
- * @example
- * let set = new TreeMultiSet();
- * ...
- * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- end() {
- return this.__t.end();
- }
-
- /**
- * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * ...
- * let it = set.find(1);
- * if (!it.equals(set.end())) {
- * console.log(`Found key: ${it.key}`); // 1
- * }
- */
- find(key) {
- return this.__t.find(key);
- }
-
- /**
- * Adds key if such key does not exist in the set.
- * @param {*} key
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let set = new TreeMultiSet();
- * set.insertUnique(1);
- * set.insertUnique(1); // this step has no effect on the set
- * let flag = set.has(1); // true
- * let size = set.size; // 1
- */
- insertUnique(key) {
- let n = new TreeNode();
- n.key = key;
- return this.__t.insertUnique(n);
- }
-
- /**
- * Adds key if such key does not exist in the set. Same as insertUnique()
- * @param {*} key
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let set = new TreeMultiSet();
- * set.insertOrReplace(1);
- * set.insertOrReplace(1); // this step has no effect on the set
- * let flag = set.has(1); // true
- * let size = set.size; // 1
- */
- insertOrReplace(key) {
- let n = new TreeNode();
- n.key = key;
- return this.__t.insertOrReplace(n);
- }
-
- /**
- * Adds key whether it exists or not in the set.
- * @param {*} key
- * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
- * @example
- * let set = new TreeMultiSet();
- * set.insertMulti(1);
- * set.insertMulti(1); // this step has no effect on the map
- * let flag = set.has(1); // true
- * let size = set.size; // 2
- */
- insertMulti(key) {
- let n = new TreeNode();
- n.key = key;
- return this.__t.insertMulti(n);
- }
-
- /**
- * Removes value for the specified iterator.
- * @param {Iterator} iterator
- * @example
- * let set = new TreeMultiSet([1,2,3]);
- * let it = set.find(2);
- * it.prev();
- * set.erase(it); // removes a node with key 1
- * console.log(set.toString()); // {2,3}
- */
- erase(iterator) {
- this.__t.erase(iterator.node);
- }
-
- /**
- * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let set = new TreeMultiSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = set.lowerBound(0);
- * let to = set.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let set = new TreeMultiSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(set.upperBound(50));
- * let to = new ReverseIterator(set.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- lowerBound(key) {
- return this.__t.lowerBound(key);
- }
-
- /**
- * Reverse iterator to the last element.
- * @returns {ReverseIterator}
- * @example
- * let set = new TreeMultiSet();
- * ...
- * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- rbegin() {
- return this.__t.rbegin();
- }
-
- /**
- * Reverse iterator pointing to before the first element.
- * @returns {ReverseIterator}
- * @example
- * let set = new TreeMultiSet();
- * ...
- * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
- * console.log(`key: ${it.key}`);
- * }
- */
- rend() {
- return this.__t.rend();
- }
-
- /**
- * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
- * @param {*} key
- * @returns {Iterator}
- * @example
- * let set = new TreeMultiSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive
- * let from = set.lowerBound(0);
- * let to = set.upperBound(50);
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- *
- * let set = new TreeMultiSet();
- * ... // add key-value pairs., using numbers as keys
- * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
- * let from = new ReverseIterator(set.upperBound(50));
- * let to = new ReverseIterator(set.lowerBound(0));
- * let it = from;
- * while (!it.equals(to)) {
- * console.log(it.key);
- * it.next();
- * }
- */
- upperBound(key) {
- return this.__t.upperBound(key);
- }
-
- /**
- * @returns first element of the container, or undefined if container is empty
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * let first = set.first(); // 1
- */
- first() {
- return this.__t.first();
- }
-
- /**
- * @returns last element of the container, or undefined if container is empty
- * @example
- * let set = new TreeMultiSet([1, 2, 3]);
- * let last = set.last(); // 3
- */
- last() {
- return this.__t.last();
- }
-
- /**
- * Serializes contents of the set in the form {key1,key2,...}
- * @returns {String}
- */
- toString() {
- return this.__t.toString();
- }
-}
-
-module.exports = {
- TreeMultiSet: TreeMultiSet,
-};
-
-/***/ })
-/******/ ]);
-});
-
-/***/ }),
-
/***/ 742:
/***/ (function(module, __unusedexports, __webpack_require__) {
diff --git a/src/main.ts b/src/main.ts
index 71da8d9..19b00b3 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,9 +1,11 @@
import * as github from '@actions/github'
import * as core from '@actions/core'
import * as rest from '@octokit/rest'
-import * as treemap from 'jstreemap'
-const CANCELLABLE_RUNS = [
+/**
+ Those are the cancellable event types tht we know about
+ */
+const CANCELLABLE_EVENT_TYPES = [
'push',
'pull_request',
'workflow_run',
@@ -11,104 +13,190 @@ const CANCELLABLE_RUNS = [
'workflow_dispatch'
]
+/**
+ * Those are different modes for cancelling
+ */
enum CancelMode {
DUPLICATES = 'duplicates',
+ ALL_DUPLICATES = 'allDuplicates',
SELF = 'self',
FAILED_JOBS = 'failedJobs',
NAMED_JOBS = 'namedJobs'
}
-function createListRunsQueryOtherRuns(
- octokit: github.GitHub,
- owner: string,
- repo: string,
- status: string,
- workflowId: number | string,
- headBranch: string,
+/**
+ * Stores information uniquely identifying the run by it's triggering source:
+ * It is the workflow id, Head Repo and Branch the change originates from and event name that originates it.
+ *
+ * All Runs coming from the same Pull Request share the same Source Group. Also all pushes to master
+ * share the same Source Group
+ */
+interface WorkflowRunSourceGroup {
+ workflowId: number | string
+ headRepo: string
+ headBranch: string
+ eventName: string
+}
+
+/**
+ * Stores information about the owner and repository used, as well as octokit object that is used for
+ * authentication.
+ */
+interface RepositoryInfo {
+ octokit: github.GitHub
+ owner: string
+ repo: string
+}
+
+/**
+ * Stores information about the workflow info that triggered the current workflow.
+ */
+interface TriggeringWorkflowInfo {
+ headRepo: string
+ headBranch: string
+ headSha: string
eventName: string
+ mergeCommitSha: string | null
+ pullRequest: rest.PullsListResponseItem | null
+}
+
+/**
+ * Converts the source of a run object into a string that can be used as map key in maps where we keep
+ * arrays of runs per source group
+ * @param sourceGroup the object identifying the source of the run (Pull Request, Master Push)
+ * @returns the unique string id for the source group
+ */
+function getSourceGroupId(sourceGroup: WorkflowRunSourceGroup): string {
+ return `:${sourceGroup.workflowId}:${sourceGroup.headRepo}:${sourceGroup.headBranch}:${sourceGroup.eventName}`
+}
+
+/**
+ * Creates query parameters selecting all runs that share the same source group as we have. This can
+ * be used to select duplicates of my own run.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param mySourceGroup - source group of the originating run
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQueryRunsSameSource(
+ repositoryInfo: RepositoryInfo,
+ status: string,
+ mySourceGroup: WorkflowRunSourceGroup
): rest.RequestOptions {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
- workflow_id: workflowId,
+ workflow_id: mySourceGroup.workflowId,
status,
- branch: headBranch,
- event: eventName
+ branch: mySourceGroup.headBranch,
+ event: mySourceGroup.eventName
}
- return octokit.actions.listWorkflowRuns.endpoint.merge(request)
+ return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
}
-
-function createListRunsQueryMyOwnRun(
- octokit: github.GitHub,
- owner: string,
- repo: string,
+/**
+ * Creates query parameters selecting only specific run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @param runId - run Id to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQuerySpecificRunId(
+ repositoryInfo: RepositoryInfo,
status: string,
workflowId: number | string,
runId: number
): rest.RequestOptions {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
workflow_id: workflowId,
status,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId.toString()
}
- return octokit.actions.listWorkflowRuns.endpoint.merge(request)
+ return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
}
+/**
+ * Creates query parameters selecting all run Ids for specified workflow Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
function createListRunsQueryAllRuns(
- octokit: github.GitHub,
- owner: string,
- repo: string,
+ repositoryInfo: RepositoryInfo,
status: string,
workflowId: number | string
): rest.RequestOptions {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
workflow_id: workflowId,
status
}
- return octokit.actions.listWorkflowRuns.endpoint.merge(request)
+ return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
}
+/**
+ * Creates query parameters selecting all jobs for specified run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return query parameters merged with the listJobsForWorkflowRun criteria
+ */
function createJobsForWorkflowRunQuery(
- octokit: github.GitHub,
- owner: string,
- repo: string,
+ repositoryInfo: RepositoryInfo,
runId: number
): rest.RequestOptions {
const request = {
- owner,
- repo,
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
}
- return octokit.actions.listJobsForWorkflowRun.endpoint.merge(request)
+ return repositoryInfo.octokit.actions.listJobsForWorkflowRun.endpoint.merge(
+ request
+ )
}
-function matchInArray(s: string, regexps: string[]): boolean {
+/**
+ * Returns true if the string matches any of the regexps in array of regexps
+ * @param stringToMatch string to match
+ * @param regexps array of regexp to match the string against
+ * @return true if there is a match
+ */
+function matchInArray(stringToMatch: string, regexps: string[]): boolean {
for (const regexp of regexps) {
- if (s.match(regexp)) {
+ if (stringToMatch.match(regexp)) {
return true
}
}
return false
}
+/**
+ * Returns true if the runId specified has jobs matching the regexp and optionally checks if those
+ * jobs are failed.
+ * * If checkIfFailed is False, it returns true if any of the job name for the run match any of the regexps
+ * * Id checkIfFailed is True, it returns true if any of the matching jobs have status 'failed'
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @param jobNameRegexps - array of job name regexps
+ * @param checkIfFailed - whether to check the 'failed' status of matched jobs
+ * @return true if there is a match
+ */
async function jobsMatchingNames(
- octokit: github.GitHub,
- owner: string,
- repo: string,
+ repositoryInfo: RepositoryInfo,
runId: number,
jobNameRegexps: string[],
checkIfFailed: boolean
): Promise<boolean> {
- const listJobs = createJobsForWorkflowRunQuery(octokit, owner, repo, runId)
+ const listJobs = createJobsForWorkflowRunQuery(repositoryInfo, runId)
if (checkIfFailed) {
core.info(
`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps} that failed\n`
@@ -118,7 +206,7 @@ async function jobsMatchingNames(
`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps}\n`
)
}
- for await (const item of octokit.paginate.iterator(listJobs)) {
+ for await (const item of repositoryInfo.octokit.paginate.iterator(listJobs)) {
for (const job of item.data.jobs) {
core.info(` The job name: ${job.name}, Conclusion: ${job.conclusion}`)
if (matchInArray(job.name, jobNameRegexps)) {
@@ -126,12 +214,14 @@ async function jobsMatchingNames(
// Only fail the build if one of the matching jobs fail
if (job.conclusion === 'failure') {
core.info(
- ` The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed. Cancelling run.`
+ ` The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed.` +
+ ` Cancelling run.`
)
return true
} else {
core.info(
- ` The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. So far, so good.`
+ ` The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. ` +
+ ` So far, so good.`
)
}
} else {
@@ -147,41 +237,62 @@ async function jobsMatchingNames(
return false
}
+/**
+ * Retrieves workflowId from the workflow URL.
+ * @param workflowUrl workflow URL to retrieve the ID from
+ * @return numerical workflow id
+ */
+function retrieveWorkflowIdFromUrl(workflowUrl: string): number {
+ const workflowIdString = workflowUrl.split('/').pop() || ''
+ if (!(workflowIdString.length > 0)) {
+ throw new Error('Could not resolve workflow')
+ }
+ return parseInt(workflowIdString)
+}
+
+/**
+ * Returns workflowId of the runId specified
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return workflow ID for the run id
+ */
async function getWorkflowId(
- octokit: github.GitHub,
- runId: number,
- owner: string,
- repo: string
+ repositoryInfo: RepositoryInfo,
+ runId: number
): Promise<number> {
- const reply = await octokit.actions.getWorkflowRun({
- owner,
- repo,
+ const reply = await repositoryInfo.octokit.actions.getWorkflowRun({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
})
core.info(`The source run ${runId} is in ${reply.data.workflow_url} workflow`)
- const workflowIdString = reply.data.workflow_url.split('/').pop() || ''
- if (!(workflowIdString.length > 0)) {
- throw new Error('Could not resolve workflow')
- }
- return parseInt(workflowIdString)
+ return retrieveWorkflowIdFromUrl(reply.data.workflow_url)
}
+/**
+ * Returns workflow runs matching the callable adding query criteria
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - array of string status values for runs that we are interested at
+ * @param cancelMode - which cancel mode the query is about
+ * @param createListRunQuery - what is the callable criteria selection
+ * @return map of workflow run items indexed by the workflow run number
+ */
async function getWorkflowRuns(
- octokit: github.GitHub,
+ repositoryInfo: RepositoryInfo,
statusValues: string[],
cancelMode: CancelMode,
createListRunQuery: CallableFunction
-): Promise<
- treemap.TreeMap<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>
-> {
- const workflowRuns = new treemap.TreeMap<
+): Promise<Map<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>> {
+ const workflowRuns = new Map<
number,
rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
>()
for (const status of statusValues) {
const listRuns = await createListRunQuery(status)
- for await (const item of octokit.paginate.iterator(listRuns)) {
+ for await (const item of repositoryInfo.octokit.paginate.iterator(
+ listRuns
+ )) {
// There is some sort of bug where the pagination URLs point to a
// different endpoint URL which trips up the resulting representation
// In that case, fallback to the actual REST 'workflow_runs' property
@@ -196,10 +307,134 @@ async function getWorkflowRuns(
return workflowRuns
}
-async function shouldBeCancelled(
- octokit: github.GitHub,
- owner: string,
- repo: string,
+/**
+ * True if the request is candidate for cancelling in case of duplicate deletion
+ * @param runItem item to check
+ * @param headRepo head Repo that we are checking against
+ * @param cancelFutureDuplicates whether future duplicates are being cancelled
+ * @param sourceRunId the source Run Id that originates the request
+ * @return true if we determine that the run Id should be cancelled
+ */
+function isCandidateForCancellingDuplicate(
+ runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+ headRepo: string,
+ cancelFutureDuplicates: boolean,
+ sourceRunId: number
+): boolean {
+ const runHeadRepo = runItem.head_repository.full_name
+ if (headRepo !== undefined && runHeadRepo !== headRepo) {
+ core.info(
+ `\nThe run ${runItem.id} is from a different ` +
+ `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`
+ )
+ return false
+ }
+ if (cancelFutureDuplicates) {
+ core.info(
+ `\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`
+ )
+ return true
+ } else {
+ if (runItem.id === sourceRunId) {
+ core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`)
+ return false
+ } else if (runItem.id > sourceRunId) {
+ core.info(
+ `\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`
+ )
+ return false
+ }
+ core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`)
+ return true
+ }
+}
+
+/**
+ * Should the run is candidate for cancelling in SELF cancelling mode?
+ * @param runItem run item
+ * @param sourceRunId source run id
+ * @return true if the run item is self
+ */
+function isCandidateForCancellingSelf(
+ runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+ sourceRunId: number
+): boolean {
+ if (runItem.id === sourceRunId) {
+ core.info(`\nReturning the "source" run: ${runItem.id}.\n`)
+ return true
+ } else {
+ return false
+ }
+}
+
+/**
+ * Should the run is candidate for cancelling in naming job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains jobs with names matching the pattern
+ */
+async function isCandidateForCancellingNamedJobs(
+ repositoryInfo: RepositoryInfo,
+ runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+ jobNamesRegexps: string[]
+): Promise<boolean> {
+ // Cancel all jobs that have failed jobs (no matter when started)
+ if (
+ await jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, false)
+ ) {
+ core.info(
+ `\nSome jobs have matching names in ${runItem.id} . Returning it.\n`
+ )
+ return true
+ } else {
+ core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`)
+ return false
+ }
+}
+
+/**
+ * Should the run is candidate for cancelling in failed job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains failed jobs with names matching the pattern
+ */
+async function isCandidateForCancellingFailedJobs(
+ repositoryInfo: RepositoryInfo,
+ runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+ jobNamesRegexps: string[]
+): Promise<boolean> {
+ // Cancel all jobs that have failed jobs (no matter when started)
+ if (
+ await jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, true)
+ ) {
+ core.info(
+ `\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`
+ )
+ return true
+ } else {
+ core.info(
+ `\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`
+ )
+ return false
+ }
+}
+
+/**
+ * Determines whether the run is candidate to be cancelled depending on the mode used
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run item
+ * @param headRepo - head repository
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - what is the source run id
+ * @param jobNamesRegexps - what are the regexps for job names
+ * @param skipEventTypes - which events should be skipped
+ * @return true if the run id is candidate for cancelling
+ */
+async function isCandidateForCancelling(
+ repositoryInfo: RepositoryInfo,
runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
headRepo: string,
cancelMode: CancelMode,
@@ -212,9 +447,10 @@ async function shouldBeCancelled(
core.info(`\nThe run ${runItem.id} is completed. Not cancelling it.\n`)
return false
}
- if (!CANCELLABLE_RUNS.includes(runItem.event.toString())) {
+ if (!CANCELLABLE_EVENT_TYPES.includes(runItem.event.toString())) {
core.info(
- `\nThe run ${runItem.id} is (${runItem.event} event - not in ${CANCELLABLE_RUNS}). Not cancelling it.\n`
+ `\nThe run ${runItem.id} is (${runItem.event} event - not ` +
+ `in ${CANCELLABLE_EVENT_TYPES}). Not cancelling it.\n`
)
return false
}
@@ -226,83 +462,31 @@ async function shouldBeCancelled(
return false
}
if (cancelMode === CancelMode.FAILED_JOBS) {
- // Cancel all jobs that have failed jobs (no matter when started)
- if (
- await jobsMatchingNames(
- octokit,
- owner,
- repo,
- runItem.id,
- jobNamesRegexps,
- true
- )
- ) {
- core.info(
- `\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`
- )
- return true
- } else {
- core.info(
- `\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`
- )
- return false
- }
+ return await isCandidateForCancellingFailedJobs(
+ repositoryInfo,
+ runItem,
+ jobNamesRegexps
+ )
} else if (cancelMode === CancelMode.NAMED_JOBS) {
- // Cancel all jobs that have failed jobs (no matter when started)
- if (
- await jobsMatchingNames(
- octokit,
- owner,
- repo,
- runItem.id,
- jobNamesRegexps,
- false
- )
- ) {
- core.info(
- `\nSome jobs have matching names in ${runItem.id} . Returning it.\n`
- )
- return true
- } else {
- core.info(
- `\nNone of the jobs match name in ${runItem.id}. Returning it.\n`
- )
- return false
- }
+ return await isCandidateForCancellingNamedJobs(
+ repositoryInfo,
+ runItem,
+ jobNamesRegexps
+ )
} else if (cancelMode === CancelMode.SELF) {
- if (runItem.id === sourceRunId) {
- core.info(`\nReturning the "source" run: ${runItem.id}.\n`)
- return true
- } else {
- return false
- }
+ return isCandidateForCancellingSelf(runItem, sourceRunId)
} else if (cancelMode === CancelMode.DUPLICATES) {
- const runHeadRepo = runItem.head_repository.full_name
- if (headRepo !== undefined && runHeadRepo !== headRepo) {
- core.info(
- `\nThe run ${runItem.id} is from a different ` +
- `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`
- )
- return false
- }
- if (cancelFutureDuplicates) {
- core.info(
- `\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`
- )
- return true
- } else {
- if (runItem.id === sourceRunId) {
- core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`)
- return false
- } else if (runItem.id > sourceRunId) {
- core.info(
- `\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`
- )
- return false
- }
- core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`)
- return true
- }
+ return isCandidateForCancellingDuplicate(
+ runItem,
+ headRepo,
+ cancelFutureDuplicates,
+ sourceRunId
+ )
+ } else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+ core.info(
+ `Returning candidate ${runItem.id} for "all_duplicates" cancelling.`
+ )
+ return true
} else {
throw Error(
`\nWrong cancel mode ${cancelMode}! This should never happen.\n`
@@ -310,17 +494,20 @@ async function shouldBeCancelled(
}
}
+/**
+ * Cancels the specified workflow run.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run Id to cancel
+ */
async function cancelRun(
- octokit: github.GitHub,
- owner: string,
- repo: string,
+ repositoryInfo: RepositoryInfo,
runId: number
): Promise<void> {
let reply
try {
- reply = await octokit.actions.cancelWorkflowRun({
- owner,
- repo,
+ reply = await repositoryInfo.octokit.actions.cancelWorkflowRun({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
})
@@ -332,72 +519,71 @@ async function cancelRun(
}
}
-async function findAndCancelRuns(
- octokit: github.GitHub,
- selfRunId: number,
+/**
+ * Returns map of workflow run items matching the criteria specified group by workflow run id
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - status values we want to check
+ * @param cancelMode - cancel mode to use
+ * @param sourceWorkflowId - source workflow id
+ * @param sourceRunId - source run id
+ * @param sourceEventName - name of the source event
+ * @param mySourceGroup - source of the run that originated it
+ * @return map of the run items matching grouped by workflow run id
+ */
+async function getWorkflowRunsMatchingCriteria(
+ repositoryInfo: RepositoryInfo,
+ statusValues: string[],
+ cancelMode:
+ | CancelMode
+ | CancelMode.DUPLICATES
+ | CancelMode.ALL_DUPLICATES
+ | CancelMode.FAILED_JOBS
+ | CancelMode.NAMED_JOBS,
sourceWorkflowId: number | string,
sourceRunId: number,
- owner: string,
- repo: string,
- headRepo: string,
- headBranch: string,
sourceEventName: string,
- cancelMode: CancelMode,
- cancelFutureDuplicates: boolean,
- notifyPRCancel: boolean,
- notifyPRMessageStart: string,
- jobNameRegexps: string[],
- skipEventTypes: string[],
- reason: string
-): Promise<number[]> {
- const statusValues = ['queued', 'in_progress']
- const workflowRuns = await getWorkflowRuns(
- octokit,
+ mySourceGroup: WorkflowRunSourceGroup
+): Promise<Map<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>> {
+ return await getWorkflowRuns(
+ repositoryInfo,
statusValues,
cancelMode,
function(status: string) {
if (cancelMode === CancelMode.SELF) {
core.info(
- `\nFinding runs for my own run: Owner: ${owner}, Repo: ${repo}, ` +
+ `\nFinding runs for my own run: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
`Workflow ID:${sourceWorkflowId}, Source Run id: ${sourceRunId}\n`
)
- return createListRunsQueryMyOwnRun(
- octokit,
- owner,
- repo,
+ return createListRunsQuerySpecificRunId(
+ repositoryInfo,
status,
sourceWorkflowId,
sourceRunId
)
} else if (
cancelMode === CancelMode.FAILED_JOBS ||
- cancelMode === CancelMode.NAMED_JOBS
+ cancelMode === CancelMode.NAMED_JOBS ||
+ cancelMode === CancelMode.ALL_DUPLICATES
) {
core.info(
- `\nFinding runs for all runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
- `Workflow ID:${sourceWorkflowId}\n`
+ `\nFinding runs for all runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+ `Status: ${status} Workflow ID:${sourceWorkflowId}\n`
)
return createListRunsQueryAllRuns(
- octokit,
- owner,
- repo,
+ repositoryInfo,
status,
sourceWorkflowId
)
} else if (cancelMode === CancelMode.DUPLICATES) {
core.info(
- `\nFinding duplicate runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
- `Workflow ID:${sourceWorkflowId}, Head Branch: ${headBranch},` +
+ `\nFinding duplicate runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+ `Status: ${status} Workflow ID:${sourceWorkflowId}, Head Branch: ${mySourceGroup.headBranch},` +
`Event name: ${sourceEventName}\n`
)
- return createListRunsQueryOtherRuns(
- octokit,
- owner,
- repo,
+ return createListRunsQueryRunsSameSource(
+ repositoryInfo,
status,
- sourceWorkflowId,
- headBranch,
- sourceEventName
+ mySourceGroup
)
} else {
throw Error(
@@ -406,18 +592,105 @@ async function findAndCancelRuns(
}
}
)
- const workflowsToCancel: [number, string][] = []
- const pullRequestToNotify: number[] = []
+}
+
+/**
+ * Finds pull request matching its headRepo, headBranch and headSha
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository from which Pull Request comes
+ * @param headBranch - head branch from which Pull Request comes
+ * @param headSha - sha for the head of the incoming Pull request
+ */
+async function findPullRequest(
+ repositoryInfo: RepositoryInfo,
+ headRepo: string,
+ headBranch: string,
+ headSha: string
+): Promise<rest.PullsListResponseItem | null> {
+ // Finds Pull request for this workflow run
+ core.info(
+ `\nFinding PR request id for: owner: ${repositoryInfo.owner}, Repo:${repositoryInfo.repo},` +
+ ` Head:${headRepo}:${headBranch}.\n`
+ )
+ const pullRequests = await repositoryInfo.octokit.pulls.list({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
+ head: `${headRepo}:${headBranch}`
+ })
+ for (const pullRequest of pullRequests.data) {
+ core.info(
+ `\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`
+ )
+ if (pullRequest.head.sha === headSha) {
+ core.info(`\nFound PR: ${pullRequest.number}\n`)
+ return pullRequest
+ }
+ }
+ core.info(`\nCould not find the PR for this build :(\n`)
+ return null
+}
+
+/**
+ * Finds pull request id for the run item.
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run Item that the pull request should be found for
+ * @return pull request number to notify (or undefined if not found)
+ */
+async function findPullRequestForRunItem(
+ repositoryInfo: RepositoryInfo,
+ runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
+): Promise<number | undefined> {
+ const pullRequest = await findPullRequest(
+ repositoryInfo,
+ runItem.head_repository.owner.login,
+ runItem.head_branch,
+ runItem.head_sha
+ )
+ if (pullRequest) {
+ return pullRequest.number
+ }
+ return undefined
+}
+
+/**
+ * Maps found workflow runs into groups - filters out the workflows that are not eligible for canceling
+ * (depends on cancel Mode) and assigns each workflow to groups - where workflow runs from the
+ * same group are put together in one array - in a map indexed by the source group id.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository the event comes from
+ * @param cancelMode - cancel mode to use
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - source run id for the run
+ * @param jobNameRegexps - regexps for job names
+ * @param skipEventTypes - array of event names to skip
+ * @param workflowRuns - map of workflow runs found
+ * @parm map where key is the source group id and value is array of workflow run item candidates to cancel
+ */
+async function filterAndMapWorkflowRunsToGroups(
+ repositoryInfo: RepositoryInfo,
+ headRepo: string,
+ cancelMode: CancelMode,
+ cancelFutureDuplicates: boolean,
+ sourceRunId: number,
+ jobNameRegexps: string[],
+ skipEventTypes: string[],
+ workflowRuns: Map<
+ number,
+ rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
+ >
+): Promise<
+ Map<string, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]>
+> {
+ const mapOfWorkflowRunCandidates = new Map()
for (const [key, runItem] of workflowRuns) {
core.info(
`\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status},` +
` Created at ${runItem.created_at}\n`
)
if (
- await shouldBeCancelled(
- octokit,
- owner,
- repo,
+ await isCandidateForCancelling(
+ repositoryInfo,
runItem,
headRepo,
cancelMode,
@@ -427,135 +700,287 @@ async function findAndCancelRuns(
skipEventTypes
)
) {
- if (notifyPRCancel && runItem.event === 'pull_request') {
- const pullRequest = await findPullRequest(
- octokit,
- owner,
- repo,
- runItem.head_repository.owner.login,
- runItem.head_branch,
- runItem.head_sha
- )
- if (pullRequest) {
- pullRequestToNotify.push(pullRequest.number)
- }
+ const candidateSourceGroup: WorkflowRunSourceGroup = {
+ workflowId: retrieveWorkflowIdFromUrl(runItem.workflow_url),
+ headBranch: runItem.head_branch,
+ headRepo: runItem.head_repository.full_name,
+ eventName: runItem.event
+ }
+ const sourceGroupId = getSourceGroupId(candidateSourceGroup)
+ let workflowRunArray:
+ | rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
+ | undefined = mapOfWorkflowRunCandidates.get(sourceGroupId)
+ if (workflowRunArray === undefined) {
+ workflowRunArray = []
+ mapOfWorkflowRunCandidates.set(sourceGroupId, workflowRunArray)
}
- workflowsToCancel.push([runItem.id, runItem.created_at])
- }
- }
- // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
- const sortedRunTuplesToCancel = workflowsToCancel.sort(
- (runTuple1, runTuple2) => runTuple2[1].localeCompare(runTuple1[1])
- )
- if (sortedRunTuplesToCancel.length > 0) {
- if (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) {
core.info(
- `\nSkipping the first run (${sortedRunTuplesToCancel[0]}) of all the matching ` +
- `duplicates - this one we are going to leave in peace!\n`
+ `The candidate ${runItem.id} has been added to ${sourceGroupId} group of candidates`
)
- sortedRunTuplesToCancel.shift()
- }
- if (sortedRunTuplesToCancel.length === 0) {
- core.info(`\nNo duplicates to cancel!\n`)
- return sortedRunTuplesToCancel.map(runTuple => runTuple[0])
+ workflowRunArray.push(runItem)
}
- core.info(
- '\n###### Cancelling runs starting from the oldest ##########\n' +
- `\n Runs to cancel: ${sortedRunTuplesToCancel.length}\n` +
- `\n PRs to notify: ${pullRequestToNotify.length}\n`
- )
- for (const runTuple of sortedRunTuplesToCancel) {
- core.info(`\nCancelling run: ${runTuple}.\n`)
- await cancelRun(octokit, owner, repo, runTuple[0])
- }
- for (const pullRequestNumber of pullRequestToNotify) {
- const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`
- await addCommentToPullRequest(
- octokit,
- owner,
- repo,
- pullRequestNumber,
- `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`
- )
- }
- core.info(
- '\n###### Finished cancelling runs ##########\n'
- )
- } else {
- core.info(
- '\n###### There are no runs to cancel! ##########\n'
- )
}
- return sortedRunTuplesToCancel.map(runTuple => runTuple[0])
-}
-
-function getRequiredEnv(key: string): string {
- const value = process.env[key]
- if (value === undefined) {
- const message = `${key} was not defined.`
- throw new Error(message)
- }
- return value
+ return mapOfWorkflowRunCandidates
}
+/**
+ * Add specified comment to Pull Request
+ * @param repositoryInfo - information about the repository used
+ * @param pullRequestNumber - number of pull request
+ * @param comment - comment to add
+ */
async function addCommentToPullRequest(
- octokit: github.GitHub,
- owner: string,
- repo: string,
+ repositoryInfo: RepositoryInfo,
pullRequestNumber: number,
comment: string
): Promise<void> {
core.info(`\nNotifying PR: ${pullRequestNumber} with '${comment}'.\n`)
- await octokit.issues.createComment({
- owner,
- repo,
+ await repositoryInfo.octokit.issues.createComment({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
issue_number: pullRequestNumber,
body: comment
})
}
-async function findPullRequest(
- octokit: github.GitHub,
- owner: string,
- repo: string,
- headRepo: string,
- headBranch: string,
- headSha: string
-): Promise<rest.PullsListResponseItem | null> {
- // Finds Pull request for this workflow run
+/**
+ * Notifies PR about cancelling
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - my own run id
+ * @param pullRequestNumber - number of pull request
+ * @param reason reason for canceling
+ */
+async function notifyPR(
+ repositoryInfo: RepositoryInfo,
+ selfRunId: number,
+ pullRequestNumber: number,
+ reason: string
+): Promise<void> {
+ const selfWorkflowRunUrl =
+ `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+ `/actions/runs/${selfRunId}`
+ await addCommentToPullRequest(
+ repositoryInfo,
+ pullRequestNumber,
+ `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`
+ )
+}
+
+/**
+ * Cancels all runs in the specified group of runs.
+ * @param repositoryInfo - information about the repository used
+ * @param sortedRunItems - items sorted in descending order (descending order by created_at)
+ * @param notifyPRCancel - whether to notify the PR when cancelling
+ * @param selfRunId - what is the self run id
+ * @param sourceGroupId - what is the source group id
+ * @param reason - reason for canceling
+ */
+async function cancelAllRunsInTheSourceGroup(
+ repositoryInfo: RepositoryInfo,
+ sortedRunItems: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[],
+ notifyPRCancel: boolean,
+ selfRunId: number,
+ sourceGroupId: string,
+ reason: string
+): Promise<number[]> {
core.info(
- `\nFinding PR request id for: owner: ${owner}, Repo:${repo}, Head:${headRepo}:${headBranch}.\n`
+ `\n###### Cancelling runs for ${sourceGroupId} starting from the most recent ##########\n` +
+ `\n Number of runs to cancel: ${sortedRunItems.length}\n`
)
- const pullRequests = await octokit.pulls.list({
- owner,
- repo,
- head: `${headRepo}:${headBranch}`
- })
- for (const pullRequest of pullRequests.data) {
- core.info(
- `\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`
+ const cancelledRuns: number[] = []
+ for (const runItem of sortedRunItems) {
+ if (notifyPRCancel && runItem.event === 'pull_request') {
+ const pullRequestNumber = await findPullRequestForRunItem(
+ repositoryInfo,
+ runItem
+ )
+ if (pullRequestNumber !== undefined) {
+ core.info(
+ `\nNotifying PR: ${pullRequestNumber} (runItem: ${runItem}) with: ${reason}\n`
+ )
+ await notifyPR(repositoryInfo, selfRunId, pullRequestNumber, reason)
+ }
+ }
+ core.info(`\nCancelling run: ${runItem}.\n`)
+ await cancelRun(repositoryInfo, runItem.id)
+ cancelledRuns.push(runItem.id)
+ }
+ core.info(
+ `\n###### Finished cancelling runs for ${sourceGroupId} ##########\n`
+ )
+ return cancelledRuns
+}
+
+/**
+ * Cancels found runs in a smart way. It takes all the found runs group by the source group, sorts them
+ * descending according to create date in each of the source groups and cancels them in that order -
+ * optionally skipping the first found run in each source group in case of duplicates.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param mapOfWorkflowRunsCandidatesToCancel map of all workflow run candidates
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param notifyPRCancel - whether to notify PRs with comments
+ * @param selfRunId - self run Id
+ * @param reason - reason for canceling
+ */
+async function cancelTheRunsPerGroup(
+ repositoryInfo: RepositoryInfo,
+ mapOfWorkflowRunsCandidatesToCancel: Map<
+ string,
+ rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
+ >,
+ cancelMode: CancelMode,
+ cancelFutureDuplicates: boolean,
+ notifyPRCancel: boolean,
+ selfRunId: number,
+ reason: string
+): Promise<number[]> {
+ const cancelledRuns: number[] = []
+ for (const [
+ sourceGroupId,
+ candidatesArray
+ ] of mapOfWorkflowRunsCandidatesToCancel) {
+ // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
+ const sortedRunItems = candidatesArray.sort((runItem1, runItem2) =>
+ runItem2.created_at.localeCompare(runItem1.created_at)
)
- if (pullRequest.head.sha === headSha) {
- core.info(`\nFound PR: ${pullRequest.number}\n`)
- return pullRequest
+ if (sortedRunItems.length > 0) {
+ if (
+ (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) ||
+ cancelMode === CancelMode.ALL_DUPLICATES
+ ) {
+ core.info(
+ `\nSkipping the first run (${sortedRunItems[0].id}) of all the matching ` +
+ `duplicates for '${sourceGroupId}'. This one we are going to leave in peace!\n`
+ )
+ sortedRunItems.shift()
+ }
+ if (sortedRunItems.length === 0) {
+ core.info(`\nNo duplicates to cancel for ${sourceGroupId}!\n`)
+ continue
+ }
+ cancelledRuns.push(
+ ...(await cancelAllRunsInTheSourceGroup(
+ repositoryInfo,
+ sortedRunItems,
+ notifyPRCancel,
+ selfRunId,
+ sourceGroupId,
+ reason
+ ))
+ )
+ } else {
+ core.info(
+ `\n###### There are no runs to cancel for ${sourceGroupId} ##########\n`
+ )
}
}
- core.info(`\nCould not find the PR for this build :(\n`)
- return null
+ return cancelledRuns
+}
+
+/**
+ * Find and cancels runs based on the criteria chosen.
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ * (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ * @param reason - reason for cancelling
+ * @return array of canceled workflow run ids
+ */
+async function findAndCancelRuns(
+ repositoryInfo: RepositoryInfo,
+ selfRunId: number,
+ sourceWorkflowId: number | string,
+ sourceRunId: number,
+ headRepo: string,
+ headBranch: string,
+ sourceEventName: string,
+ cancelMode: CancelMode,
+ cancelFutureDuplicates: boolean,
+ notifyPRCancel: boolean,
+ notifyPRMessageStart: string,
+ jobNameRegexps: string[],
+ skipEventTypes: string[],
+ reason: string
+): Promise<number[]> {
+ const statusValues = ['queued', 'in_progress']
+ const mySourceGroup: WorkflowRunSourceGroup = {
+ headBranch,
+ headRepo,
+ eventName: sourceEventName,
+ workflowId: sourceWorkflowId
+ }
+ const workflowRuns = await getWorkflowRunsMatchingCriteria(
+ repositoryInfo,
+ statusValues,
+ cancelMode,
+ sourceWorkflowId,
+ sourceRunId,
+ sourceEventName,
+ mySourceGroup
+ )
+ const mapOfWorkflowRunsCandidatesToCancel = await filterAndMapWorkflowRunsToGroups(
+ repositoryInfo,
+ headRepo,
+ cancelMode,
+ cancelFutureDuplicates,
+ sourceRunId,
+ jobNameRegexps,
+ skipEventTypes,
+ workflowRuns
+ )
+ return await cancelTheRunsPerGroup(
+ repositoryInfo,
+ mapOfWorkflowRunsCandidatesToCancel,
+ cancelMode,
+ cancelFutureDuplicates,
+ notifyPRCancel,
+ selfRunId,
+ reason
+ )
}
+/**
+ * Returns environment variable that is required - throws error if it is not defined.
+ * @param key key for the env variable
+ * @return value of the env variable
+ */
+function getRequiredEnv(key: string): string {
+ const value = process.env[key]
+ if (value === undefined) {
+ const message = `${key} was not defined.`
+ throw new Error(message)
+ }
+ return value
+}
+
+/**
+ * Gets origin of the runId - if this is a workflow run, it returns the information about the source run
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run id of the run to check
+ * @return information about the triggering workflow
+ */
async function getOrigin(
- octokit: github.GitHub,
- runId: number,
- owner: string,
- repo: string
-): Promise<
- [string, string, string, string, string, rest.PullsListResponseItem | null]
-> {
- const reply = await octokit.actions.getWorkflowRun({
- owner,
- repo,
+ repositoryInfo: RepositoryInfo,
+ runId: number
+): Promise<TriggeringWorkflowInfo> {
+ const reply = await repositoryInfo.octokit.actions.getWorkflowRun({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
// eslint-disable-next-line @typescript-eslint/camelcase
run_id: runId
})
@@ -568,32 +993,50 @@ async function getOrigin(
let pullRequest: rest.PullsListResponseItem | null = null
if (sourceRun.event === 'pull_request') {
pullRequest = await findPullRequest(
- octokit,
- owner,
- repo,
+ repositoryInfo,
sourceRun.head_repository.owner.login,
sourceRun.head_branch,
sourceRun.head_sha
)
}
- return [
- reply.data.head_repository.full_name,
- reply.data.head_branch,
- reply.data.event,
- reply.data.head_sha,
- pullRequest ? pullRequest.merge_commit_sha : '',
- pullRequest
- ]
+ return {
+ headRepo: reply.data.head_repository.full_name,
+ headBranch: reply.data.head_branch,
+ headSha: reply.data.head_sha,
+ mergeCommitSha: pullRequest ? pullRequest.merge_commit_sha : null,
+ pullRequest: pullRequest ? pullRequest : null,
+ eventName: reply.data.event
+ }
}
+/**
+ * Performs the actual cancelling.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ * (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ * (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRCancelMessage - message to send when cancelling the PR (overrides default message
+ * generated automatically)
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ */
async function performCancelJob(
- octokit: github.GitHub,
+ repositoryInfo: RepositoryInfo,
selfRunId: number,
sourceWorkflowId: number | string,
sourceRunId: number,
- owner: string,
- repo: string,
headRepo: string,
headBranch: string,
sourceEventName: string,
@@ -609,7 +1052,7 @@ async function performCancelJob(
'\n###################################################################################\n'
)
core.info(
- `All parameters: owner: ${owner}, repo: ${repo}, run id: ${sourceRunId}, ` +
+ `All parameters: owner: ${repositoryInfo.owner}, repo: ${repositoryInfo.repo}, run id: ${sourceRunId}, ` +
`head repo ${headRepo}, headBranch: ${headBranch}, ` +
`sourceEventName: ${sourceEventName}, cancelMode: ${cancelMode}, jobNames: ${jobNameRegexps}`
)
@@ -636,9 +1079,14 @@ async function performCancelJob(
reason = `It has jobs matching ${jobNameRegexps}.`
} else if (cancelMode === CancelMode.DUPLICATES) {
core.info(
- `# Cancel duplicate runs started before ${sourceRunId} for workflow ${sourceWorkflowId}.`
+ `# Cancel duplicate runs for workflow ${sourceWorkflowId} for same triggering branch as own run Id.`
+ )
+ reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`
+ } else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+ core.info(
+ `# Cancel all duplicates runs started for workflow ${sourceWorkflowId}.`
)
- reason = `It in earlier duplicate of ${sourceWorkflowId} run.`
+ reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`
} else {
throw Error(`Wrong cancel mode ${cancelMode}! This should never happen.`)
}
@@ -647,12 +1095,10 @@ async function performCancelJob(
)
return await findAndCancelRuns(
- octokit,
+ repositoryInfo,
selfRunId,
sourceWorkflowId,
sourceRunId,
- owner,
- repo,
headRepo,
headBranch,
sourceEventName,
@@ -666,11 +1112,165 @@ async function performCancelJob(
)
}
+/**
+ * Retrieves information about source workflow Id. Either from the current event or from the workflow
+ * nme specified. If the file name is not specified, the workflow to act on is either set to self event
+ * or in case of workflow_run event - to the workflow id that triggered the 'workflow_run' event.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param workflowFileName - optional workflow file name
+ * @param sourceRunId - source run id of the workfow
+ * @param selfRunId - self run id
+ * @return workflow id that is associate with the workflow we are going to act on.
+ */
+async function retrieveWorkflowId(
+ repositoryInfo: RepositoryInfo,
+ workflowFileName: string | null,
+ sourceRunId: number,
+ selfRunId: number
+): Promise<string | number> {
+ let sourceWorkflowId
+ if (workflowFileName) {
+ sourceWorkflowId = workflowFileName
+ core.info(
+ `\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`
+ )
+ } else {
+ sourceWorkflowId = await getWorkflowId(repositoryInfo, sourceRunId)
+ if (sourceRunId === selfRunId) {
+ core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`)
+ } else {
+ core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`)
+ }
+ }
+ return sourceWorkflowId
+}
+/**
+ * Sets output but also prints the output value in the logs.
+ *
+ * @param name name of the output
+ * @param value value of the output
+ */
function verboseOutput(name: string, value: string): void {
core.info(`Setting output: ${name}: ${value}`)
core.setOutput(name, value)
}
+/**
+ * Performs sanity check of the parameters passed. Some of the parameter combinations do not work so they
+ * are verified and in case od unexpected combination found, approrpriate error is raised.
+ *
+ * @param eventName - name of the event to act on
+ * @param sourceRunId - run id of the triggering event
+ * @param selfRunId - our own run id
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether future duplicate cancelling is enabled
+ * @param jobNameRegexps - array of regular expression of job names
+ */
+function performSanityChecks(
+ eventName: string,
+ sourceRunId: number,
+ selfRunId: number,
+ cancelMode: CancelMode,
+ cancelFutureDuplicates: boolean,
+ jobNameRegexps: string[]
+): void {
+ if (
+ eventName === 'workflow_run' &&
+ sourceRunId === selfRunId &&
+ cancelMode === CancelMode.DUPLICATES
+ ) {
+ throw Error(
+ `You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
+ 'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
+ 'See the docs for details.'
+ )
+ }
+
+ if (
+ jobNameRegexps.length > 0 &&
+ [
+ CancelMode.DUPLICATES,
+ CancelMode.SELF,
+ CancelMode.ALL_DUPLICATES
+ ].includes(cancelMode)
+ ) {
+ throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`)
+ }
+
+ if (cancelMode === CancelMode.ALL_DUPLICATES && !cancelFutureDuplicates) {
+ throw Error(
+ `The ${cancelMode} cancelMode has to have cancelFutureDuplicates set to true.`
+ )
+ }
+}
+
+/**
+ * Produces basic outputs for the action. This does not include cancelled workflow run id - those are
+ * set after cancelling is done.
+ *
+ * @param triggeringWorkflowInfo
+ */
+function produceBasicOutputs(
+ triggeringWorkflowInfo: TriggeringWorkflowInfo
+): void {
+ verboseOutput('sourceHeadRepo', triggeringWorkflowInfo.headRepo)
+ verboseOutput('sourceHeadBranch', triggeringWorkflowInfo.headBranch)
+ verboseOutput('sourceHeadSha', triggeringWorkflowInfo.headSha)
+ verboseOutput('sourceEvent', triggeringWorkflowInfo.eventName)
+ verboseOutput(
+ 'pullRequestNumber',
+ triggeringWorkflowInfo.pullRequest
+ ? triggeringWorkflowInfo.pullRequest.number.toString()
+ : ''
+ )
+ verboseOutput(
+ 'mergeCommitSha',
+ triggeringWorkflowInfo.mergeCommitSha
+ ? triggeringWorkflowInfo.mergeCommitSha
+ : ''
+ )
+ verboseOutput(
+ 'targetCommitSha',
+ triggeringWorkflowInfo.mergeCommitSha
+ ? triggeringWorkflowInfo.mergeCommitSha
+ : triggeringWorkflowInfo.headSha
+ )
+}
+
+/**
+ * Notifies the PR that the action has started.
+ *
+ * @param repositoryInfo information about the repository
+ * @param triggeringWorkflowInfo information about the triggering workflow
+ * @param sourceRunId run id of the source workflow
+ * @param selfRunId self run id
+ * @param notifyPRMessageStart whether to notify about the start of the action
+ */
+async function notifyActionStart(
+ repositoryInfo: RepositoryInfo,
+ triggeringWorkflowInfo: TriggeringWorkflowInfo,
+ sourceRunId: number,
+ selfRunId: number,
+ notifyPRMessageStart: string
+): Promise<void> {
+ if (notifyPRMessageStart && triggeringWorkflowInfo.pullRequest) {
+ const selfWorkflowRunUrl =
+ `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+ `/actions/runs/${selfRunId}`
+ await repositoryInfo.octokit.issues.createComment({
+ owner: repositoryInfo.owner,
+ repo: repositoryInfo.repo,
+ // eslint-disable-next-line @typescript-eslint/camelcase
+ issue_number: triggeringWorkflowInfo.pullRequest.number,
+ body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
+ })
+ }
+}
+
+/**
+ * Main run method that does everything :)
+ */
async function run(): Promise<void> {
const token = core.getInput('token', {required: true})
const octokit = new github.GitHub(token)
@@ -699,33 +1299,31 @@ async function run(): Promise<void> {
const [owner, repo] = repository.split('/')
+ const repositoryInfo: RepositoryInfo = {
+ octokit,
+ owner,
+ repo
+ }
core.info(
`\nGetting workflow id for source run id: ${sourceRunId}, owner: ${owner}, repo: ${repo},` +
` skipEventTypes: ${skipEventTypes}\n`
)
- let sourceWorkflowId
+ const sourceWorkflowId = await retrieveWorkflowId(
+ repositoryInfo,
+ workflowFileName,
+ sourceRunId,
+ selfRunId
+ )
+
+ performSanityChecks(
+ eventName,
+ sourceRunId,
+ selfRunId,
+ cancelMode,
+ cancelFutureDuplicates,
+ jobNameRegexps
+ )
- if (workflowFileName) {
- sourceWorkflowId = workflowFileName
- core.info(
- `\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`
- )
- } else {
- sourceWorkflowId = await getWorkflowId(octokit, sourceRunId, owner, repo)
- if (sourceRunId === selfRunId) {
- core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`)
- } else {
- core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`)
- }
- if (eventName === 'workflow_run' && sourceRunId === selfRunId) {
- if (cancelMode === CancelMode.DUPLICATES)
- throw Error(
- `You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
- 'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
- 'See the docs for details.'
- )
- }
- }
core.info(
`Repository: ${repository}, Owner: ${owner}, Repo: ${repo}, ` +
`Event name: ${eventName}, CancelMode: ${cancelMode}, ` +
@@ -733,54 +1331,25 @@ async function run(): Promise<void> {
`jobNames: ${jobNameRegexps}`
)
- if (
- jobNameRegexps.length > 0 &&
- [CancelMode.DUPLICATES, CancelMode.SELF].includes(cancelMode)
- ) {
- throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`)
- }
+ const triggeringWorkflowInfo = await getOrigin(repositoryInfo, sourceRunId)
+ produceBasicOutputs(triggeringWorkflowInfo)
- const [
- headRepo,
- headBranch,
- sourceEventName,
- headSha,
- mergeCommitSha,
- pullRequest
- ] = await getOrigin(octokit, sourceRunId, owner, repo)
-
- verboseOutput('sourceHeadRepo', headRepo)
- verboseOutput('sourceHeadBranch', headBranch)
- verboseOutput('sourceHeadSha', headSha)
- verboseOutput('sourceEvent', sourceEventName)
- verboseOutput(
- 'pullRequestNumber',
- pullRequest ? pullRequest.number.toString() : ''
+ await notifyActionStart(
+ repositoryInfo,
+ triggeringWorkflowInfo,
+ sourceRunId,
+ selfRunId,
+ notifyPRMessageStart
)
- verboseOutput('mergeCommitSha', mergeCommitSha)
- verboseOutput('targetCommitSha', pullRequest ? mergeCommitSha : headSha)
-
- const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`
- if (notifyPRMessageStart && pullRequest) {
- await octokit.issues.createComment({
- owner,
- repo,
- // eslint-disable-next-line @typescript-eslint/camelcase
- issue_number: pullRequest.number,
- body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
- })
- }
const cancelledRuns = await performCancelJob(
- octokit,
+ repositoryInfo,
selfRunId,
sourceWorkflowId,
sourceRunId,
- owner,
- repo,
- headRepo,
- headBranch,
- sourceEventName,
+ triggeringWorkflowInfo.headRepo,
+ triggeringWorkflowInfo.headBranch,
+ triggeringWorkflowInfo.eventName,
cancelMode,
notifyPRCancel,
notifyPRCancelMessage,