mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-06-21 17:24:23 +02:00
- Adds `runner.post_task_script` and `runner.post_task_script_timeout` (default `5m`) to run a host executable after each task’s built-in cleanup (post-steps, container teardown, bind-workdir removal). - Stops task heartbeats via `Reporter.StopHeartbeats()` while the script runs so Gitea won’t assign overlapping work; the final task acknowledgement still happens in `reporter.Close()`. - Script output goes to the runner process log; non-zero exits are warned only and do not change the job result. - Documents lifecycle, offline behavior, timeouts, and Windows limits (`.ps1` not supported yet) in `docs/post-task-script.md`. Reviewed-on: https://gitea.com/gitea/runner/pulls/1026 Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
158 lines
4.8 KiB
Go
158 lines
4.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package run
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/internal/pkg/config"
|
|
"gitea.com/gitea/runner/internal/pkg/metrics"
|
|
"gitea.com/gitea/runner/internal/pkg/report"
|
|
|
|
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
func TestRunPostTaskScriptSkippedWhenEmpty(t *testing.T) {
|
|
r := &Runner{
|
|
cfg: &config.Config{},
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
|
require.NoError(t, err)
|
|
task := &runnerv1.Task{Id: 1, Context: taskCtx}
|
|
reporter := report.NewReporter(ctx, cancel, nil, task, r.cfg)
|
|
|
|
require.NotPanics(t, func() {
|
|
r.runPostTaskScript(ctx, reporter, task, "/workspace/owner/repo")
|
|
})
|
|
}
|
|
|
|
func TestRunPostTaskScriptNonZeroExitDoesNotPanic(t *testing.T) {
|
|
dir := t.TempDir()
|
|
scriptPath := filepath.Join(dir, "fail.sh")
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte("#!/bin/sh\nexit 2\n"), 0o700))
|
|
|
|
cfg, err := config.LoadDefault("")
|
|
require.NoError(t, err)
|
|
cfg.Runner.PostTaskScript = scriptPath
|
|
|
|
r := &Runner{cfg: cfg}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
|
require.NoError(t, err)
|
|
task := &runnerv1.Task{Id: 1, Context: taskCtx}
|
|
reporter := report.NewReporter(ctx, cancel, nil, task, cfg)
|
|
|
|
require.NotPanics(t, func() {
|
|
r.runPostTaskScript(ctx, reporter, task, "/workspace/owner/repo")
|
|
})
|
|
}
|
|
|
|
func TestPostTaskScriptContextUsesFullTimeout(t *testing.T) {
|
|
const timeout = 5 * time.Minute
|
|
|
|
// A task context that finished close to its own deadline must not truncate the
|
|
// script's budget: the script should still get its full configured timeout.
|
|
near, cancelNear := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancelNear()
|
|
scriptCtx, cancel := postTaskScriptContext(near, timeout)
|
|
defer cancel()
|
|
deadline, ok := scriptCtx.Deadline()
|
|
require.True(t, ok)
|
|
assert.Greater(t, time.Until(deadline), time.Minute, "script timeout truncated to task deadline")
|
|
|
|
// An already-cancelled task context must not cancel the script either.
|
|
cancelledCtx, cancelIt := context.WithCancel(context.Background())
|
|
cancelIt()
|
|
scriptCtx2, cancel2 := postTaskScriptContext(cancelledCtx, timeout)
|
|
defer cancel2()
|
|
assert.NoError(t, scriptCtx2.Err(), "script context inherited the cancelled task context")
|
|
}
|
|
|
|
func TestPostTaskScriptEnv(t *testing.T) {
|
|
cfg, err := config.LoadDefault("")
|
|
require.NoError(t, err)
|
|
|
|
r := &Runner{
|
|
cfg: cfg,
|
|
envs: map[string]string{"BASE": "1"},
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
taskCtx, err := structpb.NewStruct(map[string]any{
|
|
"run_id": "99",
|
|
"repository": "acme/widget",
|
|
})
|
|
require.NoError(t, err)
|
|
task := &runnerv1.Task{Id: 3, Context: taskCtx}
|
|
reporter := report.NewReporter(ctx, cancel, nil, task, cfg)
|
|
setReporterJobResult(t, reporter, runnerv1.Result_RESULT_FAILURE)
|
|
|
|
env := r.postTaskScriptEnv(reporter, task, "/tmp/workspace")
|
|
assert.Equal(t, "1", env["BASE"])
|
|
assert.Equal(t, "3", env["GITEA_TASK_ID"])
|
|
assert.Equal(t, "99", env["GITEA_RUN_ID"])
|
|
assert.Equal(t, "acme/widget", env["GITEA_REPOSITORY"])
|
|
assert.Equal(t, "/tmp/workspace", env["GITEA_WORKSPACE"])
|
|
assert.Equal(t, "failure", env["GITEA_JOB_RESULT"])
|
|
}
|
|
|
|
func TestRunPostTaskScriptIntegration(t *testing.T) {
|
|
dir := t.TempDir()
|
|
outFile := filepath.Join(dir, "out.txt")
|
|
scriptPath := filepath.Join(dir, "post-task.sh")
|
|
script := "#!/bin/sh\nprintf '%s %s %s' \"$GITEA_TASK_ID\" \"$GITEA_JOB_RESULT\" \"$CUSTOM\" > \"" + outFile + "\"\n"
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o700))
|
|
|
|
cfg, err := config.LoadDefault("")
|
|
require.NoError(t, err)
|
|
cfg.Runner.PostTaskScript = scriptPath
|
|
|
|
r := &Runner{
|
|
cfg: cfg,
|
|
envs: map[string]string{"CUSTOM": "runner-env"},
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
|
require.NoError(t, err)
|
|
task := &runnerv1.Task{Id: 11, Context: taskCtx}
|
|
reporter := report.NewReporter(ctx, cancel, nil, task, cfg)
|
|
setReporterJobResult(t, reporter, runnerv1.Result_RESULT_SUCCESS)
|
|
|
|
r.runPostTaskScript(ctx, reporter, task, "/workspace/acme/repo")
|
|
|
|
content, err := os.ReadFile(outFile)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "11 success runner-env", string(content))
|
|
}
|
|
|
|
func setReporterJobResult(t *testing.T, reporter *report.Reporter, result runnerv1.Result) {
|
|
t.Helper()
|
|
require.NoError(t, reporter.Fire(&log.Entry{
|
|
Time: time.Now(),
|
|
Message: "job finished",
|
|
Data: log.Fields{
|
|
"stage": "Post",
|
|
"jobResult": metrics.ResultToStatusLabel(result),
|
|
},
|
|
}))
|
|
}
|