Concurrency groups and parallel steps (reference branch — being split, do not review) - #6147
Draft
McNultyyy wants to merge 6 commits into
Draft
Concurrency groups and parallel steps (reference branch — being split, do not review)#6147McNultyyy wants to merge 6 commits into
McNultyyy wants to merge 6 commits into
Conversation
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
marked this pull request as draft
August 4, 2026 16:36
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>
…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>
…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>
5 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This started as one branch implementing GitHub Actions
concurrencygroups 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
pkg/modelside ofconcurrency.queue— #6152, building on @xenjke's #6096concurrencygroups with cancel-in-progress and queuebackground,wait,wait-all,cancel,parallel— #6161 (draft, stacked on #6153 + #6154)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
concurrencyworks 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
concurrencyor parallel steps now:Needs Go 1.25+; no CGO, so it cross-compiles anywhere. Note
go install github.com/McNultyyy/act@...does not work —go.modcorrectly declares the module asgithub.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:
Do you want
concurrencyimplemented 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, implementsqueue: single/maxwith a FIFO queue, and treats group names case-insensitively as GitHub does.Is PR 4 acceptable? It removes the barrier that makes one workflow's stage wait on an unrelated workflow's jobs, and turns
--concurrent-jobsinto a plan-wide semaphore. Workflow-level concurrency groups genuinely need it. But it changes job start order and log interleaving for the defaultactinvocation, 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.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'sReplaceLogWriter, which broke composite action outputs. Fixed in b8f60ec; the split PRs sequence the change so it cannot recur.🤖 Generated with Claude Code