nektos/act · warning
Error occurred running finally: %v (original error: %v)
Error message
Error occurred running finally: %v (original error: %v)
What it means
Executor.Finally wraps a main executor and a finally-executor: it runs both and, if the finally itself errors, returns this composite error carrying both the finally error and the original (possibly nil) error. Note the quirk: even when the main executor succeeded (original error is nil), a failing finally turns the whole chain into an error — masking success. The message uses %v, so the original error's type/unwrap chain is lost.
Source
Thrown at pkg/common/executor.go:230
}
return nil
}
}
// IfBool only runs this executor if conditional is true
func (e Executor) IfBool(conditional bool) Executor {
return e.If(func(_ context.Context) bool {
return conditional
})
}
// Finally adds an executor to run after other executor
func (e Executor) Finally(finally Executor) Executor {
return func(ctx context.Context) error {
err := e(ctx)
err2 := finally(ctx)
if err2 != nil {
return fmt.Errorf("Error occurred running finally: %v (original error: %v)", err2, err)
}
return err
}
}
// Not return an inverted conditional
func (c Conditional) Not() Conditional {
return func(ctx context.Context) bool {
return !c(ctx)
}
}
View on GitHub (pinned to 4f41128141)
Solutions
- Read the two values in the message: fix the finally error first; the '(original error: <nil>)' suffix tells you the job steps themselves succeeded
- Make finally executors best-effort: log-and-swallow non-essential cleanup errors instead of returning them
- If cleanup must fail loudly, keep it, but ensure the original error is preserved (e.g. use errors.Join or fmt.Errorf with two %w)
- Reproduce with act -v to see which finally stage emitted the error
Example fix
// before: cleanup error masks a successful job
err := step.Finally(func(ctx context.Context) error {
return container.Remove(ctx) // returns error -> whole executor fails
})(ctx)
// after: best-effort finally
err := step.Finally(func(ctx context.Context) error {
if rerr := container.Remove(ctx); rerr != nil {
common.Logger(ctx).WithError(rerr).Warn("cleanup failed")
}
return nil
})(ctx) Defensive patterns
Strategy: try-catch
Try / catch
err := pipeline.Finally(cleanup)(ctx)
if err != nil && strings.Contains(err.Error(), "Error occurred running finally") {
// decide policy: a cleanup failure after a green job usually should not fail CI
if strings.HasSuffix(err.Error(), "(original error: <nil>)") {
log.Warnf("cleanup failed but steps succeeded: %v", err)
err = nil // or escalate, per policy
}
} Prevention
- Make finally executors best-effort: log and return nil unless cleanup is load-bearing
- Never let cleanup errors mask the original failure — carry both values explicitly
- Test the finally path separately from the happy path
When it happens
Trigger: Any runner cleanup stage wrapped in .Finally() failing: container stop/remove errors, artifact/close errors, log upload failures — occurring after the job's main steps, whether or not those steps failed.
Common situations: Post-job cleanup racing with Docker daemon shutdown; container already removed by a timeout so 'docker rm' fails; cleanup code returning errors on nil-safe paths; the failure appearing only when the job itself is green, confusing CI triage.
Related errors
AI-assisted analysis of nektos/act@4f41128141 (2026-08-15).
Data as JSON: /api/errors/26e450a2699e4c31.
Report an issue: GitHub.