fix(web): run automatic actions after a repository rescan

The rescan button of one repository now triggers automatic actions too. The webhook calls run in the background so the response does not wait for them.
This commit is contained in:
2026-10-05 01:57:05 +01:00
parent c2ce5391c0
commit 829c2aa37b
3 changed files with 46 additions and 4 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ actions:
### Automatic actions ### Automatic actions
An action with `auto: true` fires without a click. After every scan, and once at startup, the server sends the webhook for each current alert whose kind is in `kinds`. It sends once per action, repo, alert URL and head commit. A new commit on a pull request is a new send. An action with `auto: true` fires without a click. After every scan, after every single-repository rescan, and once at startup, the server sends the webhook for each current alert whose kind is in `kinds`. It sends once per action, repo, alert URL and head commit. A new commit on a pull request is a new send.
```yaml ```yaml
actions: actions:
+11 -3
View File
@@ -119,10 +119,13 @@ func (c *Collector) OnRefresh(fn func(context.Context, View)) { c.onRefresh = ap
func (c *Collector) refreshAndNotify(ctx context.Context) { func (c *Collector) refreshAndNotify(ctx context.Context) {
c.Refresh(ctx) c.Refresh(ctx)
if len(c.onRefresh) == 0 || ctx.Err() != nil { if ctx.Err() != nil {
return return
} }
v := c.View() c.notify(ctx, c.View())
}
func (c *Collector) notify(ctx context.Context, v View) {
for _, fn := range c.onRefresh { for _, fn := range c.onRefresh {
fn(ctx, v) fn(ctx, v)
} }
@@ -239,7 +242,12 @@ func (c *Collector) RefreshRepo(ctx context.Context, name string) (View, error)
c.mu.Lock() c.mu.Lock()
c.repos[canon] = r c.repos[canon] = r
c.mu.Unlock() c.mu.Unlock()
return c.View(), nil v := c.View()
if len(c.onRefresh) > 0 {
// The request context ends with the response, which must not wait for webhooks.
go c.notify(context.WithoutCancel(ctx), v)
}
return v, nil
} }
func (c *Collector) configured(name string) (string, bool) { func (c *Collector) configured(name string) (string, bool) {
+34
View File
@@ -519,3 +519,37 @@ func TestPROpenDoesNotChangeCounts(t *testing.T) {
t.Fatalf("view = %+v", v) t.Fatalf("view = %+v", v)
} }
} }
func TestRefreshRepoRunsRefreshHooksWithTheNewView(t *testing.T) {
f := &fakeFetcher{fn: func(name string) (model.Repo, error) {
return model.Repo{FullName: name, URL: "u", Issues: make([]model.Issue, 3)}, nil
}}
c := newCollector(f, "o/a")
got := make(chan View, 1)
c.OnRefresh(func(_ context.Context, v View) { got <- v })
if _, err := c.RefreshRepo(context.Background(), "o/a"); err != nil {
t.Fatal(err)
}
select {
case v := <-got:
if len(v.Repos) != 1 || len(v.Repos[0].Issues) != 3 {
t.Fatalf("hook saw a stale view: %+v", v.Repos)
}
case <-time.After(2 * time.Second):
t.Fatal("refresh hook did not run after a repository refresh")
}
}
func TestRefreshRepoFailureSkipsRefreshHooks(t *testing.T) {
c := newCollector(&fakeFetcher{fn: func(string) (model.Repo, error) { return model.Repo{}, errors.New("nope") }}, "o/a")
called := make(chan struct{}, 1)
c.OnRefresh(func(context.Context, View) { called <- struct{}{} })
if _, err := c.RefreshRepo(context.Background(), "unknown/x"); err == nil {
t.Fatal("unknown repository must fail")
}
select {
case <-called:
t.Fatal("hook ran for a refused refresh")
case <-time.After(200 * time.Millisecond):
}
}