Skip to content

Concurrency groups and parallel steps (reference branch — being split, do not review) - #6147

Draft
McNultyyy wants to merge 6 commits into
nektos:masterfrom
McNultyyy:feat/concurrency-groups
Draft

Concurrency groups and parallel steps (reference branch — being split, do not review)#6147
McNultyyy wants to merge 6 commits into
nektos:masterfrom
McNultyyy:feat/concurrency-groups

Conversation

@McNultyyy

@McNultyyy McNultyyy commented Aug 4, 2026

Copy link
Copy Markdown

Not for review — being split into smaller PRs. This branch is kept as the reference implementation so the individual PRs have something to point at. Please review those instead.

This started as one branch implementing GitHub Actions concurrency groups and the new parallel step syntax. At +3800 / −205 across 58 files it is far too large to ask anyone to review, and it mixes two unrelated features with a change to how every workflow is scheduled. Splitting it.

The stack

PR closes
1 Guard the shared log writers and mask list against concurrent access — #6153
2 Give each step execution its own runner file command directory — #6154 refs #2184 #2697 #2553
3 The pkg/model side of concurrency.queue#6152, building on @xenjke's #6096
4 Execute each workflow as an independent run instead of one global stage pipeline
5 Workflow and job level concurrency groups with cancel-in-progress and queue
6 Parallel steps: background, wait, wait-all, cancel, parallel#6161 (draft, stacked on #6153 + #6154) #6124

PRs 1–3 are independent and are up now. PR 6 is up as a draft in #6161: it closes a filed issue and does not depend on PR 4, so it went ahead of it. PR 4 changes job scheduling for everyone even if they use none of these features, so it gets its own PR with its own justification rather than being smuggled in with a feature — it stays parked until there is a signal that it is wanted. PR 5 does not need it either: job level concurrency works on the existing stage executor, and only workflow level groups require PR 4.

Trying it out before it lands

This branch is usable today if you need concurrency or parallel steps now:

git clone --branch feat/concurrency-groups https://github.com/McNultyyy/act.git && cd act && go build -o act-fork .

Needs Go 1.25+; no CGO, so it cross-compiles anywhere. Note go install github.com/McNultyyy/act@... does not work — go.mod correctly declares the module as github.com/nektos/act, so it has to be cloned and built. Bug reports for these features belong here, not on the upstream issue tracker.

Questions before I push the rest

Rather than drop three more large PRs on you:

  1. Do you want concurrency implemented at all, and in this shape? Your docs list it as planned, so you may already have a design in mind. PR 5 holds a group for the whole workflow run, implements queue: single / max with a FIFO queue, and treats group names case-insensitively as GitHub does.

  2. Is PR 4 acceptable? It removes the barrier that makes one workflow's stage wait on an unrelated workflow's jobs, and turns --concurrent-jobs into a plan-wide semaphore. Workflow-level concurrency groups genuinely need it. But it changes job start order and log interleaving for the default act invocation, and it raises real parallelism, which makes the known races in GoGitActionCache race condition with concurrent matrix jobs #6028, Concurrent map write when running a build that produces parallel builds #6057 and Same container image started multiple times using runs-on with matrix strategy #2764 more likely to fire. It may be that this should wait until those are fixed.

  3. Would you rather have the two features as separate efforts entirely? Parallel steps (PR 6, Add support for concurrent steps execution #6124) has no dependency on the concurrency work.

Happy to shape these however suits you, or to drop parts of it.

Known issue in this branch

The context-carried log writers in the parallel-steps work silently disabled newCompositeCommandExecutor's ReplaceLogWriter, which broke composite action outputs. Fixed in b8f60ec; the split PRs sequence the change so it cannot recur.

🤖 Generated with Claude Code

Implement the workflow and job level `concurrency` key, which act
previously parsed but ignored:

* Jobs sharing an evaluated concurrency group run one at a time. Jobs of
  the same workflow run share the workflow level group, so they still run
  in parallel with each other while different workflow runs in the same
  group are serialized.
* `cancel-in-progress: true` gracefully cancels the jobs currently
  holding the group (reusing the existing job cancellation mechanism, so
  `if: always()` steps and post steps still run). Cancelled jobs finish
  with result 'cancelled' instead of failing the plan, and the remaining
  jobs of a cancelled workflow run are cancelled as well.
* `group` and `cancel-in-progress` support expressions and are evaluated
  when the job starts, so `needs`, `matrix` etc. are available.
* Groups are shared with reusable workflow runners, skipped jobs do not
  take part in groups, and multi-group acquisition is ordered to avoid
  deadlocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@McNultyyy
McNultyyy marked this pull request as draft August 4, 2026 16:36
McNultyyy and others added 2 commits August 4, 2026 18:11
Implement the June 2026 GitHub Actions syntax for concurrent steps:

* `background: true` runs a step asynchronously while the job continues,
  with at most 10 background steps running concurrently and additional
  ones queueing until a slot is free.
* `wait:` pauses the job until the referenced background steps complete,
  `wait-all:` until all active background steps complete. A failed
  background step fails the job at the first wait that includes it,
  unless `continue-on-error` applies. Outputs of background steps are
  available after the wait. These steps always run, even when a previous
  step failed.
* `cancel:` gracefully terminates a running background step, cancelled
  steps do not fail the job. Background steps still running at the end
  of the job are stopped the same way.
* `parallel:` groups steps that all run as background steps with an
  implicit wait for the whole group at the end.

Concurrently running steps share the job state (step results, env,
path, masks), so those mutations are now serialized: the shared state is
locked during the setup and result processing phase of every step, the
runner file commands (GITHUB_OUTPUT etc.) get a unique directory per
step execution, outputs and states are routed by explicit step id
instead of the ambiguous CurrentStep field, and the log writers of the
execution environments are swapped under a mutex. The new tests pass
with the race detector enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@McNultyyy McNultyyy changed the title feat: support concurrency groups and cancel-in-progress feat: support concurrency groups and parallel steps (background, wait, wait-all, cancel, parallel) Aug 4, 2026
McNultyyy and others added 2 commits August 4, 2026 21:14
…steps

Concurrency groups are now held per workflow run instead of per job,
which fixes several deviations from GitHub and two deadlocks:

* The plan executes one independent run per workflow (stages of a run
  serially, runs in parallel, total job count still limited by
  --concurrent-jobs) instead of a global stage barrier, so a run holds
  its group from its first to its last job. Runs in the same group can
  no longer interleave between stages.
* At most one run is pending per group and a newer request supersedes
  (cancels) the previously pending one, as GitHub does even without
  cancel-in-progress. Superseded runs are marked 'cancelled' and never
  execute.
* Groups held by a run or its callers are carried in the context, so a
  called reusable workflow declaring the same group no longer deadlocks
  waiting for its own caller.
* Waiting for a group aborts when the waiter itself is cancelled, and a
  job that already completed is no longer retroactively marked cancelled
  by a late cancel-in-progress.

Concurrent step execution no longer races on shared job state:

* Step log writers travel with the context instead of being swapped in
  the execution environment's single global writer slot, so concurrent
  steps capture their own output; docker actions and docker:// steps
  route their workflow commands by explicit step id.
* Expression evaluators operate on snapshots of env and step results
  taken under a lock, so a background step interpolating
  ${{ steps.x.outputs.y }} cannot observe a map mid-write.
* Composite actions snapshot the parent state they inherit, merge their
  PATH additions instead of overwriting concurrent ones, and derive
  script names from their stable step id instead of the mutable
  CurrentStep, which could make two steps share a script path.

Further fixes:

* A job consisting only of wait/wait-all/cancel steps no longer panics
  on a nil post executor.
* Duplicate background step ids fail the job instead of silently hiding
  the first step and swallowing its failure.
* Cancelling a background step that already failed no longer suppresses
  that failure at a later wait.
* Background steps queued behind the 10 slot limit are logged, and a
  wait on a queued step warns instead of blocking silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements the remaining documented behaviour of GitHub's concurrency
groups:

* `concurrency.queue` selects how many runs may wait in a group.
  `single` (the default) keeps at most one pending run and a newer
  request cancels and replaces it; `max` queues up to 100 runs in
  first-in-first-out order and cancels requests arriving once the queue
  is full. The queue policy belongs to the group, so a workflow that
  omits `queue: max` can neither discard a queue admitted under it nor
  skip the supersede a single-queue group guarantees.
* Concurrency group names are matched case insensitively, so `prod` and
  `Prod` are one group as on GitHub.
* The combination of `queue: max` and `cancel-in-progress: true`, which
  GitHub refuses, is rejected while the workflow is read, so no job runs
  and produces side effects first. Values that only become known after
  evaluation are still validated before the group is used, including
  when the group expression interpolates to an empty string.

This also fixes two defects in the existing implementation:

* A job waiting for a concurrency group no longer occupies one of the
  `--concurrent-jobs` slots. Slots are taken once a job is ready to run,
  so a queue can no longer starve unrelated workflows or deadlock a run
  whose workflow level group is held while its own job waits for a slot.
* A waiter promoted to holder at the same moment it gives up waiting
  releases the group instead of leaving it held by a request that never
  runs, which previously blocked every later run in that group forever.

A job cancelled by Ctrl+C or by its workflow run while queued is no
longer reported as cancelled by the concurrency group.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@McNultyyy McNultyyy changed the title feat: support concurrency groups and parallel steps (background, wait, wait-all, cancel, parallel) feat: support concurrency groups (incl. concurrency.queue) and parallel steps Aug 4, 2026
…slot

The per-step log writers introduced with the parallel steps are preferred
over the execution environment's global writer slot, which silently
disabled the three remaining users of ReplaceLogWriter:

* the composite action command handler, so a composite action's inner
  steps had their ::set-output and ::save-state parsed by the enclosing
  job step's handler and recorded under the wrong step id
* the hashFiles helper, whose output is captured into a buffer
* the node tool path detection, likewise

The first broke TestRunEvent/uses-composite, uses-nested-composite and
composite-fail-with-output. All three now publish their writers on the
context as well, which also removes the last non-nested swaps of the
shared writer slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@McNultyyy McNultyyy changed the title feat: support concurrency groups (incl. concurrency.queue) and parallel steps Concurrency groups and parallel steps (reference branch — being split, do not review) Aug 5, 2026
McNultyyy added a commit to McNultyyy/act that referenced this pull request Aug 17, 2026
Twelve days of silence on all four PRs and 96 days since the last merge
upstream, so PRs 4-6 are no longer waiting on answers to the questions in
 nektos#6147. PR 6 went first: it closes a live issue and does not depend on the
risky scheduling change in PR 4.

Also records that the planned "copy the step" fix was dropped because
ParallelSteps() already decodes fresh children per call, and that the docker
composite suite has not run yet, which is what holds the PR back.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant