فهرست منبع

fix: persist blocked executions to saveLogs files

stepSaveLog only ran at the end of the full executor chain, so runs
blocked by concurrency, rate limits, or ACL checks never wrote results
or output files even though they appeared in the in-memory logs UI.

Persist logs from finishExecChain so every terminal execution is saved
when saveLogs is configured.

Fixes #1099

Co-authored-by: Cursor <cursoragent@cursor.com>
jamesread 2 روز پیش
والد
کامیت
f3e1ed4526
3فایلهای تغییر یافته به همراه81 افزوده شده و 1 حذف شده
  1. 2 0
      docs/modules/ROOT/pages/logs/saving.adoc
  2. 1 1
      service/internal/executor/executor.go
  3. 78 0
      service/internal/executor/executor_test.go

+ 2 - 0
docs/modules/ROOT/pages/logs/saving.adoc

@@ -3,6 +3,8 @@
 
 By default, OliveTin only keeps logs in memory, meaning that if you restart OliveTin your logs will be lost. For some use cases this is acceptable, but you can configure OliveTin to save logs for you.
 
+When `saveLogs` is enabled, OliveTin writes a results file and output file for every finished execution, including blocked runs (for example when concurrency or rate limits prevent the action from starting).
+
 You can configure the global setting for saving logs, or override it on a per-action basis;
 
 [source,yaml]

+ 1 - 1
service/internal/executor/executor.go

@@ -184,7 +184,6 @@ func DefaultExecutor(cfg *config.Config) *Executor {
 		stepExec,
 		stepExecAfter,
 		stepLogFinish,
-		stepSaveLog,
 		stepTrigger,
 	}
 
@@ -688,6 +687,7 @@ func (e *Executor) finishExecChain(req *ExecutionRequest) {
 	recordExecutionMetrics(req.logEntry)
 
 	notifyListenersFinished(req)
+	stepSaveLog(req)
 	e.drainGroupQueue()
 }
 

+ 78 - 0
service/internal/executor/executor_test.go

@@ -1042,6 +1042,84 @@ func TestStepSaveLogSanitizesNULInTitle(t *testing.T) {
 	assert.Equal(t, "nul ok", string(output))
 }
 
+func TestBlockedExecutionPersistsSaveLogs(t *testing.T) {
+	t.Parallel()
+
+	resultsDir := t.TempDir()
+	outputDir := t.TempDir()
+
+	action := &config.Action{
+		Title:         "Blocked report",
+		Shell:         "sleep 1",
+		MaxConcurrent: 1,
+		SaveLogs: config.SaveLogsConfig{
+			ResultsDirectory: resultsDir,
+			OutputDirectory:  outputDir,
+		},
+	}
+
+	e, cfg := testGroupExecutor([]*config.Action{action}, nil)
+	binding := e.FindBindingWithNoEntity(action)
+
+	wg1, tracking1 := e.ExecRequest(&ExecutionRequest{
+		Binding:           binding,
+		Cfg:               cfg,
+		AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
+	})
+
+	waitUntilExecutionStarted(t, e, tracking1)
+
+	wg2, tracking2 := e.ExecRequest(&ExecutionRequest{
+		Binding:           binding,
+		Cfg:               cfg,
+		AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
+	})
+
+	wg1.Wait()
+	wg2.Wait()
+
+	snapshot, ok := e.SnapshotLog(tracking2)
+	require.True(t, ok)
+	require.True(t, snapshot.Blocked)
+
+	resultsEntries, err := os.ReadDir(resultsDir)
+	require.NoError(t, err)
+
+	var resultsPath string
+
+	for _, entry := range resultsEntries {
+		if strings.Contains(entry.Name(), tracking2) {
+			resultsPath = filepath.Join(resultsDir, entry.Name())
+			break
+		}
+	}
+
+	require.NotEmpty(t, resultsPath)
+
+	outputEntries, err := os.ReadDir(outputDir)
+	require.NoError(t, err)
+
+	var outputPath string
+
+	for _, entry := range outputEntries {
+		if strings.Contains(entry.Name(), tracking2) {
+			outputPath = filepath.Join(outputDir, entry.Name())
+			break
+		}
+	}
+
+	require.NotEmpty(t, outputPath)
+
+	resultsData, err := os.ReadFile(resultsPath)
+	require.NoError(t, err)
+	assert.Contains(t, string(resultsData), "blocked: true")
+	assert.Contains(t, string(resultsData), tracking2)
+
+	outputData, err := os.ReadFile(outputPath)
+	require.NoError(t, err)
+	assert.Contains(t, string(outputData), "Blocked from executing due to concurrency limit")
+}
+
 func TestStepSaveLogReturnsFalseWhenDependenciesMissing(t *testing.T) {
 	started := time.Unix(1714333384, 0)
 	valid := &ExecutionRequest{