{"id":18952313,"url":"https://github.com/step-security/skip-duplicate-actions","last_synced_at":"2025-11-09T11:30:52.341Z","repository":{"id":207278630,"uuid":"718855585","full_name":"step-security/skip-duplicate-actions","owner":"step-security","description":"Save time and cost when using GitHub Actions","archived":false,"fork":false,"pushed_at":"2024-04-19T10:42:45.000Z","size":387,"stargazers_count":0,"open_issues_count":10,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-19T12:06:58.965Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/step-security.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2023-11-14T23:36:33.000Z","updated_at":"2024-07-17T20:22:20.941Z","dependencies_parsed_at":"2023-11-15T01:29:04.270Z","dependency_job_id":"dbebf712-8b85-4933-8095-7ef9e09229f4","html_url":"https://github.com/step-security/skip-duplicate-actions","commit_stats":null,"previous_names":["step-security/skip-duplicate-actions"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/step-security%2Fskip-duplicate-actions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/step-security%2Fskip-duplicate-actions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/step-security%2Fskip-duplicate-actions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/step-security%2Fskip-duplicate-actions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/step-security","download_url":"https://codeload.github.com/step-security/skip-duplicate-actions/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239951647,"owners_count":19723913,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T13:32:44.279Z","updated_at":"2025-11-09T11:30:52.332Z","avatar_url":"https://github.com/step-security.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Skip Duplicate Actions\n\n`skip-duplicate-actions` provides the following features to optimize GitHub Actions:\n\n- [Skip duplicate workflow runs](#skip-duplicate-workflow-runs) after merges, pull requests or similar.\n- [Skip concurrent or parallel workflow runs](#skip-concurrent-workflow-runs) for things that you do not want to run twice.\n- [Skip ignored paths](#skip-ignored-paths) to speedup documentation-changes or similar.\n- [Skip if paths not changed](#skip-if-paths-not-changed) for something like directory-specific tests.\n- [Cancel outdated workflow runs](#cancel-outdated-workflow-runs) after branch-pushes.\n\nAll of those features help to save time and costs; especially for long-running workflows.\nYou can choose any subset of those features.\n\n## Skip duplicate workflow runs\n\nIf you work with feature branches, then you might see lots of _duplicate workflow runs_.\nFor example, duplicate workflow runs can happen if a workflow run is performed on a feature branch, but then the workflow run is repeated right after merging the feature branch.\n`skip-duplicate-actions` allows to prevent such runs.\n\n- **Full traceability:** After clean merges, you will see a message like `Skip execution because the exact same files have been successfully checked in \u003cprevious_run_URL\u003e`.\n- **Fully configurable:** By default, manual triggers and cron will never be skipped.\n- **Flexible Git usage:** `skip-duplicate-actions` does not care whether you use fast-forward-merges, rebase-merges or squash-merges.\n  However, if a merge yields a result that is different from the source branch, then the resulting workflow run will _not_ be skipped.\n  This is commonly the case if you merge \"outdated branches\".\n\n## Skip concurrent workflow runs\n\nSometimes, there are workflows that you do not want to run twice at the same time even if they are triggered twice.\nTherefore, `skip-duplicate-actions` provides the following options to skip a workflow run if the same workflow is already running:\n\n- **Always skip:** This is useful if you have a workflow that you never want to run twice at the same time.\n- **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.\n  (_Deprecated_, use `same_content_newer` instead)\n- **Only skip newer runs with the same content:** If the same workflow is running on the exact same content, skip newer runs of it. `same_content_newer` ensures that at least one of those workflows will run, while `same_content` may skip all of them.\n- **Only skip outdated runs:** For example, this can be useful for skip-checks that are not at the beginning of a job.\n- **Never skip:** This disables the concurrent skipping functionality, but still lets you use all other options like duplicate skipping.\n\n## Skip ignored paths\n\nIn many projects, it is unnecessary to run all tests for documentation-only-changes.\nTherefore, GitHub provides a [paths-ignore](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore) feature out of the box.\nHowever, GitHub's `paths-ignore` has some limitations:\n\n- GitHub's `paths-ignore` fails to look at previous commits. This means that the outcome depends on how often you push changes.\n- Consequently, GitHub's `paths-ignore` does not work for [required checks](https://docs.github.com/en/github/administering-a-repository/about-required-status-checks).\n  If you path-ignore a required check, then pull requests will block forever without being mergeable.\n\nTo overcome those limitations, `skip-duplicate-actions` provides a more flexible `paths_ignore`-feature with an efficient backtracking-algorithm.\nInstead of stupidly looking at the current commit, `paths_ignore` will look for successful checks in your commit-history.\n\nYou can use the [`paths_filter`](#paths_filter) option if you need to define multiple `paths_ignore` patterns in a single workflow.\n\n## Skip if paths not changed\n\nIn some projects, there are tasks that should be only executed if specific sub-directories were changed.\nTherefore, GitHub provides a [paths](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onpushpull_requestpaths) feature out of the box.\nHowever, GitHub's `paths` has some limitations:\n\n- GitHub's `paths` cannot skip individual steps in a workflow.\n- GitHub's `paths` does not work with required checks that you really want to pass successfully.\n\nTo overcome those limitations, `skip-duplicate-actions` provides a more sophisticated `paths`-feature.\nInstead of blindly skipping checks, the backtracking-algorithm will only skip something if it can find a suitable check in your commit-history.\n\nYou can use the [`paths_filter`](#paths_filter) option if you need to define multiple `paths` patterns in a single workflow.\n\n## Cancel outdated workflow runs\n\nTypically, workflows should only run for the most recent commit.\nTherefore, when you push changes to a branch, `skip-duplicate-actions` can be configured to cancel any previous workflow runs that run against outdated commits.\n\n- **Full traceability:** If a workflow run is cancelled, then you will see a message like `Cancelled \u003cprevious_run_URL\u003e`.\n- **Guaranteed execution:** The cancellation-algorithm guarantees that a complete check-set will finish no matter what.\n\n## Inputs\n\n### `paths_ignore`\n\nA JSON-array with ignored path patterns.\nSee [cheat sheet](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet) for path-pattern examples.\nSee [micromatch](https://github.com/micromatch/micromatch) for details about supported path-patterns.\n\n**Example:** `'[\"**/README.md\", \"**/docs/**\"]'`\n\n**Default:** `'[]'`\n\n### `paths`\n\nA JSON-array with path patterns.\nIf this is non-empty, then `skip-duplicate-actions` will try to skip commits that did not change any of those paths.\nIt uses the same syntax as [`paths_ignore`](#paths_ignore).\n\n**Example:** `'[\"platform-specific/**\"]'`\n\n**Default:** `'[]'`\n\n### `paths_filter`\n\nA YAML-string with named [`paths_ignore`](#paths_ignore) / [`paths`](#paths) patterns.\n\n**Example:**\n\n```yaml\nfrontend:\n  paths_ignore:\n    - 'frontend/docs/**'\n  paths:\n    - 'frontend/**'\nbackend:\n  paths:\n    - 'backend/**'\n  ### Here you can optionally control/limit backtracking\n  # Boolean or number (default: true)\n  # 'false' means disable backtracking completely\n  # '5' means to stop after having traced back 5 commits\n  backtracking: 5\n```\n\nUseful if you have multiple jobs in one workflow and want to skip them based on different [`paths_ignore`](#paths_ignore) / [`paths`](#paths) patterns.\nSee the corresponding [`paths_result`](#paths_result) output and [example configuration](#example-3-skip-using-paths_filter).\n\n### `cancel_others`\n\nIf true, then workflow runs from outdated commits will be cancelled.\n\n**Default:** `'false'`\n\n### `skip_after_successful_duplicate`\n\nIf true, skip if an already finished duplicate run can be found.\n\n**Default:** `'true'`\n\n### `do_not_skip`\n\nA JSON-array with triggers that should never be skipped.\n\nPossible values are `pull_request`, `push`, `workflow_dispatch`, `schedule`, `release`, `merge_group`.\n\n**Default:** `'[\"workflow_dispatch\", \"schedule\", \"merge_group\"]'`\n\n### `concurrent_skipping`\n\nSkip a workflow run if the same workflow is already running.\n\nOne of `never`, `same_content`, `same_content_newer`, `outdated_runs`, `always`.\n\n**Default:** `'never'`\n\n## Outputs\n\n### `should_skip`\n\nReturns `'true'` if the current run should be skipped according to your configured rules. This should be evaluated for either individual steps or entire jobs.\n\n### `reason`\n\nThe reason why the current run is considered skippable or unskippable. Corresponds approximately to the input options.\n\n**Example:** `skip_after_successful_duplicate`\n\n### `skipped_by`\n\nReturns information about the workflow run which caused the current run to be skipped.\n\n**Example:**\n\n```json\n{\n  \"id\": 1709469369,\n  \"runNumber\": 737,\n  \"event\": \"pull_request\",\n  \"treeHash\": \"e3434bb7aeb3047d7df948f09419ac96cf03d73e\",\n  \"commitHash\": \"4a0432e823468ecff81a978165cb35586544c795\",\n  \"status\": \"completed\",\n  \"conclusion\": \"success\",\n  \"htmlUrl\": \"https://github.com/owner/repo/actions/runs/1709469369\",\n  \"branch\": \"master\",\n  \"repo\": \"owner/repo\",\n  \"workflowId\": 2640563,\n  \"createdAt\": \"2022-01-17T18:56:06Z\"\n}\n```\n\n- Returns information only when current run is considered skippable, otherwise an empty object (`{}`) is returned.\n\n### `paths_result`\n\nReturns information for each configured filter in `paths_filter`.\n\n**Example:**\n\n```jsonc\n{\n  \"frontend\": {\n    \"should_skip\": true,\n    \"backtrack_count\": 1,\n    \"skipped_by\": {\n      // Information about the workflow run\n    },\n  \"backend\": {\n    \"should_skip\": false,\n    \"backtrack_count\": 1,\n    \"matched_files\": [\"backend/file.txt\"]\n  },\n  \"global\": {\n    \"should_skip\": false,\n    \"backtrack_count\": 0\n  }\n}\n```\n\n- The `global` key corresponds to the \"global\" [`paths_ignore`](#paths_ignore) and [`paths`](#paths) options.\n- A list of matched files is returned in `matched_files`, if there are any.\n- The `skipped_by` return value behaves the same as the \"global\" [`skipped_by`](#skipped_by) output.\n- The `backtrack_count` shows how many commits where traced back (skipped) until an appropriate commit has been found.\n- If `skip-duplicate-actions` terminates before the paths checks are performed (for example, when a successful duplicate run has been found) an empty object is returned (`{}`).\n\n### `changed_files`\n\nA two-dimensional array, with a list of changed files for each commit that has been traced back.\n\n**Example:** `[[\"some/example/file.txt\", \"another/example/file.txt\"], [\"frontend/file.txt\"]]`\n\n- Having a two-dimensional list makes processing flexible. For example, one can flatten (and uniquify) the list to get changed files from all commits which were traced back. Or one can use `changed_files[0]` to get changed files from the latest commit. One might also use the output of `backtrack_count` from [`paths_result`](#paths_result) to process the list of changed files.\n- Returns information only if one of the options [`paths_ignore`](#paths_ignore), [`paths`](#paths), [`paths_filter`](#paths_filter) is set.\n- If `skip-duplicate-actions` terminates before the paths checks are performed (for example, when a successful duplicate run has been found) an empty array (`[]`) is returned.\n\n## Usage examples\n\nYou can use `skip-duplicate-actions` to either skip individual steps or entire jobs.\nTo minimize changes to existing jobs, it is often easier to skip entire jobs.\n\n\u003e **Note**\n\u003e\n\u003e - You may need to use [`fromJSON`](https://docs.github.com/en/actions/learn-github-actions/expressions#fromjson) to access properties of object outputs. For example, for `skipped_by.id`, you can use the expression: `${{ fromJSON(steps.skip_check.outputs.skipped_by).id }}`.\n\u003e - For GitHub repositories where [default permissions](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#setting-the-permissions-of-the-github_token-for-your-repository) for `GITHUB_TOKEN` has been set to \"permissive (read-only)\", the following lines must be included in the workflow (see [permissions syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions)):\n\u003e\n\u003e   ```yaml\n\u003e   # Minimum permissions required by skip-duplicate-actions\n\u003e   permissions:\n\u003e     actions: write\n\u003e     contents: read\n\u003e   ```\n\n### Example 1: Skip entire jobs\n\nTo skip entire jobs, you should add a `pre_job` that acts as a pre-condition for your `main_job`.\nAlthough this example looks like a lot of code, there are only two additional lines in your project-specific `main_job` (the `needs`-clause and the `if`-clause):\n\n```yml\njobs:\n  pre_job:\n    # continue-on-error: true # Uncomment once integration is finished\n    runs-on: ubuntu-latest\n    # Map a step output to a job output\n    outputs:\n      should_skip: ${{ steps.skip_check.outputs.should_skip }}\n    steps:\n      - id: skip_check\n        uses: step-security/skip-duplicate-actions@v5\n        with:\n          # All of these options are optional, so you can remove them if you are happy with the defaults\n          concurrent_skipping: 'never'\n          skip_after_successful_duplicate: 'true'\n          paths_ignore: '[\"**/README.md\", \"**/docs/**\"]'\n          do_not_skip: '[\"pull_request\", \"workflow_dispatch\", \"schedule\"]'\n\n  main_job:\n    needs: pre_job\n    if: needs.pre_job.outputs.should_skip != 'true'\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo \"Running slow tests...\" \u0026\u0026 sleep 30\n```\n\n### Example 2: Skip individual steps\n\nThe following example demonstrates how to skip an individual step with an `if`-clause and an `id`.\nIn this example, the step will be skipped if no files in `src/` or `dist/` were changed:\n\n```yml\njobs:\n  skip_individual_steps_job:\n    runs-on: ubuntu-latest\n    steps:\n      - id: skip_check\n        uses: step-security/skip-duplicate-actions@v5\n        with:\n          cancel_others: 'false'\n          paths: '[\"src/**\", \"dist/**\"]'\n      - if: steps.skip_check.outputs.should_skip != 'true'\n        run: |\n          echo \"Run only if src/ or dist/ changed...\" \u0026\u0026 sleep 30\n          echo \"Do other stuff...\"\n```\n\n### Example 3: Skip using `paths_filter`\n\nThe `paths_filter` option can be used if you have multiple jobs in a workflow and want to skip them based on different [`paths_ignore`](#paths_ignore) / [`paths`](#paths) patterns. When defining such filters, the action returns corresponding information in the [`paths_result`](#paths_result) output.\nFor example in a monorepo, you might want to run jobs related to the \"frontend\" only if some files in the corresponding \"frontend/\" folder have changed and the same for \"backend\". This can be achieved with the following configuration:\n\n```yml\njobs:\n  pre_job:\n    runs-on: ubuntu-latest\n    outputs:\n      should_skip: ${{ steps.skip_check.outputs.should_skip }}\n      paths_result: ${{ steps.skip_check.outputs.paths_result }}\n    steps:\n      - id: skip_check\n        uses: step-security/skip-duplicate-actions@v5\n        with:\n          paths_filter: |\n            frontend:\n              paths_ignore:\n                - 'frontend/docs/**'\n              paths:\n                - 'frontend/**'\n            backend:\n              paths:\n                - 'backend/**'\n          # Can be mixed with the \"global\" 'paths_ignore' / 'paths' options, for example:\n          # paths_ignore: '[\"**/README.md\"]'\n\n  frontend:\n    needs: pre_job\n    # If 'skip-duplicate-actions' terminates before the paths checks are performed (for example, when a successful duplicate run has\n    # been found) 'paths_result' outputs an empty object ('{}'). This can be easily intercepted in the if condition of a job\n    # by checking the result of the \"global\" 'should_skip' output first.\n    if: needs.pre_job.outputs.should_skip != 'true' || !fromJSON(needs.pre_job.outputs.paths_result).frontend.should_skip\n    # ...\n\n  backend:\n    # ...\n```\n\n## How does it work?\n\n`skip-duplicate-actions` uses the [Workflow Runs API](https://docs.github.com/en/rest/reference/actions#workflow-runs) to query workflow runs.\n`skip-duplicate-actions` will only look at workflow runs that belong to the same workflow as the current workflow run.\nAfter querying such workflow runs, it will compare them with the current workflow run as follows:\n\n- If there exists a workflow run with the same tree hash, then we have identified a duplicate workflow run.\n- If there exists an in-progress workflow run, then we can cancel it or skip, depending on your configuration.\n\n## How does path-skipping work?\n\nAs mentioned above, `skip-duplicate-actions` provides a path-skipping functionality that is somewhat similar to GitHub's native `paths` and `paths_ignore` functionality.\nHowever, path-skipping is not entirely trivial because there exist multiple options on how to do path-skipping:\n\n### Option 1: Only look at the \"current\" commit\n\nThis is the thing that GitHub is currently doing, and we consider it as insufficient because it does not work for \"required\" checks.\nAnother problem is that the outcomes can be heavily dependent on the pushing-sequence of commits.\n\n### Option 2: Look at Pull-Request-diffs\n\nPR-diffs are simple to understand, but they only work after opening a PR, not immediately after pushing a feature-branch.\n\n### Option 3: Look for successful checks of previous commits\n\nThis option is implemented by `skip-duplicate-actions`.\nAn advantage is that this works regardless of whether you are using PRs or raw feature-branches, and of course it also works for \"required\" checks.\nInternally, `skip-duplicate-actions` uses the [Repos Commit API](https://docs.github.com/en/rest/reference/repos#get-a-commit) to perform an efficient backtracking-algorithm for paths-skipping-detection.\n\nThis is how the algorithm works approximately:\n\n```mermaid\nstateDiagram-v2\n    Check_Commit: Check Commit\n    [*] --\u003e Check_Commit: Current commit\n\n    state Path_Ignored \u003c\u003cchoice\u003e\u003e\n    Check_Commit --\u003e Path_Ignored: Do all changed files match against \"paths_ignore\"?\n    Ignored_Yes: Yes\n    Ignored_No: No\n    Path_Ignored --\u003e Ignored_Yes\n    Path_Ignored --\u003e Ignored_No\n\n    state Path_Skipped \u003c\u003cchoice\u003e\u003e\n    Ignored_No --\u003e Path_Skipped: Do none of the changed files match against \"paths\"?\n    Skipped_Yes: Yes\n    Skipped_No: No\n    Path_Skipped --\u003e Skipped_Yes: No matches\n    Path_Skipped --\u003e Skipped_No: Some matches\n\n    Parent_Commit: Fetch Parent Commit\n    Ignored_Yes --\u003e Parent_Commit\n    Skipped_Yes --\u003e Parent_Commit\n\n    state Successful_Run \u003c\u003cchoice\u003e\u003e\n    Parent_Commit --\u003e Successful_Run: Is there a successful run for this commit?\n    Run_Yes: Yes\n    Run_No: No\n    Successful_Run --\u003e Run_Yes\n    Successful_Run --\u003e Run_No\n\n    Run_No --\u003e Check_Commit: Parent commit\n\n    Skip: Skip!\n    Run_Yes --\u003e Skip: (Because all changes since this run are in ignored or skipped paths)\n\n    Dont_Skip: Don't Skip!\n    Skipped_No --\u003e Dont_Skip: (Because changed files needs to be \"tested\")\n```\n\n## Frequently Asked Questions\n\n### How to Use Skip Check With Required Matrix Jobs?\n\nIf you have matrix jobs that are registered as required status checks and the matrix runs conditionally based on the skip check, you might run into the problem that the pull request remains in a unmergable state forever because the jobs are not executed at all and thus not reported as skipped (`Expected - Waiting for status to be reported`).\n\nThere are several approaches to circumvent this problem:\n\n- Define the condition (`if`) in each step in the matrix job instead of a single condition on the job level\n- If you want the check to be considered successful only if all jobs in the matrix were successful, you can add a subsequent job whose only task is to report the final status of the matrix. Then you can register this final job as a required status check:\n\n  ```yaml\n  result:\n    name: Result\n    if: needs.pre_job.outputs.should_skip != 'true' \u0026\u0026 always()\n    runs-on: ubuntu-latest\n    needs:\n      - pre_job\n      - example-matrix-job\n    steps:\n      - name: Mark result as failed\n        if: needs.example-matrix-job.result != 'success'\n        run: exit 1\n  ```\n\n- Define an opposite workflow, as offically suggested by GitHub: [Handling skipped but required checks](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstep-security%2Fskip-duplicate-actions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstep-security%2Fskip-duplicate-actions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstep-security%2Fskip-duplicate-actions/lists"}