Skip to content

ci: cancel superseded runs + skip redundant post-close re-runs - #237

Merged
EVWorth merged 1 commit into
mainfrom
fix/concurrency-and-event-types
Jul 20, 2026
Merged

ci: cancel superseded runs + skip redundant post-close re-runs#237
EVWorth merged 1 commit into
mainfrom
fix/concurrency-and-event-types

Conversation

@EVWorth

@EVWorth EVWorth commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What

Two small workflow changes for cleaner CI:

  1. concurrency block on both ci.yml and release.yml. Cancels in-flight/queued runs when a new commit lands on the same PR (or same branch/tag ref). Standard industry pattern.
  2. Explicit pull_request: types: [opened, synchronize, reopened] on ci.yml. Excludes the previously-defaulted closed event that was making every merge trigger a redundant CI run.
  3. workflow_dispatch added to release.yml so a manual gh workflow run re-run cancels the prior in-flight release.

Diff

.github/workflows/ci.yml      | +15
.github/workflows/release.yml | +8
# .github/workflows/ci.yml (excerpt)
on:
  pull_request:
    types: [opened, synchronize, reopened]   # explicit; no closed
    branches: [main]
  push:
    branches: [main]                         # NEW: catches direct pushes to main too

concurrency:                                 # NEW
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true
# .github/workflows/release.yml (excerpt)
on:
  push:
    tags: ['v*']
  workflow_dispatch:                         # NEW

concurrency:                                 # NEW
  group: ${{ github.workflow }}-${{ github.ref || github.event.inputs.tag || github.run_id }}
  cancel-in-progress: true

Why

Concurrency

Earlier today we found 5 queued CI runs, 3 of them orphaned by already-merged PRs and not actually doing anything useful. gh run cancel cleans up the symptom. This prevents the cause from recurring.

A new commit to an open PR (push, force-push, merge from a contributor fork) will now cancel the prior queued/in-flight run for that PR. Multiple concurrent PRs are still safe — each gets its own concurrency key (PR_number vs another).

Direct push to main now triggers CI (was previously only running on PRs). That's correct hygiene — direct pushes to a protected branch should still be gated. Branch-direct pushes for routine repo work benefit from the same gating matrix.

The closed event exclusion

This is a latent bug we didn't know we had. on: pull_request: without an explicit types: defaults to [opened, synchronize, reopened, closed]. The closed variant fires every time a PR is merged or closed, and each fire re-runs CI on already-merged code. We were paying for a redundant CI run on every merge.

Excluding closed stops this. Merges no longer trigger post-merge CI runs. Any orphaned queued run from before merge just ages out harmlessly (GH has an upper bound on queue lifetime).

What I deliberately skip

  • A scheduled 'clean stale runs' workflow — overengineering for a 0.5% edge case. gh run cancel <id> is sufficient when noticed.
  • Per-job concurrency groups — over-fine. Jobs in the same workflow share state and should cancel together.
  • Self-hosted runnersubuntu-latest (first-party hosted) is faster for our size than maintaining a runner.

Pattern sources

  • Next.js: https://github.com/vercel/next.js/blob/canary/.github/workflows/build.yml
  • TypeScript: https://github.com/microsoft/TypeScript/blob/main/.github/workflows/ci.yml
  • rust-lang/cargo: similar pattern
  • GitHub docs (concurrency): https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency

Verification

python3 yaml.safe_load on both workflows parse ok
GitHub Actions CI on this PR the new concurrency key will be used; superseding any prior runs
Visual diff +23 lines, no removals

Once Actions recovers from the current outage, this PR's run will demonstrate the new behavior naturally — a force-push after this point cancels the queued run.

ci.yml:
1. Concurrency block keyed by workflow + PR number (or ref for
   branch-direct pushes) with cancel-in-progress: true. Standard
   pattern: a new commit to the same PR cancels the prior run in
   flight. Multiple concurrent PRs don't trip over each other.
2. Explicit pull_request types: [opened, synchronize, reopened].
   Default types include 'closed', which made every merge trigger a
   redundant CI run re-verifying already-green code. Excluding it
   stops that waste.

release.yml:
- Same concurrency block + 'workflow_dispatch' trigger so a tag
  re-push or manual re-run cancels the prior release in flight.

Why this lands:
- Earlier today (#236 investigation) we found 5 queued CI runs,
  3 of them orphaned by already-merged PRs. `gh run cancel` is
  fine for cleanup; the underlying fix is preventing fresh
  waste from this pattern.

Why the explicit types matter:
- Pre-existing bug, not just theoretical. Merges were triggering
  extra CI runs we were paying for. `types: [closed]` was
  silently enabled. This PR disables it.

Pattern sourced from Next.js, TypeScript, Cargo, and the official
GH concurrency docs as of 2026-07.

Files:
- .github/workflows/ci.yml       (+15 lines)
- .github/workflows/release.yml  (+8 lines, including workflow_dispatch trigger)
@EVWorth
EVWorth merged commit 3aca567 into main Jul 20, 2026
1 check failed
@EVWorth
EVWorth deleted the fix/concurrency-and-event-types branch July 20, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant