diff --git a/cmd/agent/core/agent.go b/cmd/agent/core/agent.go index 32b63aa1c..d91b76e81 100644 --- a/cmd/agent/core/agent.go +++ b/cmd/agent/core/agent.go @@ -90,7 +90,14 @@ func run(ctx context.Context, c *cli.Command, backends []types.Backend) error { hostname, _ = os.Hostname() } - counter.Polling = c.Int("max-workflows") + maxWorkflows := c.Int("max-workflows") + singleWorkflow := c.Bool("single-workflow") + if singleWorkflow && maxWorkflows > 1 { + log.Warn().Msgf("max-workflows forced from %d to 1 due to agent running single workflow mode.", maxWorkflows) + maxWorkflows = 1 + } + + counter.Polling = maxWorkflows counter.Running = 0 if c.Bool("healthcheck") { @@ -201,8 +208,6 @@ func run(ctx context.Context, c *cli.Command, backends []types.Backend) error { } log.Debug().Msgf("loaded %s backend engine", backendEngine.Name()) - maxWorkflows := c.Int("max-workflows") - customLabels := make(map[string]string) if err := stringSliceAddToMap(c.StringSlice("labels"), customLabels); err != nil { return err @@ -298,6 +303,11 @@ func run(ctx context.Context, c *cli.Command, backends []types.Backend) error { log.Debug().Msg("polling new workflow") if err := runner.Run(agentCtx, shutdownCtx); err != nil { + if singleWorkflow { + log.Error().Err(err).Msg("runner done with error") + ctxCancel(nil) + return nil + } log.Error().Err(err).Msg("runner error, retrying...") // Check if context is canceled if agentCtx.Err() != nil { @@ -311,13 +321,23 @@ func run(ctx context.Context, c *cli.Command, backends []types.Backend) error { // Continue to next iteration } } + + if singleWorkflow { + log.Info().Msg("shutdown single workflow runner") + ctxCancel(nil) + return nil + } } }) } - log.Info().Msgf( - "starting Woodpecker agent with version '%s' and backend '%s' using platform '%s' running up to %d pipelines in parallel", - version.String(), backendEngine.Name(), engInfo.Platform, maxWorkflows) + log.Info(). + Str("version", version.String()). + Str("backend", backendEngine.Name()). + Str("platform", engInfo.Platform). + Int("parallel workflows", maxWorkflows). + Bool("single workflow", singleWorkflow). + Msg("starting Woodpecker agent") return serviceWaitingGroup.Wait() } diff --git a/cmd/agent/core/flags.go b/cmd/agent/core/flags.go index 2ef646380..e7ad92f03 100644 --- a/cmd/agent/core/flags.go +++ b/cmd/agent/core/flags.go @@ -77,6 +77,12 @@ var flags = []cli.Flag{ Usage: "agent parallel workflows", Value: 1, }, + &cli.BoolFlag{ + Sources: cli.EnvVars("WOODPECKER_AGENT_SINGLE_WORKFLOW"), + Name: "single-workflow", + Usage: "exit the agent after first workflow", + Value: false, + }, &cli.BoolFlag{ Sources: cli.EnvVars("WOODPECKER_HEALTHCHECK"), Name: "healthcheck", diff --git a/docs/docs/30-administration/10-configuration/30-agent.md b/docs/docs/30-administration/10-configuration/30-agent.md index 589c57b83..10f1cbc46 100644 --- a/docs/docs/30-administration/10-configuration/30-agent.md +++ b/docs/docs/30-administration/10-configuration/30-agent.md @@ -148,6 +148,19 @@ Configures the number of parallel workflows. --- +### AGENT_SINGLE_WORKFLOW + +- Name: `WOODPECKER_AGENT_SINGLE_WORKFLOW` +- Default: `false` + +Configures the agent to exit (shutdown) after executing one workflow. When configured, +`WOODPECKER_MAX_WORKFLOWS` is forced to 1. + +This one-shot mode is useful in ephemeral environments that are provisioned on demand +by external automation — for example, when an autoscaler spins up a dedicated machine. In these setups, the agent starts, executes exactly one workflow, and exits, allowing the environment to be cleanly torn down afterward. + +--- + ### AGENT_LABELS - Name: `WOODPECKER_AGENT_LABELS`