Skip to content

Commit

Permalink
V3: Rethink Concurrent Skipping (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
fkirc committed Dec 10, 2020
1 parent 0c0fd7d commit f05289c
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 78 deletions.
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Expand Up @@ -6,7 +6,6 @@ on:

jobs:
build:
needs: pre_job
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Expand Up @@ -16,6 +16,7 @@ jobs:
- id: skip_check
uses: fkirc/skip-duplicate-actions@master
with:
concurrent_skipping: 'never'
paths_ignore: '["**/README.md", "**/docs/**"]'

main_job:
Expand Down Expand Up @@ -53,8 +54,8 @@ jobs:
github_token: ${{ github.token }}
paths_ignore: '["**/*.md"]'
cancel_others: 'true'
concurrent_skipping: 'always'
do_not_skip: '["pull_request", "workflow_dispatch", "schedule"]'
concurrent_skipping: 'true'
- if: ${{ steps.skip_check.outputs.should_skip == 'false' }}
run: |
echo "Do stuff..." && sleep 30
5 changes: 3 additions & 2 deletions CHANGELOG.md
@@ -1,8 +1,9 @@
See https://github.com/marketplace/actions/skip-duplicate-actions for a list of non-breaking changes.

## 2.2
## Breaking changes from 2.X to 3

- Reduce GitHub token boilerplate
Set `concurrent_skipping` to one of the values that are described in the README,
or omit it if you do not want any concurrent skipping functionality.

## Breaking changes from 1.X to 2

Expand Down
22 changes: 14 additions & 8 deletions README.md
Expand Up @@ -3,6 +3,7 @@
`skip-duplicate-actions` provides the following features to optimize GitHub Actions:

- [Skip duplicate workflow-runs](#skip-duplicate-workflow-runs) after merges, pull requests or similar.
- [Skip concurrent or parallel workflow-runs](#skip-concurrent-workflow-runs) for things that you do not want to run twice.
- [Skip ignored paths](#skip-ignored-paths) to speedup documentation-changes or similar.
- [Skip if paths not changed](#skip-if-paths-not-changed) for something like directory-specific tests.
- [Cancel outdated workflow-runs](#cancel-outdated-workflow-runs) after branch-pushes.
Expand All @@ -14,16 +15,24 @@ You can choose any subset of those features.

If you work with feature branches, then you might see lots of _duplicate workflow-runs_.
For example, duplicate workflow-runs can happen if a workflow runs on a feature branch, but then the workflow is repeated right after merging the feature branch.
`skip-duplicate-actions` helps to prevent such unnecessary runs.
`skip-duplicate-actions` allows to prevent such runs.

- **Full traceability:** After clean merges, you will see a message like `Skip execution because the exact same files have been successfully checked in <previous_run_URL>`.
- **Skip concurrent workflow-runs:** If the same workflow is unnecessarily triggered twice, then one of the workflow-runs will be skipped.
For example, this can happen when you push a tag right after pushing a commit.
- **Fully configurable:** By default, manual triggers and cron will never be skipped.
- **Flexible Git usage:** `skip-duplicate-actions` does not care whether you use fast-forward-merges, rebase-merges or squash-merges.
However, if a merge yields a result that is different from the source branch, then the resulting workflow-run will _not_ be skipped.
This is commonly the case if you merge "outdated branches".

## Skip concurrent workflow-runs

Sometimes, there are workflows that you do not want to run twice at the same time even if they are triggered twice.
Therefore, `skip-duplicate-actions` provides the following options to skip a workflow-run if the same workflow is already running:

- **Always skip:** This is useful if you have a workflow that you never want to run twice at the same time.
In contrast to the [cancel_others](#cancel-outdated-workflow-runs) option, this option lets the older run finish.
- **Only skip same content:** For example, this can be useful if a workflow has both a `push` and a `pull_request` trigger, or if you push a tag right after pushing a commit.
- **Never skip:** This disables the concurrent skipping functionality, but still lets you use all other options like duplicate skipping.

## Skip ignored paths

In many projects, it is unnecessary to run all tests for documentation-only-changes.
Expand Down Expand Up @@ -59,10 +68,6 @@ Therefore, when you push changes to a branch, `skip-duplicate-actions` will canc

## Inputs

### `github_token`

A GitHub token that only needs to access the current repo. Default `github.token`.

### `paths_ignore`

A JSON-array with ignored path-patterns, e.g. something like `'["**/README.md", "**/docs/**"]'`.
Expand All @@ -87,7 +92,7 @@ A JSON-array with triggers that should never be skipped. Default `'["workflow_di

### `concurrent_skipping`

If false, unfinished workflow-runs will be safely ignored. Default `true`.
One of `never`, `same_content`, `always`. Default `never`.

## Outputs

Expand Down Expand Up @@ -117,6 +122,7 @@ jobs:
- id: skip_check
uses: fkirc/skip-duplicate-actions@master
with:
concurrent_skipping: 'never'
paths_ignore: '["**/README.md", "**/docs/**"]'

main_job:
Expand Down
6 changes: 3 additions & 3 deletions action.yml
Expand Up @@ -26,9 +26,9 @@ inputs:
required: false
default: '["workflow_dispatch", "schedule"]'
concurrent_skipping:
description: 'If false, unfinished workflow-runs will be safely ignored'
required: false
default: 'true'
description: 'One of never, same_content, always'
required: true
default: 'never'

outputs:
should_skip:
Expand Down
83 changes: 53 additions & 30 deletions dist/index.js
Expand Up @@ -9880,6 +9880,14 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__webpack_require__(2186));
const github = __importStar(__webpack_require__(5438));
const micromatch = __webpack_require__(6228);
const concurrentSkippingMap = {
"always": null,
"same_content": null,
"never": null,
};
function getConcurrentSkippingOptions() {
return Object.keys(concurrentSkippingMap);
}
function parseWorkflowRun(run) {
var _a, _b, _c;
const treeHash = (_a = run.head_commit) === null || _a === void 0 ? void 0 : _a.tree_id;
Expand Down Expand Up @@ -9949,13 +9957,13 @@ async function main() {
repoOwner,
repoName,
currentRun,
otherRuns: parseOlderRuns(data, currentRun),
olderRuns: parseOlderRuns(data, currentRun),
allRuns: parseAllRuns(data),
octokit,
pathsIgnore: getStringArrayInput("paths_ignore"),
paths: getStringArrayInput("paths"),
doNotSkip: getStringArrayInput("do_not_skip"),
concurrentSkipping: getBooleanInput("concurrent_skipping", true),
concurrentSkipping: getConcurrentSkippingInput("concurrent_skipping"),
};
}
catch (e) {
Expand All @@ -9971,10 +9979,8 @@ async function main() {
core.info(`Do not skip execution because the workflow was triggered with '${context.currentRun.event}'`);
exitSuccess({ shouldSkip: false });
}
detectDuplicateRuns(context);
if (context.doNotSkip.includes("pull_request") || context.doNotSkip.includes("push")) {
detectExplicitConcurrentTrigger(context);
}
detectSuccessfulDuplicateRuns(context);
detectConcurrentRuns(context);
if (context.paths.length >= 1 || context.pathsIgnore.length >= 1) {
await backtracePathSkipping(context);
}
Expand All @@ -9983,7 +9989,7 @@ async function main() {
}
async function cancelOutdatedRuns(context) {
const currentRun = context.currentRun;
const cancelVictims = context.otherRuns.filter((run) => {
const cancelVictims = context.olderRuns.filter((run) => {
if (run.status === 'completed') {
return false;
}
Expand All @@ -10010,47 +10016,45 @@ async function cancelWorkflowRun(run, context) {
core.warning(`Failed to cancel ${run.html_url}`);
}
}
function detectDuplicateRuns(context) {
const duplicateRuns = context.otherRuns.filter((run) => run.treeHash === context.currentRun.treeHash);
function detectSuccessfulDuplicateRuns(context) {
const duplicateRuns = context.olderRuns.filter((run) => run.treeHash === context.currentRun.treeHash);
const successfulDuplicate = duplicateRuns.find((run) => {
return run.status === 'completed' && run.conclusion === 'success';
});
if (successfulDuplicate) {
core.info(`Skip execution because the exact same files have been successfully checked in ${successfulDuplicate.html_url}`);
exitSuccess({ shouldSkip: true });
}
const concurrentDuplicate = duplicateRuns.find((run) => {
if (run.status === 'completed') {
return false;
}
if (context.currentRun.branch && context.currentRun.branch !== run.branch) {
core.info(`The exact same files are concurrently checked on a different branch in ${run.html_url}`);
return false;
}
return true;
});
if (concurrentDuplicate && context.concurrentSkipping) {
core.info(`Skip execution because the exact same files are concurrently checked in ${concurrentDuplicate.html_url}`);
exitSuccess({ shouldSkip: true });
}
}
function detectExplicitConcurrentTrigger(context) {
if (!context.concurrentSkipping) {
function detectConcurrentRuns(context) {
if (context.concurrentSkipping === "never") {
return;
}
const duplicateTriggerRun = context.allRuns.find((run) => {
if (run.treeHash !== context.currentRun.treeHash) {
const concurrentRuns = context.allRuns.filter((run) => {
if (run.status === 'completed') {
return false;
}
if (run.runId === context.currentRun.runId) {
return false;
}
return true;
});
if (duplicateTriggerRun) {
core.info(`Skip execution because this is a '${context.currentRun.event}'-trigger and the exact same files are concurrently checked in ${duplicateTriggerRun.html_url}`);
if (!concurrentRuns.length) {
core.info(`Did not find any concurrent workflow-runs`);
return;
}
if (context.concurrentSkipping === "always") {
core.info(`Skip execution because another instance of the same workflow is already running in ${concurrentRuns[0].html_url}`);
exitSuccess({ shouldSkip: true });
}
const concurrentDuplicate = concurrentRuns.find((run) => run.treeHash === context.currentRun.treeHash);
if (concurrentDuplicate) {
core.info(`Skip execution because the exact same files are concurrently checked in ${concurrentDuplicate.html_url}`);
exitSuccess({ shouldSkip: true });
}
else {
core.info(`Did not find any duplicate concurrent workflow-runs`);
}
}
async function backtracePathSkipping(context) {
var _a, _b;
Expand All @@ -10072,7 +10076,7 @@ async function backtracePathSkipping(context) {
}
function exitIfSuccessfulRunExists(commit, context) {
const treeHash = commit.commit.tree.sha;
const matchingRuns = context.otherRuns.filter((run) => run.treeHash === treeHash);
const matchingRuns = context.olderRuns.filter((run) => run.treeHash === treeHash);
const successfulRun = matchingRuns.find((run) => {
return run.status === 'completed' && run.conclusion === 'success';
});
Expand Down Expand Up @@ -10134,6 +10138,25 @@ function exitSuccess(args) {
core.setOutput("should_skip", args.shouldSkip);
return process.exit(0);
}
function formatCliOptions(options) {
return `${options.map((o) => `"${o}"`).join(", ")}`;
}
function getConcurrentSkippingInput(name) {
const rawInput = core.getInput(name, { required: true });
if (rawInput.toLowerCase() === 'false') {
return "never";
}
else if (rawInput.toLowerCase() === 'true') {
return "same_content";
}
const options = getConcurrentSkippingOptions();
if (options.includes(rawInput)) {
return rawInput;
}
else {
logFatal(`'${name}' must be one of ${formatCliOptions(options)}`);
}
}
function getBooleanInput(name, defaultValue) {
const rawInput = core.getInput(name, { required: false });
if (!rawInput) {
Expand Down

0 comments on commit f05289c

Please sign in to comment.