The management of concurrent workflow executions in continuous integration and deployment pipelines is a critical aspect of DevOps efficiency. Historically, developers relied on third-party actions to abort redundant jobs triggered by rapid commits or conflicting pull requests. However, the landscape of GitHub Actions has evolved significantly. While legacy solutions like styfle/cancel-workflow-action and GongT/cancel-previous-workflows provided essential functionality, GitHub has since introduced native concurrency controls that offer superior stability, security, and ease of configuration. Understanding the transition from these custom actions to native features, as well as the edge cases where custom actions remain necessary, is vital for optimizing resource usage and reducing queue times.
The Shift from Custom Actions to Native Concurrency
The primary recommendation for modern GitHub Actions workflows is to avoid installing custom actions for the sole purpose of canceling previous runs. GitHub has integrated this functionality directly into the platform via the concurrency property. This native approach is more robust because it is managed by the GitHub runner infrastructure rather than relying on API calls from within a running job, which can introduce latency or race conditions.
The native concurrency configuration allows administrators to define a group of related workflow runs. When a new run is triggered for that group, GitHub can automatically cancel any in-progress runs belonging to the same group. This is particularly useful for feature branches where only the latest commit's tests are relevant. The configuration typically involves defining a group identifier and a condition for cancellation.
yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
In this example, the group is defined by the workflow name and the reference (branch or tag). The cancel-in-progress condition is set to true for all references except refs/heads/main. This ensures that on the main branch, workflow runs are allowed to complete without interruption, preserving deployment integrity, while on feature branches, older runs are canceled to save resources. This native method eliminates the need for the styfle/cancel-workflow-action in most standard scenarios.
Legacy Custom Actions: Mechanics and Use Cases
Prior to the widespread adoption of native concurrency, custom actions such as styfle/cancel-workflow-action were the standard solution. These actions operate by querying the GitHub Workflow Runs API to identify previous runs that match the current branch but do not match the current SHA (commit hash). By capturing the current branch and SHA upon a git push, the action identifies in-progress or queued runs that are now obsolete and cancels them.
The action is designed to be placed as the first step in a workflow, often immediately after the checkout step. This placement allows the action to cancel itself on the next push, ensuring that only the latest run proceeds. The action targets runs with a status of queued or in_progress.
yaml
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action
- name: Run Tests
run: node test.js
Another variation, GongT/cancel-previous-workflows, offers the ability to delete previous runs entirely, including completed ones, through an environment variable DELETE. This can be useful for cleaning up workflow history, though it requires careful consideration due to the potential loss of audit trails.
yaml
name: Cancel
on: push
jobs:
cancel:
name: Cancel Previous Runs
runs-on: ubuntu-latest
steps:
- name: cancel running workflows
uses: GongT/cancel-previous-workflows@master
env:
GITHUB_TOKEN: ${{ github.token }}
DELETE: true
Despite their utility, these actions have significant limitations. They were created before the introduction of concurrency groups and are now largely considered a fallback option. The reliance on API calls means they are subject to rate limits and permission constraints, which native concurrency avoids.
Advanced Scenarios: Scheduled Workflows and Forked Pull Requests
While native concurrency handles most cases, there are advanced scenarios where custom actions or specific configurations are still required. One such scenario involves workflows triggered by workflow_run events or scheduled tasks. In these cases, a separate workflow can be created to cancel runs from other workflows. This is particularly useful when modifying every existing workflow to add concurrency groups is impractical.
A scheduled workflow can iterate through all branches and pull requests, limiting execution to one run per branch or PR. This approach ensures that resource consumption is capped, preventing runaway costs and queue congestion. However, this method is less immediate than native concurrency, as it operates on a cron interval rather than at the moment of trigger.
A critical limitation of custom actions arises with pull requests originating from forked repositories. Due to security restrictions, the GITHUB_TOKEN provided to workflows in forked source branches has read-only permissions. Canceling a workflow requires write access to the actions scope. Consequently, custom actions like styfle/cancel-workflow-action are a no-op in these environments when using the default token.
To address this, a workaround involves using a workflow_run event listener. This listener is triggered when a specific workflow is requested. By defining this listener in a separate file, such as .github/workflows/cancel.yml, and using the GITHUB_TOKEN from the base repository (which has write permissions), the action can cancel runs from the forked PR workflows.
yaml
name: Cancel
on:
workflow_run:
workflows: ["CI"]
types:
- requested
jobs:
cancel:
runs-on: ubuntu-latest
steps:
- uses: styfle/cancel-workflow-action
with:
workflow_id: ${{ github.event.workflow.id }}
This pattern ensures that even workflows from forks are managed correctly, bypassing the permission barrier inherent in the direct execution context.
Permissions and Security Considerations
Security is a paramount concern when managing workflow executions. By default, GitHub creates the GITHUB_TOKEN with a mix of read and write permissions. However, best practices dictate adopting a least-privilege model, where tokens have read-only permissions by default.
When using custom actions like styfle/cancel-workflow-action, explicit permission elevation is required because the action needs to write to the actions scope to cancel runs. This can be achieved by defining permissions at the job level. This approach ensures that only the specific job requiring cancellation capabilities has the necessary access, minimizing the attack surface.
yaml
jobs:
test:
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action
with:
access_token: ${{ github.token }}
This configuration is typical in environments with restrictive global access policies. It allows for granular control, ensuring that the cancellation logic does not inadvertently expose other parts of the repository to unnecessary write access. For native concurrency, no additional permissions are required, as the cancellation is handled internally by GitHub's infrastructure.
Granular Control: Status Filtering and Force Cancellation
Custom actions offer features that go beyond simple cancellation, providing granular control over which runs are terminated. For instance, the only_status parameter allows users to cancel only workflows in a specific state, such as waiting. This is useful in protected environments where workflows are waiting for manual approval. By targeting only the waiting state, administrators can clear out stale approval requests without interrupting active builds.
yaml
steps:
- uses: styfle/cancel-workflow-action
with:
only_status: 'waiting'
Additionally, the force_cancel option enables the action to bypass conditions that would otherwise prevent cancellation, such as always() conditions or jobs stuck in a "Waiting For Approval" state. This ensures that even stubborn or protected jobs can be terminated when necessary, though it should be used with caution to avoid disrupting critical deployment gates.
yaml
steps:
- uses: styfle/cancel-workflow-action
with:
force_cancel: true
The workflow_id parameter allows for targeted cancellation of specific workflows. It accepts a workflow ID (number), a workflow file name (string), or the value all to cancel all workflows running in the branch. This flexibility supports complex multi-workflow setups where different cancellation policies may apply to different stages of the pipeline.
yaml
steps:
- uses: styfle/cancel-workflow-action
with:
workflow_id: 479426
Conclusion
The evolution of workflow management in GitHub Actions reflects a broader trend toward native, infrastructure-level solutions. While custom actions like styfle/cancel-workflow-action provided a valuable stopgap, the native concurrency property now offers a more efficient, secure, and maintainable approach for most use cases. It eliminates the need for API calls, reduces complexity, and integrates seamlessly with GitHub's permission model.
However, custom actions remain relevant in specific edge cases, such as managing workflows from forked pull requests or implementing complex scheduling strategies. Understanding the mechanics of these actions, including their permission requirements and limitations, is essential for advanced DevOps engineers. By leveraging native concurrency for standard workflows and reserving custom actions for specialized scenarios, organizations can optimize their CI/CD pipelines for both performance and security. As GitHub continues to enhance its platform, the reliance on third-party cancellation actions is likely to diminish further, but the principles of resource management and least-privilege access will remain constant.