Skip to content

Commit

Permalink
Add feature of cancelling future duplicates (enabled by default) (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
potiuk committed Oct 30, 2020
1 parent 0acb1c0 commit c8448eb
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 64 deletions.
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,18 @@ and `schedule` events are no longer needed.

## Inputs

| Input | Required | Default | Comment |
|-------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `token` | yes | | The github token passed from `${{ secrets.GITHUB_TOKEN }}` |
| `cancelMode` | no | `duplicates` | The mode to run cancel on. The available options are `duplicates`, `self`, `failedJobs`, `namedJobs` |
| `sourceRunId` | no | | Useful only in `workflow_run` triggered events. It should be set to the id of the workflow triggering the run `${{ github.event.workflow_run.id }}` in case cancel operation should cancel the source workflow. |
| `notifyPRCancel` | no | | Boolean. If set to true, it notifies the cancelled PRs with a comment containing reason why they are being cancelled. |
| `notifyPRCancelMessage` | no | | Optional cancel message to use instead of the default one when notifyPRCancel is true. It is only used in 'self' cancelling mode. |
| `notifyPRMessageStart` | no | | Only for workflow_run events triggered by the PRs. If not empty, it notifies those PRs with the message specified at the start of the workflow - adding the link to the triggered workflow_run. |
| `jobNameRegexps` | no | | An array of job name regexps. Only runs containing any job name matching any of of the regexp in this array are considered for cancelling in `failedJobs` and `namedJobs` cancel modes. |
| `skipEventTypes` | no | | Array of event names that should be skipped when cancelling (JSON-encoded string). This might be used in order to skip direct pushes or scheduled events. |
| `workflowFileName` | no | | Name of the workflow file. It can be used if you want to cancel a different workflow than yours. |
| Input | Required | Default | Comment |
|--------------------------|----------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `token` | yes | | The github token passed from `${{ secrets.GITHUB_TOKEN }}` |
| `cancelMode` | no | `duplicates` | The mode to run cancel on. The available options are `duplicates`, `self`, `failedJobs`, `namedJobs` |
| `cancelFutureDuplicates` | no | true | In case of duplicate canceling, cancel also future duplicates leaving only the "freshest" running job and not all the future jobs. By default it is set to true. |
| `sourceRunId` | no | | Useful only in `workflow_run` triggered events. It should be set to the id of the workflow triggering the run `${{ github.event.workflow_run.id }}` in case cancel operation should cancel the source workflow. |
| `notifyPRCancel` | no | | Boolean. If set to true, it notifies the cancelled PRs with a comment containing reason why they are being cancelled. |
| `notifyPRCancelMessage` | no | | Optional cancel message to use instead of the default one when notifyPRCancel is true. It is only used in 'self' cancelling mode. |
| `notifyPRMessageStart` | no | | Only for workflow_run events triggered by the PRs. If not empty, it notifies those PRs with the message specified at the start of the workflow - adding the link to the triggered workflow_run. |
| `jobNameRegexps` | no | | An array of job name regexps. Only runs containing any job name matching any of of the regexp in this array are considered for cancelling in `failedJobs` and `namedJobs` cancel modes. |
| `skipEventTypes` | no | | Array of event names that should be skipped when cancelling (JSON-encoded string). This might be used in order to skip direct pushes or scheduled events. |
| `workflowFileName` | no | | Name of the workflow file. It can be used if you want to cancel a different workflow than yours. |


The job cancel modes work as follows:
Expand Down Expand Up @@ -205,6 +206,7 @@ jobs:
name: "Cancel duplicate workflow runs"
with:
cancelMode: duplicates
cancelFutureDuplicates: true
token: ${{ secrets.GITHUB_TOKEN }}
sourceRunId: ${{ github.event.workflow_run.id }}
notifyPRCancel: true
Expand Down Expand Up @@ -266,6 +268,7 @@ jobs:
name: "Cancel duplicate CI runs"
with:
cancelMode: duplicates
cancelFutureDuplicates: true
token: ${{ secrets.GITHUB_TOKEN }}
notifyPRCancel: true
notifyPRMessageStart: |
Expand Down Expand Up @@ -512,6 +515,7 @@ on:
uses: potiuk/cancel-workflow-runs@v2
with:
cancelMode: duplicates
cancelFutureDuplicates: true
token: ${{ secrets.GITHUB_TOKEN }}
workflowFileName: other_workflow.yml
```
Expand Down Expand Up @@ -553,6 +557,7 @@ jobs:
name: "Cancel duplicate workflow runs"
with:
cancelMode: duplicates
cancelFutureDuplicates: true
notifyPRCancel: true
```

Expand Down
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ inputs:
* `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: |
In case of duplicate canceling, cancel also future duplicates leaving only the "freshest" running
job and not all the future jobs. By default it is set to true.
required: false
jobNameRegexps:
description: |
Array of job name regexps (JSON-encoded string). Used by `failedJobs` and `namedJobs` cancel modes
Expand Down
71 changes: 43 additions & 28 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1622,7 +1622,7 @@ function getWorkflowRuns(octokit, statusValues, cancelMode, createListRunQuery)
return workflowRuns;
});
}
function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, sourceRunId, jobNamesRegexps, skipEventTypes) {
function shouldBeCancelled(octokit, owner, repo, 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`);
Expand Down Expand Up @@ -1651,17 +1651,17 @@ function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode,
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} . Cancelling it.\n`);
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}. Not cancelling it.\n`);
core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`);
return false;
}
}
else if (cancelMode === CancelMode.SELF) {
if (runItem.id === sourceRunId) {
core.info(`\nCancelling the "source" run: ${runItem.id}.\n`);
core.info(`\nReturning the "source" run: ${runItem.id}.\n`);
return true;
}
else {
Expand All @@ -1675,16 +1675,20 @@ function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode,
`repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`);
return false;
}
if (runItem.id === sourceRunId) {
core.info(`\nThis is my own run ${runItem.id}. I have self-preservation mechanism. Not cancelling myself!\n`);
return false;
}
else if (runItem.id > sourceRunId) {
core.info(`\nThe run ${runItem.id} is started later than mt own run ${sourceRunId}. 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 {
core.info(`\nCancelling duplicate of my own run: ${runItem.id}.\n`);
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;
}
}
Expand All @@ -1710,7 +1714,7 @@ function cancelRun(octokit, owner, repo, runId) {
}
});
}
function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
return __awaiter(this, void 0, void 0, function* () {
const statusValues = ['queued', 'in_progress'];
const workflowRuns = yield getWorkflowRuns(octokit, statusValues, cancelMode, function (status) {
Expand All @@ -1735,29 +1739,39 @@ function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, ow
throw Error(`\nWrong cancel mode ${cancelMode}! This should never happen.\n`);
}
});
const idsToCancel = [];
const workflowsToCancel = [];
const pullRequestToNotify = [];
for (const [key, runItem] of workflowRuns) {
core.info(`\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status}\n`);
if (yield shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, sourceRunId, jobNameRegexps, skipEventTypes)) {
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);
}
}
idsToCancel.push(runItem.id);
workflowsToCancel.push([runItem.id, runItem.created_at]);
}
}
// Sort from smallest number - this way we always kill current one at the end (if we kill it at all)
const sortedIdsToCancel = idsToCancel.sort((id1, id2) => id1 - id2);
if (sortedIdsToCancel.length > 0) {
// 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: ${sortedIdsToCancel.length}\n` +
`\n Runs to cancel: ${sortedRunTuplesToCancel.length}\n` +
`\n PRs to notify: ${pullRequestToNotify.length}\n`);
for (const runId of sortedIdsToCancel) {
core.info(`\nCancelling run: ${runId}.\n`);
yield cancelRun(octokit, owner, repo, runId);
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}`;
Expand All @@ -1768,7 +1782,7 @@ function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, ow
else {
core.info('\n###### There are no runs to cancel! ##########\n');
}
return sortedIdsToCancel;
return sortedRunTuplesToCancel.map(runTuple => runTuple[0]);
});
}
function getRequiredEnv(key) {
Expand Down Expand Up @@ -1837,7 +1851,7 @@ function getOrigin(octokit, runId, owner, repo) {
];
});
}
function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes) {
function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, 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}, ` +
Expand Down Expand Up @@ -1867,7 +1881,7 @@ function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, own
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, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
return yield findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
});
}
function verboseOutput(name, value) {
Expand All @@ -1887,6 +1901,7 @@ function run() {
const notifyPRMessageStart = core.getInput('notifyPRMessageStart');
const sourceRunId = parseInt(core.getInput('sourceRunId')) || selfRunId;
const jobNameRegexpsString = core.getInput('jobNameRegexps');
const cancelFutureDuplicates = (core.getInput('cancelFutureDuplicates') || 'true').toLowerCase() === 'true';
const jobNameRegexps = jobNameRegexpsString
? JSON.parse(jobNameRegexpsString)
: [];
Expand Down Expand Up @@ -1944,7 +1959,7 @@ function run() {
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);
const cancelledRuns = yield performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates);
verboseOutput('cancelledRuns', JSON.stringify(cancelledRuns));
});
}
Expand Down

0 comments on commit c8448eb

Please sign in to comment.