feat: upload job summary when supported (#917)

- Add GitHub-style Actions **job summaries** support (writes to `GITHUB_STEP_SUMMARY` / `workflow/SUMMARY.md`) and render them in the run UI.
- Gitea stores summaries internally (DB) and serves them in the run view payload.
- `act_runner` uploads the summary **only when Gitea advertises support** (`X-Gitea-Actions-Capabilities: job-summary`), and warns on upload failures without failing the job.

## Compatibility
- New Gitea + old runner: no upload → no summary shown (no behavior change)
- New runner + old Gitea: capability not advertised → runner skips upload (no behavior change)

## Issue
- Fixes go-gitea/gitea#23721

Reviewed-on: https://gitea.com/gitea/runner/pulls/917
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-by: Zettat123 <39446+zettat123@noreply.gitea.com>
This commit is contained in:
Nicolas
2026-06-08 17:24:03 +00:00
parent 1073c8bfec
commit 53c4db6a4b
6 changed files with 624 additions and 29 deletions

View File

@@ -148,6 +148,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
log.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
}
runner.SetCapabilitiesFromDeclare(resp)
if cfg.Metrics.Enabled {
metrics.Init()

View File

@@ -47,6 +47,7 @@ type Runner struct {
labels labels.Labels
envs map[string]string
cacheHandler *artifactcache.Handler
capabilities string
runningTasks sync.Map
runningCount atomic.Int64
@@ -185,6 +186,14 @@ func (r *Runner) cleanupStaleTaskDirs(ctx context.Context, workdirRoot string) {
}
}
func (r *Runner) SetCapabilitiesFromDeclare(resp *connect.Response[runnerv1.DeclareResponse]) {
if resp == nil {
return
}
// Capability negotiation is done via response headers to avoid a hard proto bump.
r.capabilities = strings.TrimSpace(resp.Header().Get("X-Gitea-Actions-Capabilities"))
}
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
if _, ok := r.runningTasks.Load(task.Id); ok {
return fmt.Errorf("task %d is already running", task.Id)
@@ -219,9 +228,10 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
}
func (r *Runner) cloneEnvs() map[string]string {
// +3 reserves space for the per-task keys injected by run():
// ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_RUNTIME_TOKEN.
envs := make(map[string]string, len(r.envs)+3)
// Reserve space for the per-task keys injected by run():
// ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_RUNTIME_TOKEN,
// GITEA_ACTIONS_CAPABILITIES, GITEA_RUN_ID.
envs := make(map[string]string, len(r.envs)+5)
maps.Copy(envs, r.envs)
return envs
}
@@ -261,6 +271,13 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
taskContext := task.Context.Fields
envs := r.cloneEnvs()
if r.capabilities != "" {
envs["GITEA_ACTIONS_CAPABILITIES"] = r.capabilities
}
if v := taskContext["run_id"].GetStringValue(); v != "" {
envs["GITEA_RUN_ID"] = v
}
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
r.getDefaultActionsURL(task),
r.client.Address())