feat(git): push webhook receiver + repo connect on service create

New public POST /api/git/webhooks/🆔 verifies provider signatures
(GitHub/Gitea HMAC-SHA256, GitLab token), filters branch pushes, matches
connected repo to services by clone_url/full_name + branch, inserts
deployment rows and enqueues runDeploymentAndSync (trigger=webhook).
Service-create dialog now connects the selected provider repo and
registers a push webhook (409-tolerant via connected-repo lookup).

api-client: connectGitRepository, listConnectedGitRepositories,
createGitWebhook typed from generated schemas. GitWebhook spec schema
corrected (events array + branch field).

Authored By: TDvorak <[email protected]>
This commit is contained in:
Tomas Dvorak committed 2026-09-17 10:30:36 +02:00
1 parent 93e3074477
commit 25703592c6
8 files changed
+550 -11

No files matched your search

@@ -0,0 +1,84 @@
package api
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http/httptest"
"testing"
)
func signBody(t *testing.T, secret string, body []byte) string {
t.Helper()
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
return hex.EncodeToString(mac.Sum(nil))
}
func TestVerifyGitWebhookSignature(t *testing.T) {
body := []byte(`{"ref":"refs/heads/main"}`)
secret := "test-secret"
valid := signBody(t, secret, body)
cases := []struct {
name string
provider string
headers map[string]string
want bool
}{
{
name: "github sha256 prefix",
provider: "github",
headers: map[string]string{"X-Hub-Signature-256": "sha256=" + valid},
want: true,
},
{
name: "gitea signature",
provider: "gitea",
headers: map[string]string{"X-Gitea-Signature": valid},
want: true,
},
{
name: "gitlab token",
provider: "gitlab",
headers: map[string]string{"X-Gitlab-Token": secret},
want: true,
},
{
name: "gitlab rejects wrong token",
provider: "gitlab",
headers: map[string]string{"X-Gitlab-Token": "wrong"},
want: false,
},
{
name: "github rejects wrong signature",
provider: "github",
headers: map[string]string{"X-Hub-Signature-256": "sha256=" + signBody(t, "other", body)},
want: false,
},
{
name: "missing signature",
provider: "github",
headers: map[string]string{},
want: false,
},
{
name: "malformed hex",
provider: "github",
headers: map[string]string{"X-Hub-Signature-256": "sha256=zzz"},
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("POST", "/api/git/webhooks/x", nil)
for k, v := range tc.headers {
req.Header.Set(k, v)
}
if got := verifyGitWebhookSignature(tc.provider, secret, req, body); got != tc.want {
t.Fatalf("verifyGitWebhookSignature = %v, want %v", got, tc.want)
}
})
}
}