0a4e6f0b7e063927c7128756e1c1b3db871edabf

Author
Kieran Klukas <kieran@dunkirk.sh>
Committer
Kieran Klukas <kieran@dunkirk.sh>
Date

Message

fix: send the visibility webhook only when visibility changes

The repository visibility webhook fired every time visibility was set,
even when it already had that value, because the check compared the new
value against itself. It now compares against the value saved before
the change, so subscribers only hear about real changes. Setting the
visibility of a missing repository now reports that it was not found
before touching any files.

💘 Generated with Crush

Assisted-by: Crush:claude-opus-5-5

Diff

  1diff --git a/pkg/backend/repo.go b/pkg/backend/repo.go
  2index 0d2a5b546dbc0c059a495b3fd54853af174943bb..a0965338ec23139778690afa48b4af5a8f083412 100644
  3--- a/pkg/backend/repo.go
  4+++ b/pkg/backend/repo.go
  5@@ -664,8 +664,15 @@ func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) err
  6 	name = utils.SanitizeRepo(name)
  7 	rp := filepath.Join(d.repoPath(name))
  8 
  9+	var changed bool
 10 	if err := db.WrapError(
 11 		d.db.TransactionContext(ctx, func(tx *db.Tx) error {
 12+			m, err := d.store.GetRepoByName(ctx, tx, name)
 13+			if err != nil {
 14+				return err
 15+			}
 16+			changed = m.Private != private
 17+
 18 			fp := filepath.Join(rp, "git-daemon-export-ok")
 19 			if !private {
 20 				if err := os.WriteFile(fp, []byte{}, fs.ModePerm); err != nil {
 21@@ -684,27 +691,27 @@ func (d *Backend) SetPrivate(ctx context.Context, name string, private bool) err
 22 			return d.store.SetRepoIsPrivateByName(ctx, tx, name, private)
 23 		}),
 24 	); err != nil {
 25+		if errors.Is(err, db.ErrRecordNotFound) {
 26+			return proto.ErrRepoNotFound
 27+		}
 28 		return err
 29 	}
 30 
 31-	user := proto.UserFromContext(ctx)
 32+	if !changed {
 33+		return nil
 34+	}
 35+
 36 	repo, err := d.Repository(ctx, name)
 37 	if err != nil {
 38 		return err
 39 	}
 40 
 41-	if repo.IsPrivate() != !private {
 42-		wh, err := webhook.NewRepositoryEvent(ctx, user, repo, webhook.RepositoryEventActionVisibilityChange)
 43-		if err != nil {
 44-			return err
 45-		}
 46-
 47-		if err := webhook.SendEvent(ctx, wh); err != nil {
 48-			return err
 49-		}
 50+	wh, err := webhook.NewRepositoryEvent(ctx, proto.UserFromContext(ctx), repo, webhook.RepositoryEventActionVisibilityChange)
 51+	if err != nil {
 52+		return err
 53 	}
 54 
 55-	return nil
 56+	return webhook.SendEvent(ctx, wh)
 57 }
 58 
 59 // SetProjectName sets the project name of a repository.
 60diff --git a/pkg/backend/repo_visibility_webhook_test.go b/pkg/backend/repo_visibility_webhook_test.go
 61new file mode 100644
 62index 0000000000000000000000000000000000000000..8d54e96c2fa8344462e02f0e4bb9e3748b0d291e
 63--- /dev/null
 64+++ b/pkg/backend/repo_visibility_webhook_test.go
 65@@ -0,0 +1,60 @@
 66+package backend
 67+
 68+import (
 69+	"context"
 70+	"testing"
 71+
 72+	"github.com/charmbracelet/soft-serve/pkg/config"
 73+	"github.com/charmbracelet/soft-serve/pkg/db"
 74+	"github.com/charmbracelet/soft-serve/pkg/proto"
 75+	"github.com/charmbracelet/soft-serve/pkg/store"
 76+	"github.com/charmbracelet/soft-serve/pkg/webhook"
 77+	"github.com/matryer/is"
 78+)
 79+
 80+// TestSetPrivateWebhookOnlyOnChange verifies that the visibility change
 81+// webhook fires when visibility changes and not when it is set to the
 82+// value it already has.
 83+func TestSetPrivateWebhookOnlyOnChange(t *testing.T) {
 84+	is := is.New(t)
 85+	be, cfg := newTestBackend(t)
 86+
 87+	ctx := context.Background()
 88+	ctx = config.WithContext(ctx, cfg)
 89+	ctx = db.WithContext(ctx, be.db)
 90+	ctx = store.WithContext(ctx, be.store)
 91+	admin, err := be.User(ctx, "admin")
 92+	is.NoErr(err)
 93+	ctx = proto.WithUserContext(ctx, admin)
 94+
 95+	repo, err := be.CreateRepository(ctx, "repo", nil, proto.RepositoryOptions{})
 96+	is.NoErr(err)
 97+
 98+	// Deliveries to this address fail fast, but are still recorded.
 99+	var hookID int64
100+	is.NoErr(be.db.TransactionContext(ctx, func(tx *db.Tx) error {
101+		hookID, err = be.store.CreateWebhook(ctx, tx, repo.ID(), "http://127.0.0.1:1/", "", int(webhook.ContentTypeJSON), true)
102+		if err != nil {
103+			return err
104+		}
105+		return be.store.CreateWebhookEvents(ctx, tx, hookID, []int{int(webhook.EventRepositoryVisibilityChange)})
106+	}))
107+
108+	deliveries := func() int {
109+		ds, err := be.store.ListWebhookDeliveriesByWebhookID(ctx, be.db, hookID)
110+		is.NoErr(err)
111+		return len(ds)
112+	}
113+
114+	is.NoErr(be.SetPrivate(ctx, "repo", false))
115+	is.Equal(deliveries(), 0)
116+
117+	is.NoErr(be.SetPrivate(ctx, "repo", true))
118+	is.Equal(deliveries(), 1)
119+
120+	is.NoErr(be.SetPrivate(ctx, "repo", true))
121+	is.Equal(deliveries(), 1)
122+
123+	is.NoErr(be.SetPrivate(ctx, "repo", false))
124+	is.Equal(deliveries(), 2)
125+}