From f461a601eb7cf0162ccfa20c48f5c3ba2b3e834e Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 2 Apr 2026 13:41:32 -0600 Subject: [PATCH 1/9] Run multiple instances of act concurrently When running two instances of act against the same Docker environment, the container naming code produces consistent, repeatable names every time - even if the user isn't interested in reusing the build containers. As a result, these act instances will interfere with each other and this will result in odd errors and failures. The solution is to introduce a random string to the container name such that all containers created and managed by a specific instance of act will be distinct from those managed by other instances. In addition, if the --reuse option is in play, this behavior is disabled and the old, repeatable naming approach (albeit with a slight tweak to the name :) ) will come into play again. --- pkg/runner/run_context.go | 54 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 5d4277123ed..d474b555c0d 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "io" + "math/big" "os" "path/filepath" "regexp" @@ -28,6 +29,9 @@ import ( "github.com/opencontainers/selinux/go-selinux" ) +var randomStringCharset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") +var randomStringCharsetLength = len(randomStringCharset) + // RunContext contains info about current job type RunContext struct { Name string @@ -89,8 +93,56 @@ func (rc *RunContext) GetEnv() map[string]string { return rc.Env } +func (rc *RunContext) randomString(length int) (string, error) { + if length <= 0 { + return "", errors.New("The string length must be >= 0") + } + + length64 := int64(randomStringCharsetLength) + target := make([]rune, length) + for i := range target { + p, err := rand.Int(rand.Reader, big.NewInt(length64)) + if err != nil { + return "", err + } + target[i] = randomStringCharset[p.Int64()] + } + return string(target), nil +} + func (rc *RunContext) jobContainerName() string { - return createContainerName("act", rc.String()) + // Let's assume a fixed, consistent "unique identifier" for + // the container name. If the container is not meant to be + // reusable, we will try to generate a unique identifier + // that can replace this constant one. + id := "reusable" + if !rc.Config.ReuseContainers { + // To allow multiple concurrent instances of ACT, we need to use + // a unique identifier that will differentiate all containers + // spawned by this instance from those spawned by others. If we + // don't do that, then weird conflicts will arise due to possible + // container name collsions. We use a secure random string of + // alphanumeric characters for this purpose. The string will be + // of the same length as the fixed-identifier used when reusable + // containers are desired, minus 2 characters, for consistency. + // + // The 2 character difference eliminate the chance that the random + // string be the same as the "fixed" ID + length := len(id) + 2 + + // This function will try to generate a secure random string. If + // that fails for some reason, it will use the default math/rand + // as its source of randomness. + if newID, err := rc.randomString(length); err != nil { + // TODO: Should we log this error? How (without a lot of hoops)? + // If we had some sort of issue with the randomness, + // let's just use the PID as the "randomness" + id = fmt.Sprintf("%08X", os.Getpid()) + } else { + id = newID + } + } + return createContainerName("act", id, rc.String()) } // networkName return the name of the network which will be created by `act` automatically for job, From 7c7e070268271e05049a33c74a4930fbcad91a82 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 2 Apr 2026 14:14:02 -0600 Subject: [PATCH 2/9] BUG: generate a single random identifier --- pkg/runner/run_context.go | 80 ++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index d474b555c0d..cac81d42aa4 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -29,8 +29,7 @@ import ( "github.com/opencontainers/selinux/go-selinux" ) -var randomStringCharset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") -var randomStringCharsetLength = len(randomStringCharset) +var commonIdentifier = "reusable" // RunContext contains info about current job type RunContext struct { @@ -59,6 +58,41 @@ type RunContext struct { nodeToolFullPath string } +func init() { + var charset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + + // To allow multiple concurrent instances of ACT, we need to use + // a unique identifier that will differentiate all containers + // spawned by this instance from those spawned by others. If we + // don't do that, then weird conflicts will arise due to possible + // container name collsions. We use a secure random string of + // alphanumeric characters for this purpose. The string will be + // of the same length as the fixed-identifier used when reusable + // containers are desired, minus 2 characters, for consistency. + // + // The 2 character difference eliminate the chance that the random + // string be the same as the "fixed" ID + length := len(commonIdentifier) + 2 + + // This function will try to generate a secure random string. If + // that fails for some reason, it will use the default math/rand + // as its source of randomness. + length64 := int64(len(charset)) + target := make([]rune, length) + for i := range target { + p, err := rand.Int(rand.Reader, big.NewInt(length64)) + if err != nil { + // TODO: Should we log this error? How (without a lot of hoops)? + // If we had some sort of issue with the randomness, + // let's just use the PID as the "randomness" + commonIdentifier = fmt.Sprintf("%08X", os.Getpid()) + return + } + target[i] = charset[p.Int64()] + } + commonIdentifier = string(target) +} + func (rc *RunContext) AddMask(mask string) { rc.Masks = append(rc.Masks, mask) } @@ -93,23 +127,6 @@ func (rc *RunContext) GetEnv() map[string]string { return rc.Env } -func (rc *RunContext) randomString(length int) (string, error) { - if length <= 0 { - return "", errors.New("The string length must be >= 0") - } - - length64 := int64(randomStringCharsetLength) - target := make([]rune, length) - for i := range target { - p, err := rand.Int(rand.Reader, big.NewInt(length64)) - if err != nil { - return "", err - } - target[i] = randomStringCharset[p.Int64()] - } - return string(target), nil -} - func (rc *RunContext) jobContainerName() string { // Let's assume a fixed, consistent "unique identifier" for // the container name. If the container is not meant to be @@ -117,30 +134,7 @@ func (rc *RunContext) jobContainerName() string { // that can replace this constant one. id := "reusable" if !rc.Config.ReuseContainers { - // To allow multiple concurrent instances of ACT, we need to use - // a unique identifier that will differentiate all containers - // spawned by this instance from those spawned by others. If we - // don't do that, then weird conflicts will arise due to possible - // container name collsions. We use a secure random string of - // alphanumeric characters for this purpose. The string will be - // of the same length as the fixed-identifier used when reusable - // containers are desired, minus 2 characters, for consistency. - // - // The 2 character difference eliminate the chance that the random - // string be the same as the "fixed" ID - length := len(id) + 2 - - // This function will try to generate a secure random string. If - // that fails for some reason, it will use the default math/rand - // as its source of randomness. - if newID, err := rc.randomString(length); err != nil { - // TODO: Should we log this error? How (without a lot of hoops)? - // If we had some sort of issue with the randomness, - // let's just use the PID as the "randomness" - id = fmt.Sprintf("%08X", os.Getpid()) - } else { - id = newID - } + id = commonIdentifier } return createContainerName("act", id, rc.String()) } From 3e7a0356f627da2942b3cbd163ebfdcc8aca7491 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 2 Apr 2026 14:50:54 -0600 Subject: [PATCH 3/9] Make the random string unique to the RunContext This covers the case of someone using the act code as a library (???), and avoids them running into the same issues under that scenario. --- pkg/runner/run_context.go | 53 ++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index cac81d42aa4..4060ce3b48f 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -29,7 +29,7 @@ import ( "github.com/opencontainers/selinux/go-selinux" ) -var commonIdentifier = "reusable" +const reusableCommonIdentifier = "reusable" // RunContext contains info about current job type RunContext struct { @@ -56,27 +56,34 @@ type RunContext struct { caller *caller // job calling this RunContext (reusable workflows) Cancelled bool nodeToolFullPath string + commonIdentifier string } -func init() { - var charset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") +func (rc *RunContext) computeCommonIdentifier() string { + if rc.Config.ReuseContainers { + // If the container is meant to be reusable, we must + // return a constant value to keep container names consistent + return reusableCommonIdentifier + } - // To allow multiple concurrent instances of ACT, we need to use + // The container is not meant to be reusable! Thus, we must + // create a randomized value to distinguish this RunContext's + // container instances from all others' + // + // To allow multiple concurrent RunContexts, we need to use // a unique identifier that will differentiate all containers // spawned by this instance from those spawned by others. If we // don't do that, then weird conflicts will arise due to possible // container name collsions. We use a secure random string of // alphanumeric characters for this purpose. The string will be - // of the same length as the fixed-identifier used when reusable - // containers are desired, minus 2 characters, for consistency. + // of the same length as the identifier used when reusable + // containers are desired, plus 2 characters. // - // The 2 character difference eliminate the chance that the random + // The 2 character difference eliminates the chance that the random // string be the same as the "fixed" ID - length := len(commonIdentifier) + 2 + var charset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + length := len(reusableCommonIdentifier) + 2 - // This function will try to generate a secure random string. If - // that fails for some reason, it will use the default math/rand - // as its source of randomness. length64 := int64(len(charset)) target := make([]rune, length) for i := range target { @@ -85,12 +92,20 @@ func init() { // TODO: Should we log this error? How (without a lot of hoops)? // If we had some sort of issue with the randomness, // let's just use the PID as the "randomness" - commonIdentifier = fmt.Sprintf("%08X", os.Getpid()) - return + return fmt.Sprintf("%08X", os.Getpid()) } target[i] = charset[p.Int64()] } - commonIdentifier = string(target) + return string(target) +} + +func (rc *RunContext) getCommonIdentifier() string { + // If it needs to be computed, compute it! + // Otherwise, return the computed value + if rc.commonIdentifier == "" { + rc.commonIdentifier = rc.computeCommonIdentifier() + } + return rc.commonIdentifier } func (rc *RunContext) AddMask(mask string) { @@ -128,15 +143,7 @@ func (rc *RunContext) GetEnv() map[string]string { } func (rc *RunContext) jobContainerName() string { - // Let's assume a fixed, consistent "unique identifier" for - // the container name. If the container is not meant to be - // reusable, we will try to generate a unique identifier - // that can replace this constant one. - id := "reusable" - if !rc.Config.ReuseContainers { - id = commonIdentifier - } - return createContainerName("act", id, rc.String()) + return createContainerName("act", rc.getCommonIdentifier(), rc.String()) } // networkName return the name of the network which will be created by `act` automatically for job, From 4eb8eb648bad6a4cfadc81961b76e1be0778c5b1 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 2 Apr 2026 17:35:46 -0600 Subject: [PATCH 4/9] Add --parallel to expressly permit parallel operation --- cmd/input.go | 1 + cmd/root.go | 2 ++ pkg/runner/run_context.go | 53 +++++++++++++++++---------------------- pkg/runner/runner.go | 1 + 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/cmd/input.go b/cmd/input.go index c348ef3acf5..38018203222 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -14,6 +14,7 @@ type Input struct { autodetectEvent bool eventPath string reuseContainers bool + allowConcurrentRuns bool bindWorkdir bool secrets []string vars []string diff --git a/cmd/root.go b/cmd/root.go index 1fbb55bcfe1..11d64b8f716 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,6 +81,7 @@ func createRootCommand(ctx context.Context, input *Input, version string) *cobra rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)") rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)") rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs") + rootCmd.Flags().BoolVarP(&input.allowConcurrentRuns, "parallel", "", false, "take measures to permit multiple instances of act to run simultaneously against the same Docker server, but has no effect if --reuse is present (without this setting, strange conflicts and failures can happen)") rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy") rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present") rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present") @@ -611,6 +612,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str ForcePull: !input.actionOfflineMode && input.forcePull, ForceRebuild: input.forceRebuild, ReuseContainers: input.reuseContainers, + AllowConcurrentRuns: input.allowConcurrentRuns, Workdir: input.Workdir(), ActionCacheDir: input.actionCachePath, ActionOfflineMode: input.actionOfflineMode, diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 4060ce3b48f..6b8d88c1a8c 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -29,7 +29,7 @@ import ( "github.com/opencontainers/selinux/go-selinux" ) -const reusableCommonIdentifier = "reusable" +var commonIdentifier = "reusable" // RunContext contains info about current job type RunContext struct { @@ -56,34 +56,27 @@ type RunContext struct { caller *caller // job calling this RunContext (reusable workflows) Cancelled bool nodeToolFullPath string - commonIdentifier string } -func (rc *RunContext) computeCommonIdentifier() string { - if rc.Config.ReuseContainers { - // If the container is meant to be reusable, we must - // return a constant value to keep container names consistent - return reusableCommonIdentifier - } +func init() { + var charset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - // The container is not meant to be reusable! Thus, we must - // create a randomized value to distinguish this RunContext's - // container instances from all others' - // - // To allow multiple concurrent RunContexts, we need to use + // To allow multiple concurrent instances of ACT, we need to use // a unique identifier that will differentiate all containers // spawned by this instance from those spawned by others. If we // don't do that, then weird conflicts will arise due to possible // container name collsions. We use a secure random string of // alphanumeric characters for this purpose. The string will be - // of the same length as the identifier used when reusable - // containers are desired, plus 2 characters. + // of the same length as the fixed-identifier used when reusable + // containers are desired, minus 2 characters, for consistency. // - // The 2 character difference eliminates the chance that the random + // The 2 character difference eliminate the chance that the random // string be the same as the "fixed" ID - var charset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - length := len(reusableCommonIdentifier) + 2 + length := len(commonIdentifier) + 2 + // This function will try to generate a secure random string. If + // that fails for some reason, it will use the default math/rand + // as its source of randomness. length64 := int64(len(charset)) target := make([]rune, length) for i := range target { @@ -92,20 +85,12 @@ func (rc *RunContext) computeCommonIdentifier() string { // TODO: Should we log this error? How (without a lot of hoops)? // If we had some sort of issue with the randomness, // let's just use the PID as the "randomness" - return fmt.Sprintf("%08X", os.Getpid()) + commonIdentifier = fmt.Sprintf("%08X", os.Getpid()) + return } target[i] = charset[p.Int64()] } - return string(target) -} - -func (rc *RunContext) getCommonIdentifier() string { - // If it needs to be computed, compute it! - // Otherwise, return the computed value - if rc.commonIdentifier == "" { - rc.commonIdentifier = rc.computeCommonIdentifier() - } - return rc.commonIdentifier + commonIdentifier = string(target) } func (rc *RunContext) AddMask(mask string) { @@ -143,7 +128,15 @@ func (rc *RunContext) GetEnv() map[string]string { } func (rc *RunContext) jobContainerName() string { - return createContainerName("act", rc.getCommonIdentifier(), rc.String()) + // Assume we want to use the consistent, constant identifier + id := "reusable" + if !rc.Config.ReuseContainers && rc.Config.AllowConcurrentRuns { + // If we're not going to reuse the containers, and we want to be + // able to run multiple instances of act concurrently, then we + // must use the instance-specific (random) identifier + id = commonIdentifier + } + return createContainerName("act", id, rc.String()) } // networkName return the name of the network which will be created by `act` automatically for job, diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index b72bbc69746..fc65b081ff1 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -29,6 +29,7 @@ type Config struct { EventPath string // path to JSON file to use for event.json in containers DefaultBranch string // name of the main branch for this repository ReuseContainers bool // reuse containers to maintain state + AllowConcurrentRuns bool // allow running multiple act instances concurrently ForcePull bool // force pulling of the image, even if already present ForceRebuild bool // force rebuilding local docker image action LogOutput bool // log the output from docker run From 38a9eddd2e9c476ead6dd19c163173eed001b153 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 3 Apr 2026 07:40:53 -0600 Subject: [PATCH 5/9] Use exact old naming if --parallel is not in play --- pkg/runner/run_context.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 6b8d88c1a8c..53fb6405103 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -29,7 +29,7 @@ import ( "github.com/opencontainers/selinux/go-selinux" ) -var commonIdentifier = "reusable" +var commonIdentifier = "" // RunContext contains info about current job type RunContext struct { @@ -129,14 +129,13 @@ func (rc *RunContext) GetEnv() map[string]string { func (rc *RunContext) jobContainerName() string { // Assume we want to use the consistent, constant identifier - id := "reusable" if !rc.Config.ReuseContainers && rc.Config.AllowConcurrentRuns { // If we're not going to reuse the containers, and we want to be // able to run multiple instances of act concurrently, then we // must use the instance-specific (random) identifier - id = commonIdentifier + return createContainerName("act", commonIdentifier, rc.String()) } - return createContainerName("act", id, rc.String()) + return createContainerName("act", rc.String()) } // networkName return the name of the network which will be created by `act` automatically for job, From 0bd8e4cabe425ea69a9d309a0638ff050d64945c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 3 Apr 2026 08:30:53 -0600 Subject: [PATCH 6/9] Change the flag to be more expressive The parameter now describes what it does and why ... --parallel implied other things --- cmd/input.go | 2 +- cmd/root.go | 4 ++-- pkg/runner/run_context.go | 27 ++++++++++++--------------- pkg/runner/runner.go | 2 +- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/cmd/input.go b/cmd/input.go index 38018203222..239b0405be9 100644 --- a/cmd/input.go +++ b/cmd/input.go @@ -14,7 +14,7 @@ type Input struct { autodetectEvent bool eventPath string reuseContainers bool - allowConcurrentRuns bool + uniqueContainerNames bool bindWorkdir bool secrets []string vars []string diff --git a/cmd/root.go b/cmd/root.go index 11d64b8f716..352cd5cc063 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,7 +81,7 @@ func createRootCommand(ctx context.Context, input *Input, version string) *cobra rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)") rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)") rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs") - rootCmd.Flags().BoolVarP(&input.allowConcurrentRuns, "parallel", "", false, "take measures to permit multiple instances of act to run simultaneously against the same Docker server, but has no effect if --reuse is present (without this setting, strange conflicts and failures can happen)") + rootCmd.Flags().BoolVarP(&input.uniqueContainerNames, "unique-container-names", "", false, "container names will include a (quasi-)unique (random) component to avoid conflicts with other instances of act running against the same Docker server without conflict (disabled if --reuse is present)") rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy") rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present") rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present") @@ -612,7 +612,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str ForcePull: !input.actionOfflineMode && input.forcePull, ForceRebuild: input.forceRebuild, ReuseContainers: input.reuseContainers, - AllowConcurrentRuns: input.allowConcurrentRuns, + UniqueContainerNames: input.uniqueContainerNames, Workdir: input.Workdir(), ActionCacheDir: input.actionCachePath, ActionOfflineMode: input.actionOfflineMode, diff --git a/pkg/runner/run_context.go b/pkg/runner/run_context.go index 53fb6405103..57a32559334 100644 --- a/pkg/runner/run_context.go +++ b/pkg/runner/run_context.go @@ -29,7 +29,7 @@ import ( "github.com/opencontainers/selinux/go-selinux" ) -var commonIdentifier = "" +var uniqueIdentifier = "" // RunContext contains info about current job type RunContext struct { @@ -66,31 +66,28 @@ func init() { // spawned by this instance from those spawned by others. If we // don't do that, then weird conflicts will arise due to possible // container name collsions. We use a secure random string of - // alphanumeric characters for this purpose. The string will be - // of the same length as the fixed-identifier used when reusable - // containers are desired, minus 2 characters, for consistency. + // alphanumeric characters for this purpose. + // + // We don't use a UUID due to its length - we don't want too long + // a string ... just long enough to provide sufficient randomness // - // The 2 character difference eliminate the chance that the random - // string be the same as the "fixed" ID - length := len(commonIdentifier) + 2 - // This function will try to generate a secure random string. If - // that fails for some reason, it will use the default math/rand - // as its source of randomness. + // that fails for some reason, it will use the PID as its + // "unique identifier" length64 := int64(len(charset)) - target := make([]rune, length) + target := make([]rune, 8) for i := range target { p, err := rand.Int(rand.Reader, big.NewInt(length64)) if err != nil { // TODO: Should we log this error? How (without a lot of hoops)? // If we had some sort of issue with the randomness, // let's just use the PID as the "randomness" - commonIdentifier = fmt.Sprintf("%08X", os.Getpid()) + uniqueIdentifier = fmt.Sprintf("%08X", os.Getpid()) return } target[i] = charset[p.Int64()] } - commonIdentifier = string(target) + uniqueIdentifier = string(target) } func (rc *RunContext) AddMask(mask string) { @@ -129,11 +126,11 @@ func (rc *RunContext) GetEnv() map[string]string { func (rc *RunContext) jobContainerName() string { // Assume we want to use the consistent, constant identifier - if !rc.Config.ReuseContainers && rc.Config.AllowConcurrentRuns { + if rc.Config.UniqueContainerNames && !rc.Config.ReuseContainers { // If we're not going to reuse the containers, and we want to be // able to run multiple instances of act concurrently, then we // must use the instance-specific (random) identifier - return createContainerName("act", commonIdentifier, rc.String()) + return createContainerName("act", uniqueIdentifier, rc.String()) } return createContainerName("act", rc.String()) } diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index fc65b081ff1..fddc371669a 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -29,7 +29,7 @@ type Config struct { EventPath string // path to JSON file to use for event.json in containers DefaultBranch string // name of the main branch for this repository ReuseContainers bool // reuse containers to maintain state - AllowConcurrentRuns bool // allow running multiple act instances concurrently + UniqueContainerNames bool // make container names for this instance unique ForcePull bool // force pulling of the image, even if already present ForceRebuild bool // force rebuilding local docker image action LogOutput bool // log the output from docker run From 180961c82bb3e150b8aace529f9a5320fa470e7d Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 3 Apr 2026 08:37:14 -0600 Subject: [PATCH 7/9] Minor messaging fix --- cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index 352cd5cc063..779de9d9851 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,7 +81,7 @@ func createRootCommand(ctx context.Context, input *Input, version string) *cobra rootCmd.Flags().StringArrayVarP(&input.inputs, "input", "", []string{}, "action input to make available to actions (e.g. --input myinput=foo)") rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)") rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "don't remove container(s) on successfully completed workflow(s) to maintain state between runs") - rootCmd.Flags().BoolVarP(&input.uniqueContainerNames, "unique-container-names", "", false, "container names will include a (quasi-)unique (random) component to avoid conflicts with other instances of act running against the same Docker server without conflict (disabled if --reuse is present)") + rootCmd.Flags().BoolVarP(&input.uniqueContainerNames, "unique-container-names", "", false, "container names will include a (quasi-)unique (random) component to avoid conflicts with other instances of act running against the same Docker server (disabled if --reuse is present)") rootCmd.Flags().BoolVarP(&input.bindWorkdir, "bind", "b", false, "bind working directory to container, rather than copy") rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", true, "pull docker image(s) even if already present") rootCmd.Flags().BoolVarP(&input.forceRebuild, "rebuild", "", true, "rebuild local action docker image(s) even if already present") From 287a8f1d68d71a7a8ddd6d85a0d9aee5efffe135 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Sat, 4 Apr 2026 19:37:47 -0600 Subject: [PATCH 8/9] Canonicalize action paths always Some NodeJS actions can trip up if this isn't done properly --- pkg/runner/action.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pkg/runner/action.go b/pkg/runner/action.go index b46a4d03a75..281475a6911 100644 --- a/pkg/runner/action.go +++ b/pkg/runner/action.go @@ -459,6 +459,34 @@ func populateEnvsFromInput(ctx context.Context, env *map[string]string, action * } } +func canonicalizePath(path string) string { + // The empty path is the empty path ... use "." + result := path + if result == "" { + result = "." + } + + var next string + var err error + + // make it absolute + if next, err = filepath.Abs(result); err != nil { + // If this failed there isn't much we can do ... + return result + } + + // store the value + result = next + + // try to resolve the symlinks + if next, err = filepath.EvalSymlinks(result); err != nil { + return result + } + + // return the final outcome + return next +} + func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) (string, string) { actionName := "" containerActionDir := "." @@ -477,7 +505,7 @@ func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) actionName = strings.ReplaceAll(actionName, "\\", "/") } } - return actionName, containerActionDir + return actionName, canonicalizePath(containerActionDir) } func getOsSafeRelativePath(s, prefix string) string { From 48ac216f07e7902841dae5bcc27b5e69ea26cdaf Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Sat, 4 Apr 2026 19:39:40 -0600 Subject: [PATCH 9/9] Roll back changes --- pkg/runner/action.go | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/pkg/runner/action.go b/pkg/runner/action.go index 281475a6911..b46a4d03a75 100644 --- a/pkg/runner/action.go +++ b/pkg/runner/action.go @@ -459,34 +459,6 @@ func populateEnvsFromInput(ctx context.Context, env *map[string]string, action * } } -func canonicalizePath(path string) string { - // The empty path is the empty path ... use "." - result := path - if result == "" { - result = "." - } - - var next string - var err error - - // make it absolute - if next, err = filepath.Abs(result); err != nil { - // If this failed there isn't much we can do ... - return result - } - - // store the value - result = next - - // try to resolve the symlinks - if next, err = filepath.EvalSymlinks(result); err != nil { - return result - } - - // return the final outcome - return next -} - func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) (string, string) { actionName := "" containerActionDir := "." @@ -505,7 +477,7 @@ func getContainerActionPaths(step *model.Step, actionDir string, rc *RunContext) actionName = strings.ReplaceAll(actionName, "\\", "/") } } - return actionName, canonicalizePath(containerActionDir) + return actionName, containerActionDir } func getOsSafeRelativePath(s, prefix string) string {