Deprecated Patterns and Modern Strategies for Canceling GitHub Workflow Runs

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.

Sources

  1. styfle/cancel-workflow-action
  2. Usage of cancel-workflow-action GitHub Action
  3. Cancel Previous Workflow Runs
  4. Cancel Previous Runs of Current Workflow

Related Posts