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

  1. Read the two values in the message: fix the finally error first; the '(original error: <nil>)' suffix tells you the job steps themselves succeeded
  2. Make finally executors best-effort: log-and-swallow non-essential cleanup errors instead of returning them
  3. 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)
  4. 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

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.