fix: fix panics, enable the forcetypeassert lint (#1123)

An unchecked type assertion panics on input it did not expect, as a missing `tool_cache` key did in https://gitea.com/gitea/runner/pulls/1122.

Every flagged site is now handled where it can fail, or typed so it cannot: a `lock.Keyed` replaces the two `sync.Map` mutex registries, and the reporter's outputs carry an explicit sent flag. Mocks keep their assertions, a mismatch there is a setup error the panic names.

Bugs it turned up (only the first is reachable from workflows):

1. A scalar `matrix.include` or `matrix.exclude`, e.g. `include: foo` or `include: [1, 2]`, panicked the runner with `interface conversion: interface {} is string, not map[string]interface {}`. Verified against `main`, it is now a workflow error. `OnSchedule` panicked the same way on a malformed `on.schedule` entry.
1. `ExternalURL()` panicked on the nil listener after `Close()`, the port is now resolved once at startup.
1. `errors.Is(err, git.ErrShortRef)` followed by `err.(*git.Error)` panics as soon as anything wraps that error, so it is `errors.As` now.
1. An output name the server acknowledged without ever being sent one was recorded as sent forever, which silently dropped a later value for that name.

48576ab3e5 fixes one discovered issue: a matrix key holding a nested object was logged and then run as if the job had no matrix, so it now fails like an unknown `exclude` key.
Reviewed-on: https://gitea.com/gitea/runner/pulls/1123
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-07-30 07:09:53 +00:00
committed by silverwind
parent 0192861155
commit 3f7fd16ea1
15 changed files with 210 additions and 109 deletions

View File

@@ -7,6 +7,8 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"
"sync/atomic"
"testing"
@@ -1011,33 +1013,59 @@ func TestReporter_SetOutputs(t *testing.T) {
r := &Reporter{state: &runnerv1.TaskState{}}
r.SetOutputs(map[string]string{"foo": "bar"})
got, ok := r.outputs.Load("foo")
got, ok := r.outputs["foo"]
require.True(t, ok)
assert.Equal(t, "bar", got)
assert.Equal(t, "bar", got.value)
// first value wins: a later write to the same key is ignored
r.SetOutputs(map[string]string{"foo": "baz"})
got, _ = r.outputs.Load("foo")
assert.Equal(t, "bar", got)
assert.Equal(t, "bar", r.outputs["foo"].value)
// keys longer than maxOutputKeyLen are dropped
longKey := strings.Repeat("k", maxOutputKeyLen+1)
r.SetOutputs(map[string]string{longKey: "v"})
_, ok = r.outputs.Load(longKey)
_, ok = r.outputs[longKey]
assert.False(t, ok)
// values longer than maxOutputValueLen are dropped
longValue := strings.Repeat("v", maxOutputValueLen+1)
r.SetOutputs(map[string]string{"big": longValue})
_, ok = r.outputs.Load("big")
_, ok = r.outputs["big"]
assert.False(t, ok)
// a value at exactly the limit is still stored
maxValue := strings.Repeat("v", maxOutputValueLen)
r.SetOutputs(map[string]string{"atlimit": maxValue})
got, ok = r.outputs.Load("atlimit")
got, ok = r.outputs["atlimit"]
require.True(t, ok)
assert.Len(t, got, maxOutputValueLen)
assert.Len(t, got.value, maxOutputValueLen)
}
// An output the server acknowledged is not reported again.
func TestReporter_OutputsSentOnce(t *testing.T) {
client := mocks.NewClient(t)
var reported []map[string]string
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
func(_ context.Context, req *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
reported = append(reported, req.Msg.Outputs)
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{SentOutputs: slices.Collect(maps.Keys(req.Msg.Outputs))}), nil
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
taskCtx, err := structpb.NewStruct(map[string]any{})
require.NoError(t, err)
cfg, _ := config.LoadDefault("")
r := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
r.SetOutputs(map[string]string{"foo": "bar"})
require.NoError(t, r.ReportState(false))
assert.True(t, r.outputs["foo"].sent)
require.NoError(t, r.ReportState(true))
require.Len(t, reported, 2)
assert.Equal(t, map[string]string{"foo": "bar"}, reported[0])
assert.Empty(t, reported[1])
}
func TestReporter_EffectiveCloseTimeout(t *testing.T) {