Push run on main is cancelled when a second merge lands seconds later #26

Closed
opened 2026-11-13 14:10:16 +01:00 by omp-deck · 2 comments
Collaborator

Problem

The Build / build (push) check on merge commit 530bd777ae041662a64195eb7254beca641f5ff7 (merge of #23) is red with "Has been cancelled" — run #19, API run 16494. The job did not fail: it was cancelled 17s in, mid apt-get install, with no failing step and the runner reporting context canceled:

2026-11-13T13:07:26.9972313Z Processing triggers for libc-bin (2.41-12+deb13u4) ...
2026-11-13T13:07:27.1976539Z ⚙️ [runner]: context canceled …
2026-11-13T13:07:27.5752474Z 🏁  Job failed

Two pull requests were merged into main within seconds of each other (#23 → 530bd77, then #22 → 2669dcd): run #19 (530bd77) was cancelled at 13:07:27.2, run #20 (2669dcd) reached the same runner at 13:07:28.6 and finished green. The first main run was superseded by the second. Nothing in the tree caused it — .forgejo/workflows/build.yml installs its dependencies with explicit apt-get and never reads mise.toml, the only file #23 touched.

The cancellation is Forgejo's default concurrency management, not a repository setting: with no concurrency key, a new push / pull_request (synchronize) event cancels any other invocation of the same workflow (Forgejo Actions reference, concurrency). The consequence is that an intermediate main commit keeps a permanently red check for content that was never broken, while the proof that the content is fine sits one commit later.

Desired behaviour: a push run on main is never cancelled — every main commit keeps a verdict that is not retracted. Pull-request runs keep superseding each other, since an obsolete PR commit's result is not interesting and the runner's time is better spent on the newest one.

Plan

Context

Add a workflow-level concurrency setting to .forgejo/workflows/build.yml that disables Forgejo's automatic cancellation for push events while keeping it for pull_request events. This is the repository's only workflow and its only job. No other behaviour of the workflow changes; nothing else in the repository is touched.

Approach

Single change, in .forgejo/workflows/build.yml: insert a top-level concurrency block between the on: block (currently lines 3-6) and jobs: (currently line 8), so the file head reads exactly:

name: Build

on:
  push:
    branches: ["main"]
  pull_request:

concurrency:
  cancel-in-progress: ${{ forgejo.event_name == 'pull_request' }}

jobs:
  build:

Why this exact form:

  • No group key, deliberately. Per the Forgejo reference, without a group cancel-in-progress: false means no concurrency management at all — runs proceed in parallel up to the runner's capacity and all of them finish. Adding a group would instead serialise every run of the workflow into a strict queue, which is not wanted: the runs are independent of one another.
  • Keyed on forgejo.event_name, not on the ref. The Forgejo docs show cancel-in-progress: ${{ forgejo.ref_name != 'main' }} as their example, but the documented sample forgejo context for a pull-request event carries "ref": "refs/heads/main" / "ref_name": "main", so a ref-based test risks evaluating false on PR runs and switching off exactly the cancellation that should stay on. event_name is unambiguous. push runs of this workflow can only come from main (on.push.branches: ["main"]), so "event is not pull_request" is precisely "push on main".
  • concurrency is documented as accepting expressions over the forgejo, inputs and vars contexts, and event_name is a documented field of the forgejo context.

No other file changes. Nothing in the repository reads or duplicates this setting (.forgejo/workflows/ contains only build.yml).

Critical files & anchors

  • .forgejo/workflows/build.yml — insertion point is the blank line between pull_request: (line 6) and jobs: (line 8). The jobs:/build: block below is untouched.

Verification

Open the change as a pull request. The pull request's own Build run must appear and complete green. That is the check: Forgejo evaluates the concurrency expression server-side when it creates the run, so a malformed expression or an unsupported key surfaces as a workflow that fails to parse or a run that never starts, rather than as a build failure.

Assumptions & contingencies

  • The Forgejo instance supports the concurrency key with expressions (reported as Forgejo 17.0.1; unverified — confirm first, no session tool reads the instance version). If the PR produces a workflow parse error or the expression is rejected, fall back to the plain documented form concurrency: / cancel-in-progress: false (no group, no expression) and say so on the pull request: that keeps main runs from being cancelled — the point of this issue — at the cost of also leaving superseded PR runs to finish.
  • Runner capacity is an instance-side setting (runner.capacity, default 1) that this change cannot influence (unverified — confirm first, the runner's configuration is not reachable from this repository). With capacity 1, two main runs execute one after the other instead of simultaneously; both still run to completion, which is what this change is for.
## Problem The `Build / build (push)` check on merge commit `530bd777ae041662a64195eb7254beca641f5ff7` (merge of #23) is red with "Has been cancelled" — run #19, API run 16494. The job did not fail: it was cancelled 17s in, mid `apt-get install`, with no failing step and the runner reporting `context canceled`: ``` 2026-11-13T13:07:26.9972313Z Processing triggers for libc-bin (2.41-12+deb13u4) ... 2026-11-13T13:07:27.1976539Z ⚙️ [runner]: context canceled … 2026-11-13T13:07:27.5752474Z 🏁 Job failed ``` Two pull requests were merged into `main` within seconds of each other (#23 → `530bd77`, then #22 → `2669dcd`): run #19 (`530bd77`) was cancelled at `13:07:27.2`, run #20 (`2669dcd`) reached the same runner at `13:07:28.6` and finished green. The first `main` run was superseded by the second. Nothing in the tree caused it — `.forgejo/workflows/build.yml` installs its dependencies with explicit `apt-get` and never reads `mise.toml`, the only file #23 touched. The cancellation is Forgejo's *default* concurrency management, not a repository setting: with no `concurrency` key, a new `push` / `pull_request (synchronize)` event cancels any other invocation of the same workflow (Forgejo Actions reference, [`concurrency`](https://forgejo.org/docs/v17.0/user/actions/reference/#concurrency)). The consequence is that an intermediate `main` commit keeps a permanently red check for content that was never broken, while the proof that the content is fine sits one commit later. Desired behaviour: a `push` run on `main` is never cancelled — every `main` commit keeps a verdict that is not retracted. Pull-request runs keep superseding each other, since an obsolete PR commit's result is not interesting and the runner's time is better spent on the newest one. ## Plan ### Context Add a workflow-level `concurrency` setting to `.forgejo/workflows/build.yml` that disables Forgejo's automatic cancellation for `push` events while keeping it for `pull_request` events. This is the repository's only workflow and its only job. No other behaviour of the workflow changes; nothing else in the repository is touched. ### Approach Single change, in `.forgejo/workflows/build.yml`: insert a top-level `concurrency` block between the `on:` block (currently lines 3-6) and `jobs:` (currently line 8), so the file head reads exactly: ```yaml name: Build on: push: branches: ["main"] pull_request: concurrency: cancel-in-progress: ${{ forgejo.event_name == 'pull_request' }} jobs: build: ``` Why this exact form: - **No `group` key, deliberately.** Per the Forgejo reference, without a group `cancel-in-progress: false` means *no concurrency management at all* — runs proceed in parallel up to the runner's capacity and all of them finish. Adding a `group` would instead serialise every run of the workflow into a strict queue, which is not wanted: the runs are independent of one another. - **Keyed on `forgejo.event_name`, not on the ref.** The Forgejo docs show `cancel-in-progress: ${{ forgejo.ref_name != 'main' }}` as their example, but the documented sample `forgejo` context for a pull-request event carries `"ref": "refs/heads/main"` / `"ref_name": "main"`, so a ref-based test risks evaluating `false` on PR runs and switching off exactly the cancellation that should stay on. `event_name` is unambiguous. `push` runs of this workflow can only come from `main` (`on.push.branches: ["main"]`), so "event is not `pull_request`" is precisely "push on `main`". - `concurrency` is documented as accepting expressions over the `forgejo`, `inputs` and `vars` contexts, and `event_name` is a documented field of the `forgejo` context. No other file changes. Nothing in the repository reads or duplicates this setting (`.forgejo/workflows/` contains only `build.yml`). ### Critical files & anchors - `.forgejo/workflows/build.yml` — insertion point is the blank line between `pull_request:` (line 6) and `jobs:` (line 8). The `jobs:`/`build:` block below is untouched. ### Verification Open the change as a pull request. The pull request's own `Build` run must appear and complete green. That is the check: Forgejo evaluates the `concurrency` expression server-side when it creates the run, so a malformed expression or an unsupported key surfaces as a workflow that fails to parse or a run that never starts, rather than as a build failure. ### Assumptions & contingencies - The Forgejo instance supports the `concurrency` key with expressions (reported as Forgejo 17.0.1; *unverified — confirm first*, no session tool reads the instance version). If the PR produces a workflow parse error or the expression is rejected, fall back to the plain documented form `concurrency:` / `cancel-in-progress: false` (no group, no expression) and say so on the pull request: that keeps `main` runs from being cancelled — the point of this issue — at the cost of also leaving superseded PR runs to finish. - Runner capacity is an instance-side setting (`runner.capacity`, default 1) that this change cannot influence (*unverified — confirm first*, the runner's configuration is not reachable from this repository). With capacity 1, two `main` runs execute one after the other instead of simultaneously; both still run to completion, which is what this change is for.
Owner

For the current workflow, I want queuing. All runs should finish. And since they are independent of each other, they can run concurrently.

For the current workflow, I want queuing. All runs should finish. And since they are independent of each other, they can run concurrently.
Author
Collaborator

That is supported, and it is a two-line change to .forgejo/workflows/build.yml.

The cancellation was not configured anywhere — it is Forgejo's default concurrency management: with no concurrency key, a new push/pull_request (synchronize) event cancels any other invocation of the same workflow on the same branch (Forgejo Actions reference, concurrency). Turning that off is exactly what you describe:

If the concurrency.group setting is not provided […] if evaluated to false, then no concurrency management will occur. Multiple invocations of the workflow may run simultaneously, as long as the available runner instances have capacity to execute the workflows.

So without a group, cancel-in-progress: false gives "all runs finish, concurrently". Note the distinction: adding a group plus cancel-in-progress: false would give strict queuing (serialized, one at a time) — which is not what you want here, since the runs are independent.

This instance is Forgejo 17.0.1, which has the feature.

Option A — never cancel, anywhere (push and PR)

on:
  push:
    branches: ["main"]
  pull_request:

concurrency:
  cancel-in-progress: false

Every run of Build finishes, whatever lands afterwards. Downside: force-pushing a PR eight times in a row leaves eight builds running against obsolete commits.

Option B — never cancel on main, keep superseding on PR branches

concurrency:
  cancel-in-progress: ${{ forgejo.ref_name != 'main' }}

main commits each get a verdict that is never retracted; PR branches keep the "only the newest commit matters" behaviour, so runner time is not spent on commits nobody will look at. This is the documented expression form (contexts forgejo/github, inputs, vars are allowed in concurrency).

My recommendation is B: the point of this issue is that an main commit kept a red check for content that was never broken, and PR runs that are superseded by a newer push are a different case where cancelling is the useful default. But A is defensible if you want "every run that was ever started has a result" as a flat rule.

Caveat on "concurrently"

concurrency: cancel-in-progress: false removes the cancellation; it does not add runner capacity. From the logs of runs #19/#20 both merges were dispatched to the same runner, so whether two main runs actually overlap depends on that runner's configured capacity (runner.capacity in its config.yml, default 1). With capacity 1 they will simply run one after the other — still both to completion, which is the property you asked for. If genuine parallelism matters, the runner's capacity needs raising too, and that is an instance-side change, not a repository one.

About 530bd77

Independent of which option you take: the existing red check on 530bd777ae041662a64195eb7254beca641f5ff7 stays red until run #19 is re-run from the Actions UI. The change above only affects future runs.

I cannot push branches or edit files in this session — this is a proposal. Say which option you want and I can open it as a PR via the normal implementation flow.

That is supported, and it is a two-line change to `.forgejo/workflows/build.yml`. The cancellation was not configured anywhere — it is Forgejo's *default* concurrency management: with no `concurrency` key, a new `push`/`pull_request (synchronize)` event cancels any other invocation of the same workflow on the same branch ([Forgejo Actions reference, `concurrency`](https://forgejo.org/docs/latest/user/actions/reference#concurrency)). Turning that off is exactly what you describe: > If the `concurrency.group` setting is not provided […] if evaluated to `false`, then no concurrency management will occur. Multiple invocations of the workflow may run simultaneously, as long as the available runner instances have capacity to execute the workflows. So *without* a group, `cancel-in-progress: false` gives "all runs finish, concurrently". Note the distinction: adding a `group` plus `cancel-in-progress: false` would give strict queuing (serialized, one at a time) — which is not what you want here, since the runs are independent. This instance is Forgejo 17.0.1, which has the feature. ## Option A — never cancel, anywhere (push and PR) ```yaml on: push: branches: ["main"] pull_request: concurrency: cancel-in-progress: false ``` Every run of `Build` finishes, whatever lands afterwards. Downside: force-pushing a PR eight times in a row leaves eight builds running against obsolete commits. ## Option B — never cancel on `main`, keep superseding on PR branches ```yaml concurrency: cancel-in-progress: ${{ forgejo.ref_name != 'main' }} ``` `main` commits each get a verdict that is never retracted; PR branches keep the "only the newest commit matters" behaviour, so runner time is not spent on commits nobody will look at. This is the documented expression form (contexts `forgejo`/`github`, `inputs`, `vars` are allowed in `concurrency`). My recommendation is B: the point of this issue is that an *`main`* commit kept a red check for content that was never broken, and PR runs that are superseded by a newer push are a different case where cancelling is the useful default. But A is defensible if you want "every run that was ever started has a result" as a flat rule. ## Caveat on "concurrently" `concurrency: cancel-in-progress: false` removes the *cancellation*; it does not add runner capacity. From the logs of runs #19/#20 both merges were dispatched to the same runner, so whether two `main` runs actually overlap depends on that runner's configured capacity (`runner.capacity` in its `config.yml`, default 1). With capacity 1 they will simply run one after the other — still both to completion, which is the property you asked for. If genuine parallelism matters, the runner's capacity needs raising too, and that is an instance-side change, not a repository one. ## About `530bd77` Independent of which option you take: the existing red check on `530bd777ae041662a64195eb7254beca641f5ff7` stays red until run #19 is re-run from the Actions UI. The change above only affects future runs. I cannot push branches or edit files in this session — this is a proposal. Say which option you want and I can open it as a PR via the normal implementation flow.
lucas closed this issue 2026-11-13 15:04:40 +01:00
Sign in to join this conversation.
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/nanoKontroller2#26
No description provided.