Compare commits

..
1 Commits
Author SHA1 Message Date
lukaszraczylo 78a6723143 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.
2026-10-05 01:57:05 +01:00
3 changed files with 46 additions and 4 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ 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
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) {
c.Refresh(ctx)
if len(c.onRefresh) == 0 || ctx.Err() != nil {
if ctx.Err() != nil {
return
}
v := c.View()
c.notify(ctx, c.View())
}
func (c *Collector) notify(ctx context.Context, v View) {
for _, fn := range c.onRefresh {
fn(ctx, v)
}
@@ -239,7 +242,12 @@ func (c *Collector) RefreshRepo(ctx context.Context, name string) (View, error)
c.mu.Lock()
c.repos[canon] = r
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) {
+34
View File
@@ -519,3 +519,37 @@ func TestPROpenDoesNotChangeCounts(t *testing.T) {
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):
}
}