Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions cmd/agent/core/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/agent/core/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions docs/docs/30-administration/10-configuration/30-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down