Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions internal/app/gitrepo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package app

import "strings"

// normalizeGitRepo reduces a git remote URL to "host/owner/repo" so webhook
// payloads match the URL stored on an app regardless of scheme, credentials,
// a trailing ".git" or slash, or letter case. GitHub sends clone_url with
// ".git" while users often paste the browser URL without it.
//
// https://github.com/Org/Repo.git -> github.com/org/repo
// git@github.com:org/repo.git -> github.com/org/repo
// ssh://git@github.com/org/repo/ -> github.com/org/repo
func normalizeGitRepo(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
if i := strings.Index(s, "://"); i >= 0 {
s = s[i+3:]
} else if at := strings.Index(s, "@"); at >= 0 {
// scp-like syntax: user@host:path
if colon := strings.Index(s[at:], ":"); colon >= 0 {
s = s[:at+colon] + "/" + s[at+colon+1:]
}
}
if slash := strings.Index(s, "/"); slash >= 0 {
if at := strings.LastIndex(s[:slash], "@"); at >= 0 {
s = s[at+1:]
}
}
s = strings.TrimRight(s, "/")
s = strings.TrimSuffix(s, ".git")
return strings.TrimRight(s, "/")
}

// webhookDeployable reports whether a push webhook may redeploy an app in the
// given status. Failed apps are included so a deploy that failed before its
// image existed recovers on the next push; stopped apps stay stopped.
func webhookDeployable(status string) bool {
return status == StatusRunning || status == StatusFailed
}
71 changes: 71 additions & 0 deletions internal/app/gitrepo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package app

import "testing"

func TestNormalizeGitRepo(t *testing.T) {
want := "github.com/amg-id/cmsapi-3"
for _, in := range []string{
"https://github.com/amg-id/cmsapi-3",
"https://github.com/amg-id/cmsapi-3.git",
"https://github.com/amg-id/cmsapi-3/",
" https://GitHub.com/AMG-ID/cmsapi-3.git ",
"http://github.com/amg-id/cmsapi-3",
"https://user:token@github.com/amg-id/cmsapi-3.git",
"git@github.com:amg-id/cmsapi-3.git",
"ssh://git@github.com/amg-id/cmsapi-3.git",
"github.com/amg-id/cmsapi-3",
} {
if got := normalizeGitRepo(in); got != want {
t.Errorf("normalizeGitRepo(%q) = %q, want %q", in, got, want)
}
}
if a, b := normalizeGitRepo("https://github.com/amg-id/cmsapi-3"), normalizeGitRepo("https://github.com/amg-id/cmsapi-2"); a == b {
t.Errorf("different repos normalized equal: %q", a)
}
}

func TestWebhookDeployable(t *testing.T) {
cases := map[string]bool{
StatusRunning: true,
StatusFailed: true,
StatusStopped: false,
StatusDeploying: false,
StatusCreated: false,
StatusDraft: false,
}
for status, want := range cases {
if got := webhookDeployable(status); got != want {
t.Errorf("webhookDeployable(%q) = %v, want %v", status, got, want)
}
}
}

func TestRepo_FindAllByGitRepoNormalizesURL(t *testing.T) {
repo, srvRepo := setupRepo(t)
srvID := createTestServer(t, srvRepo)
mk := func(name, gitRepo, branch, status string) {
t.Helper()
a := &App{Name: name, ServerID: srvID, Compose: "services: {}", GitRepo: gitRepo, GitBranch: branch, Status: status}
if err := repo.Create(a); err != nil {
t.Fatal(err)
}
}
mk("no-suffix", "https://github.com/amg-id/cmsapi-3", "staging", StatusFailed)
mk("with-suffix", "https://github.com/amg-id/cmsapi-3.git", "staging", StatusRunning)
mk("other-branch", "https://github.com/amg-id/cmsapi-3", "main", StatusRunning)
mk("other-repo", "https://github.com/amg-id/cmsapi-2.git", "staging", StatusRunning)
mk("draft", "https://github.com/amg-id/cmsapi-3.git", "staging", StatusDraft)
mk("no-repo", "", "staging", StatusRunning)

apps, err := repo.FindAllByGitRepo("https://github.com/amg-id/cmsapi-3.git", "staging")
if err != nil {
t.Fatal(err)
}
var names []string
for _, a := range apps {
names = append(names, a.Name)
}
if len(names) != 2 || names[0] != "no-suffix" || names[1] != "with-suffix" {
t.Fatalf("got %v, want [no-suffix with-suffix]", names)
}
}
15 changes: 14 additions & 1 deletion internal/app/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,21 @@ func (r *Repository) Get(id int64) (*App, error) {
return &a, nil
}

// FindAllByGitRepo returns non-draft apps on branch whose git_repo points at
// the same repository as repo, compared with normalizeGitRepo.
func (r *Repository) FindAllByGitRepo(repo, branch string) ([]App, error) {
return r.queryApps("WHERE git_repo = ? AND git_branch = ? AND status != 'draft' ORDER BY id", repo, branch)
candidates, err := r.queryApps("WHERE git_repo IS NOT NULL AND git_repo != '' AND git_branch = ? AND status != 'draft' ORDER BY id", branch)
if err != nil {
return nil, err
}
want := normalizeGitRepo(repo)
var apps []App
for _, a := range candidates {
if normalizeGitRepo(a.GitRepo) == want {
apps = append(apps, a)
}
}
return apps, nil
}

func (r *Repository) ListByServer(serverID int64) ([]App, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/app/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func (s *Service) DeployByGit(repo, branch, commitSHA string) {
return
}
for _, app := range apps {
if app.Status != StatusRunning {
if !webhookDeployable(app.Status) {
log.Printf("Webhook: app %q is %s, skipping deploy", app.Name, app.Status)
continue
}
Expand Down
Loading