From 55d2ba565e103b69d34788c8fa96313ed2820991 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 17:03:16 +0530 Subject: [PATCH 01/47] init project and implement logger --- .gitignore | 6 ++++++ go.mod | 3 +++ logger/logger.go | 39 +++++++++++++++++++++++++++++++++++++++ logger/logger_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ main.go | 4 ++++ 5 files changed, 94 insertions(+) create mode 100644 go.mod create mode 100644 logger/logger.go create mode 100644 logger/logger_test.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore index 66fd13c..b87c03c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,9 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# Logs +*.log + +# IDE files +.idea/ \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5727228 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/DeathVenom54/pm2-deploy-inator + +go 1.17 diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..8d5aa0a --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,39 @@ +package logger + +import ( + "log" + "os" +) + +func writeToFile(file, text string) error { + f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + + defer func(f *os.File) { + err := f.Close() + if err != nil { + log.Fatalf("Error while closing file %s: %s\n", file, err) + } + }(f) + + log.SetOutput(f) + log.Println(text) + + return nil +} + +func Log(message string) { + err := writeToFile("all.log", message) + if err != nil { + log.Fatalf("Error while writing log to normal.log: %s", err) + } +} + +func Error(error string) { + err := writeToFile("error.log", error) + if err != nil { + log.Fatalf("Error while writing log to error.log: %s", err) + } +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..0ab07b3 --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,42 @@ +package logger + +import ( + "fmt" + "io/ioutil" + "strings" + "testing" +) + +func TestLogger(t *testing.T) { + runLogTest(t, "This is a test log.", "all.log") + runLogTest(t, "This is a test error.", "error.log") +} + +func runLogTest(t *testing.T, message, filename string) { + t.Run(fmt.Sprintf("should add \"%s\" to %s", message, filename), func(t *testing.T) { + if filename == "all.log" { + Log(message) + } else { + Error(message) + } + + // read file to make sure it has the log at the end + log, err := ioutil.ReadFile(filename) + handle(t, err) + + success := strings.HasSuffix(string(log), message+"\n") + if !success { + t.Errorf("%s doesn't end with \"%s\\n\"", filename, message) + } + + // write a different log to prevent + // a false positive on the next test run + Log("Reset") + }) +} + +func handle(t *testing.T, err error) { + if err != nil { + t.Error(err) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..da29a2c --- /dev/null +++ b/main.go @@ -0,0 +1,4 @@ +package main + +func main() { +} From 559f5fb515d9f1e9e7302dbcbd463eb376067b10 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 19:21:01 +0530 Subject: [PATCH 02/47] rename project and add chi --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5727228..c6400f5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module github.com/DeathVenom54/pm2-deploy-inator +module github.com/DeathVenom54/github-deploy-inator go 1.17 + +require github.com/go-chi/chi/v5 v5.0.7 // indirect From 66c91da1bc76e6833bc8ad9e364e4c5a8515bd8b Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 19:21:56 +0530 Subject: [PATCH 03/47] log to stdout and stderr --- logger/logger.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/logger/logger.go b/logger/logger.go index 8d5aa0a..e0c5e5c 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -1,6 +1,7 @@ package logger import ( + "fmt" "log" "os" ) @@ -25,6 +26,8 @@ func writeToFile(file, text string) error { } func Log(message string) { + fmt.Println(message) + err := writeToFile("all.log", message) if err != nil { log.Fatalf("Error while writing log to normal.log: %s", err) @@ -32,6 +35,8 @@ func Log(message string) { } func Error(error string) { + log.Println(error) + err := writeToFile("error.log", error) if err != nil { log.Fatalf("Error while writing log to error.log: %s", err) From e53df2c243084324f5a14d53e4dd9385b9497993 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 20:12:04 +0530 Subject: [PATCH 04/47] test for github webhooks --- handlers/index.go | 15 +++++++++++++++ logger/logger.go | 4 ++-- main.go | 14 ++++++++++++++ router/main.go | 18 ++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 handlers/index.go create mode 100644 router/main.go diff --git a/handlers/index.go b/handlers/index.go new file mode 100644 index 0000000..1a5ee86 --- /dev/null +++ b/handlers/index.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "fmt" + "net/http" +) + +func Index(w http.ResponseWriter, r *http.Request) { + fmt.Println(r.Body) + + _, err := w.Write([]byte("Hello")) + if err != nil { + return + } +} diff --git a/logger/logger.go b/logger/logger.go index e0c5e5c..edcf53b 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -34,10 +34,10 @@ func Log(message string) { } } -func Error(error string) { +func Error(error error) { log.Println(error) - err := writeToFile("error.log", error) + err := writeToFile("error.log", error.Error()) if err != nil { log.Fatalf("Error while writing log to error.log: %s", err) } diff --git a/main.go b/main.go index da29a2c..df209e4 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,18 @@ package main +import ( + "github.com/DeathVenom54/github-deploy-inator/logger" + "github.com/DeathVenom54/github-deploy-inator/router" + "net/http" +) + func main() { + r := router.CreateRouter() + + err := http.ListenAndServe(":4567", r) + if err != nil { + logger.Error(err) + } + + logger.Log("Listening...") } diff --git a/router/main.go b/router/main.go new file mode 100644 index 0000000..c2e814c --- /dev/null +++ b/router/main.go @@ -0,0 +1,18 @@ +package router + +import ( + "github.com/DeathVenom54/github-deploy-inator/handlers" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +func CreateRouter() *chi.Mux { + router := chi.NewRouter() + + router.Use(middleware.Recoverer) + router.Use(middleware.Logger) + + router.Post("/", handlers.Index) + + return router +} From 99df3512cea9b4f2f8b9346603d32e36c524f34a Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 20:17:33 +0530 Subject: [PATCH 05/47] test for github webhooks 2 --- handlers/index.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/handlers/index.go b/handlers/index.go index 1a5ee86..ebbd837 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -2,13 +2,19 @@ package handlers import ( "fmt" + "github.com/DeathVenom54/github-deploy-inator/logger" + "io/ioutil" "net/http" ) func Index(w http.ResponseWriter, r *http.Request) { - fmt.Println(r.Body) + bodyBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + logger.Error(err) + } + fmt.Println(string(bodyBytes)) - _, err := w.Write([]byte("Hello")) + _, err = w.Write([]byte("Hello")) if err != nil { return } From 658443c38c7693ea9fe59c29105cdeb8f0b94f83 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 20:34:19 +0530 Subject: [PATCH 06/47] test for github webhooks 3 --- handlers/index.go | 13 +-- structs/GithubWebhook.go | 189 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 structs/GithubWebhook.go diff --git a/handlers/index.go b/handlers/index.go index ebbd837..8bd2bbb 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -1,20 +1,23 @@ package handlers import ( + "encoding/json" "fmt" "github.com/DeathVenom54/github-deploy-inator/logger" - "io/ioutil" + "github.com/DeathVenom54/github-deploy-inator/structs" "net/http" ) func Index(w http.ResponseWriter, r *http.Request) { - bodyBytes, err := ioutil.ReadAll(r.Body) - if err != nil { + decoder := json.NewDecoder(r.Body) + + var data structs.GithubWebhook + if err := decoder.Decode(&data); err != nil { logger.Error(err) } - fmt.Println(string(bodyBytes)) + fmt.Println(data) - _, err = w.Write([]byte("Hello")) + _, err := w.Write([]byte("Hello")) if err != nil { return } diff --git a/structs/GithubWebhook.go b/structs/GithubWebhook.go new file mode 100644 index 0000000..834ece4 --- /dev/null +++ b/structs/GithubWebhook.go @@ -0,0 +1,189 @@ +package structs + +import "time" + +type GithubWebhook struct { + Ref string `json:"ref"` + Before string `json:"before"` + After string `json:"after"` + Repository Repository `json:"repository"` + Pusher Pusher `json:"pusher"` + Sender Sender `json:"sender"` + Created bool `json:"created"` + Deleted bool `json:"deleted"` + Forced bool `json:"forced"` + BaseRef interface{} `json:"base_ref"` + Compare string `json:"compare"` + Commits []Commits `json:"commits"` + HeadCommit HeadCommit `json:"head_commit"` +} +type Owner struct { + Name string `json:"name"` + Email string `json:"email"` + Login string `json:"login"` + ID int `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` +} +type License struct { + Key string `json:"key"` + Name string `json:"name"` + SpdxID string `json:"spdx_id"` + URL string `json:"url"` + NodeID string `json:"node_id"` +} +type Repository struct { + ID int `json:"id"` + NodeID string `json:"node_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Private bool `json:"private"` + Owner Owner `json:"owner"` + HTMLURL string `json:"html_url"` + Description string `json:"description"` + Fork bool `json:"fork"` + URL string `json:"url"` + ForksURL string `json:"forks_url"` + KeysURL string `json:"keys_url"` + CollaboratorsURL string `json:"collaborators_url"` + TeamsURL string `json:"teams_url"` + HooksURL string `json:"hooks_url"` + IssueEventsURL string `json:"issue_events_url"` + EventsURL string `json:"events_url"` + AssigneesURL string `json:"assignees_url"` + BranchesURL string `json:"branches_url"` + TagsURL string `json:"tags_url"` + BlobsURL string `json:"blobs_url"` + GitTagsURL string `json:"git_tags_url"` + GitRefsURL string `json:"git_refs_url"` + TreesURL string `json:"trees_url"` + StatusesURL string `json:"statuses_url"` + LanguagesURL string `json:"languages_url"` + StargazersURL string `json:"stargazers_url"` + ContributorsURL string `json:"contributors_url"` + SubscribersURL string `json:"subscribers_url"` + SubscriptionURL string `json:"subscription_url"` + CommitsURL string `json:"commits_url"` + GitCommitsURL string `json:"git_commits_url"` + CommentsURL string `json:"comments_url"` + IssueCommentURL string `json:"issue_comment_url"` + ContentsURL string `json:"contents_url"` + CompareURL string `json:"compare_url"` + MergesURL string `json:"merges_url"` + ArchiveURL string `json:"archive_url"` + DownloadsURL string `json:"downloads_url"` + IssuesURL string `json:"issues_url"` + PullsURL string `json:"pulls_url"` + MilestonesURL string `json:"milestones_url"` + NotificationsURL string `json:"notifications_url"` + LabelsURL string `json:"labels_url"` + ReleasesURL string `json:"releases_url"` + DeploymentsURL string `json:"deployments_url"` + CreatedAt int `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PushedAt int `json:"pushed_at"` + GitURL string `json:"git_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + SvnURL string `json:"svn_url"` + Homepage interface{} `json:"homepage"` + Size int `json:"size"` + StargazersCount int `json:"stargazers_count"` + WatchersCount int `json:"watchers_count"` + Language interface{} `json:"language"` + HasIssues bool `json:"has_issues"` + HasProjects bool `json:"has_projects"` + HasDownloads bool `json:"has_downloads"` + HasWiki bool `json:"has_wiki"` + HasPages bool `json:"has_pages"` + ForksCount int `json:"forks_count"` + MirrorURL interface{} `json:"mirror_url"` + Archived bool `json:"archived"` + Disabled bool `json:"disabled"` + OpenIssuesCount int `json:"open_issues_count"` + License License `json:"license"` + AllowForking bool `json:"allow_forking"` + IsTemplate bool `json:"is_template"` + Topics []interface{} `json:"topics"` + Visibility string `json:"visibility"` + Forks int `json:"forks"` + OpenIssues int `json:"open_issues"` + Watchers int `json:"watchers"` + DefaultBranch string `json:"default_branch"` + Stargazers int `json:"stargazers"` + MasterBranch string `json:"master_branch"` +} +type Pusher struct { + Name string `json:"name"` + Email string `json:"email"` +} +type Sender struct { + Login string `json:"login"` + ID int `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` +} +type Author struct { + Name string `json:"name"` + Email string `json:"email"` + Username string `json:"username"` +} +type Committer struct { + Name string `json:"name"` + Email string `json:"email"` + Username string `json:"username"` +} +type Commits struct { + ID string `json:"id"` + TreeID string `json:"tree_id"` + Distinct bool `json:"distinct"` + Message string `json:"message"` + Timestamp time.Time `json:"timestamp"` + URL string `json:"url"` + Author Author `json:"author"` + Committer Committer `json:"committer"` + Added []interface{} `json:"added"` + Removed []interface{} `json:"removed"` + Modified []string `json:"modified"` +} +type HeadCommit struct { + ID string `json:"id"` + TreeID string `json:"tree_id"` + Distinct bool `json:"distinct"` + Message string `json:"message"` + Timestamp time.Time `json:"timestamp"` + URL string `json:"url"` + Author Author `json:"author"` + Committer Committer `json:"committer"` + Added []interface{} `json:"added"` + Removed []interface{} `json:"removed"` + Modified []string `json:"modified"` +} From fc1b25c8ecdb99a4654dcc56ed82446e1ef8ddc8 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 20:51:17 +0530 Subject: [PATCH 07/47] define config --- structs/Config.go | 6 ++++++ structs/Listener.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 structs/Config.go create mode 100644 structs/Listener.go diff --git a/structs/Config.go b/structs/Config.go new file mode 100644 index 0000000..b30b495 --- /dev/null +++ b/structs/Config.go @@ -0,0 +1,6 @@ +package structs + +type Config struct { + Port int16 `json:"port"` + Listeners []Listener `json:"listeners"` +} diff --git a/structs/Listener.go b/structs/Listener.go new file mode 100644 index 0000000..334875b --- /dev/null +++ b/structs/Listener.go @@ -0,0 +1,15 @@ +package structs + +type Listener struct { + Name string `json:"name"` + Endpoint string `json:"endpoint"` + Directory string `json:"directory"` + Command string `json:"command"` + NotifyDiscord bool `json:"notifyDiscord"` + + Discord struct { + Token string `json:"token"` + ChannelId string `json:"channelId"` + NotifyBeforeRun bool `json:"notifyBeforeRun"` + } +} From ee82b5135b0baee0a8ac46aeb2fe08af454c89f3 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 20:58:48 +0530 Subject: [PATCH 08/47] add fatal param --- logger/logger.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/logger/logger.go b/logger/logger.go index edcf53b..3bdaa28 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -34,11 +34,15 @@ func Log(message string) { } } -func Error(error error) { - log.Println(error) - +func Error(error error, fatal bool) { err := writeToFile("error.log", error.Error()) if err != nil { log.Fatalf("Error while writing log to error.log: %s", err) } + + if fatal { + log.Fatalln(error) + } else { + log.Println(error) + } } From 02e4f145eeba6202a830191499df43558f32ba93 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 30 Jan 2022 21:00:58 +0530 Subject: [PATCH 09/47] rename Index to WebhookHandler --- handlers/{index.go => webhookHandler.go} | 4 ++-- main.go | 4 +--- router/main.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) rename handlers/{index.go => webhookHandler.go} (81%) diff --git a/handlers/index.go b/handlers/webhookHandler.go similarity index 81% rename from handlers/index.go rename to handlers/webhookHandler.go index 8bd2bbb..38b6eed 100644 --- a/handlers/index.go +++ b/handlers/webhookHandler.go @@ -8,12 +8,12 @@ import ( "net/http" ) -func Index(w http.ResponseWriter, r *http.Request) { +func WebhookHandler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var data structs.GithubWebhook if err := decoder.Decode(&data); err != nil { - logger.Error(err) + logger.Error(err, false) } fmt.Println(data) diff --git a/main.go b/main.go index df209e4..c80afdf 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,6 @@ func main() { err := http.ListenAndServe(":4567", r) if err != nil { - logger.Error(err) + logger.Error(err, true) } - - logger.Log("Listening...") } diff --git a/router/main.go b/router/main.go index c2e814c..e02bd54 100644 --- a/router/main.go +++ b/router/main.go @@ -12,7 +12,7 @@ func CreateRouter() *chi.Mux { router.Use(middleware.Recoverer) router.Use(middleware.Logger) - router.Post("/", handlers.Index) + router.Post("/", handlers.WebhookHandler) return router } From 81db3cc46ee93902abf27a6931efbbab0581793d Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 11:08:31 +0530 Subject: [PATCH 10/47] fix various problems with logger --- logger/logger.go | 13 +++++++------ logger/logger_test.go | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/logger/logger.go b/logger/logger.go index 3bdaa28..87dd593 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -15,12 +15,13 @@ func writeToFile(file, text string) error { defer func(f *os.File) { err := f.Close() if err != nil { - log.Fatalf("Error while closing file %s: %s\n", file, err) + log.Printf("Error while closing file %s: %s\n", file, err) } }(f) log.SetOutput(f) log.Println(text) + log.SetOutput(os.Stderr) return nil } @@ -34,15 +35,15 @@ func Log(message string) { } } -func Error(error error, fatal bool) { - err := writeToFile("error.log", error.Error()) +func Error(errorMsg error, fatal bool) { + err := writeToFile("error.log", errorMsg.Error()) if err != nil { - log.Fatalf("Error while writing log to error.log: %s", err) + log.Fatalf("Error while writing log to errorMsg.log: %s", err) } if fatal { - log.Fatalln(error) + log.Fatalln(errorMsg) } else { - log.Println(error) + log.Println(errorMsg) } } diff --git a/logger/logger_test.go b/logger/logger_test.go index 0ab07b3..7ee87ad 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -1,6 +1,7 @@ package logger import ( + "errors" "fmt" "io/ioutil" "strings" @@ -17,7 +18,7 @@ func runLogTest(t *testing.T, message, filename string) { if filename == "all.log" { Log(message) } else { - Error(message) + Error(errors.New(message), false) } // read file to make sure it has the log at the end From d9abc180438bdff39f18472126688c0f0d8f79d4 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 11:09:49 +0530 Subject: [PATCH 11/47] merge Config and Listener --- structs/Config.go | 15 ++++++++++++++- structs/Listener.go | 15 --------------- 2 files changed, 14 insertions(+), 16 deletions(-) delete mode 100644 structs/Listener.go diff --git a/structs/Config.go b/structs/Config.go index b30b495..3f66c52 100644 --- a/structs/Config.go +++ b/structs/Config.go @@ -1,6 +1,19 @@ package structs type Config struct { - Port int16 `json:"port"` + Port string `json:"port"` // example: ":8080" Listeners []Listener `json:"listeners"` } + +type Listener struct { + Name string `json:"name"` + Endpoint string `json:"endpoint"` + Directory string `json:"directory"` + Command string `json:"command"` + NotifyDiscord bool `json:"notifyDiscord"` + + Discord struct { + Webhook string `json:"webhook"` + NotifyBeforeRun bool `json:"notifyBeforeRun"` + } +} diff --git a/structs/Listener.go b/structs/Listener.go deleted file mode 100644 index 334875b..0000000 --- a/structs/Listener.go +++ /dev/null @@ -1,15 +0,0 @@ -package structs - -type Listener struct { - Name string `json:"name"` - Endpoint string `json:"endpoint"` - Directory string `json:"directory"` - Command string `json:"command"` - NotifyDiscord bool `json:"notifyDiscord"` - - Discord struct { - Token string `json:"token"` - ChannelId string `json:"channelId"` - NotifyBeforeRun bool `json:"notifyBeforeRun"` - } -} From 654fa12d1b7f4272990b855ebe60aadf83335f5e Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 11:10:28 +0530 Subject: [PATCH 12/47] add config methods --- config/executeConfig.go | 13 +++++++++ config/readConfig.go | 27 ++++++++++++++++++ config/validateConfig.go | 59 ++++++++++++++++++++++++++++++++++++++++ main.go | 7 ++--- 4 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 config/executeConfig.go create mode 100644 config/readConfig.go create mode 100644 config/validateConfig.go diff --git a/config/executeConfig.go b/config/executeConfig.go new file mode 100644 index 0000000..1faa78b --- /dev/null +++ b/config/executeConfig.go @@ -0,0 +1,13 @@ +package config + +import "fmt" + +func ExecuteConfig() error { + config, err := ReadConfig() + if err != nil { + return err + } + + fmt.Printf("%+v\n", config) + return nil +} diff --git a/config/readConfig.go b/config/readConfig.go new file mode 100644 index 0000000..705746d --- /dev/null +++ b/config/readConfig.go @@ -0,0 +1,27 @@ +package config + +import ( + "encoding/json" + "github.com/DeathVenom54/github-deploy-inator/structs" + "io/ioutil" +) + +func ReadConfig() (structs.Config, error) { + configText, err := ioutil.ReadFile("config.json") + if err != nil { + return structs.Config{}, err + } + + var config structs.Config + err = json.Unmarshal(configText, &config) + if err != nil { + return structs.Config{}, err + } + + err = ValidateConfig(&config) + if err != nil { + return structs.Config{}, err + } + + return config, nil +} diff --git a/config/validateConfig.go b/config/validateConfig.go new file mode 100644 index 0000000..3bb70e5 --- /dev/null +++ b/config/validateConfig.go @@ -0,0 +1,59 @@ +package config + +import ( + "errors" + "fmt" + "github.com/DeathVenom54/github-deploy-inator/structs" + "regexp" +) + +func ValidateConfig(config *structs.Config) error { + // validate port + portRegex := `^:\d+$` + match, err := regexp.MatchString(portRegex, config.Port) + if err != nil { + return err + } + if !match { + return errors.New("invalid port in config.json, should be in format \":0000\"") + } + + // validate listeners + if len(config.Listeners) == 0 { + return errors.New("no listeners assigned, please add at least one") + } + + for i, listener := range config.Listeners { + // required fields + err := shouldExist("name", listener.Name, i) + if err != nil { + return err + } + err = shouldExist("endpoint", listener.Endpoint, i) + if err != nil { + return err + } + err = shouldExist("directory", listener.Directory, i) + if err != nil { + return err + } + err = shouldExist("command", listener.Command, i) + if err != nil { + return err + } + + // discord + if listener.NotifyDiscord && listener.Discord.Webhook == "" { + return errors.New(fmt.Sprintf("Discord.Webhook for listeners[%d] must be provided when NotifyDiscord is true\n", i)) + } + } + return nil +} + +func shouldExist(paramName string, paramValue string, listenerIndex int) error { + if paramValue == "" { + return errors.New(fmt.Sprintf("Invalid %s for listeners[%d]: \"%s\"", paramName, listenerIndex, paramValue)) + } + + return nil +} diff --git a/main.go b/main.go index c80afdf..7183532 100644 --- a/main.go +++ b/main.go @@ -1,15 +1,12 @@ package main import ( + "github.com/DeathVenom54/github-deploy-inator/config" "github.com/DeathVenom54/github-deploy-inator/logger" - "github.com/DeathVenom54/github-deploy-inator/router" - "net/http" ) func main() { - r := router.CreateRouter() - - err := http.ListenAndServe(":4567", r) + err := config.ExecuteConfig() if err != nil { logger.Error(err, true) } From 15dd645dc4ddb22ac02aaf270ff1fc9484e4199f Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 13:07:53 +0530 Subject: [PATCH 13/47] add example config --- config.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config.json diff --git a/config.json b/config.json new file mode 100644 index 0000000..7d09506 --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "port": ":4567", + "listeners": [ + { + "name": "listener-name", + "endpoint": "/github/webhook", + "directory": "/home/dv/projects/nodejs-bot", + "command": "yarn deploy", + "notifyDiscord": true, + "discord": { + "webhook": "webhook-goes-here", + "notifyBeforeRun": "" + } + } + ] +} \ No newline at end of file From bb00f5ac2af9ba5a455c0838ddcb4d2b399c47f5 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 13:19:52 +0530 Subject: [PATCH 14/47] start webserver according to config --- config/executeConfig.go | 17 +++++++++++++++-- handlers/webhookHandler.go | 22 ++++++++++++---------- router/main.go | 7 +++++-- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/config/executeConfig.go b/config/executeConfig.go index 1faa78b..8c73faf 100644 --- a/config/executeConfig.go +++ b/config/executeConfig.go @@ -1,6 +1,11 @@ package config -import "fmt" +import ( + "fmt" + "github.com/DeathVenom54/github-deploy-inator/logger" + "github.com/DeathVenom54/github-deploy-inator/router" + "net/http" +) func ExecuteConfig() error { config, err := ReadConfig() @@ -8,6 +13,14 @@ func ExecuteConfig() error { return err } - fmt.Printf("%+v\n", config) + // start server + r := router.CreateRouter(config.Listeners) + + logger.Log(fmt.Sprintf("Listening for Github webhooks at %s", config.Port)) + err = http.ListenAndServe(config.Port, r) + if err != nil { + return err + } + return nil } diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 38b6eed..97fe66d 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -8,17 +8,19 @@ import ( "net/http" ) -func WebhookHandler(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) +func CreateWebhookHandler(directory string, command string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) - var data structs.GithubWebhook - if err := decoder.Decode(&data); err != nil { - logger.Error(err, false) - } - fmt.Println(data) + var data structs.GithubWebhook + if err := decoder.Decode(&data); err != nil { + logger.Error(err, false) + } + fmt.Println(data) - _, err := w.Write([]byte("Hello")) - if err != nil { - return + _, err := w.Write([]byte("Hello")) + if err != nil { + return + } } } diff --git a/router/main.go b/router/main.go index e02bd54..d3708b2 100644 --- a/router/main.go +++ b/router/main.go @@ -2,17 +2,20 @@ package router import ( "github.com/DeathVenom54/github-deploy-inator/handlers" + "github.com/DeathVenom54/github-deploy-inator/structs" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) -func CreateRouter() *chi.Mux { +func CreateRouter(listeners []structs.Listener) *chi.Mux { router := chi.NewRouter() router.Use(middleware.Recoverer) router.Use(middleware.Logger) - router.Post("/", handlers.WebhookHandler) + for _, listener := range listeners { + router.Post(listener.Endpoint, handlers.CreateWebhookHandler(listener.Directory, listener.Command)) + } return router } From 28a1261f041d7c0253ac4d56d2d723b2d2effc12 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 13:26:09 +0530 Subject: [PATCH 15/47] edit config.json --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 7d09506..83e9e88 100644 --- a/config.json +++ b/config.json @@ -9,7 +9,7 @@ "notifyDiscord": true, "discord": { "webhook": "webhook-goes-here", - "notifyBeforeRun": "" + "notifyBeforeRun": true } } ] From fb69f831f0e2d9be31802610e7ed806cadfc2dbb Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 13:35:25 +0530 Subject: [PATCH 16/47] github hook test --- config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index 83e9e88..629184a 100644 --- a/config.json +++ b/config.json @@ -4,8 +4,8 @@ { "name": "listener-name", "endpoint": "/github/webhook", - "directory": "/home/dv/projects/nodejs-bot", - "command": "yarn deploy", + "directory": "/home/dv/projects", + "command": "ls", "notifyDiscord": true, "discord": { "webhook": "webhook-goes-here", From bb84de36a2701089ef5c532acbae1ff5f723befe Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 13:36:17 +0530 Subject: [PATCH 17/47] github hook test 2 --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 629184a..7d7a9b0 100644 --- a/config.json +++ b/config.json @@ -6,7 +6,7 @@ "endpoint": "/github/webhook", "directory": "/home/dv/projects", "command": "ls", - "notifyDiscord": true, + "notifyDiscord": false, "discord": { "webhook": "webhook-goes-here", "notifyBeforeRun": true From 71496681d9f5bc2a98b6d6dabcd1ec1b14df3832 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 14:25:14 +0530 Subject: [PATCH 18/47] github hook test 3 --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 7d7a9b0..629184a 100644 --- a/config.json +++ b/config.json @@ -6,7 +6,7 @@ "endpoint": "/github/webhook", "directory": "/home/dv/projects", "command": "ls", - "notifyDiscord": false, + "notifyDiscord": true, "discord": { "webhook": "webhook-goes-here", "notifyBeforeRun": true From 53c75104b893abd0e4aa61f3774389b0237471cd Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 14:39:36 +0530 Subject: [PATCH 19/47] add filters --- structs/Config.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/structs/Config.go b/structs/Config.go index 3f66c52..7e502e1 100644 --- a/structs/Config.go +++ b/structs/Config.go @@ -6,14 +6,22 @@ type Config struct { } type Listener struct { - Name string `json:"name"` - Endpoint string `json:"endpoint"` - Directory string `json:"directory"` - Command string `json:"command"` - NotifyDiscord bool `json:"notifyDiscord"` + // required properties + Endpoint string `json:"endpoint"` // endpoint where http server will listen for hooks + Directory string `json:"directory"` // the directory in which the command will be run + Command string `json:"command"` // the command to run + // for your deployment, it is suggested to put the various commands + // in your scripts like in node.js or a .sh file, and execute it. + + //additional filters + Branch string `json:"branch"` // execute only if push is on this branch + Authors []string `json:"authors"` // execute only if head commit author is one of these + + NotifyDiscord bool `json:"notifyDiscord"` Discord struct { Webhook string `json:"webhook"` NotifyBeforeRun bool `json:"notifyBeforeRun"` + SendOutput bool `json:"sendOutput"` } } From 9743cc8c376a7a83316f2f54bd2f0649493841da Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 15:18:29 +0530 Subject: [PATCH 20/47] implement command execution and filters --- handlers/webhookHandler.go | 41 +++++++++++++++++++++++++++++++++----- structs/Config.go | 5 +++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 97fe66d..5d6dc34 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -6,19 +6,50 @@ import ( "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" "net/http" + "os/exec" + "strings" ) -func CreateWebhookHandler(directory string, command string) func(w http.ResponseWriter, r *http.Request) { +func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) - var data structs.GithubWebhook - if err := decoder.Decode(&data); err != nil { + var webhook structs.GithubWebhook + if err := decoder.Decode(&webhook); err != nil { logger.Error(err, false) } - fmt.Println(data) - _, err := w.Write([]byte("Hello")) + // run filters + if listener.Branch != "" { + branch := webhook.Ref[11:] + if listener.Branch != branch { + return + } + } + if len(listener.AllowedPushers) > 0 { + pusherIsAllowed := false + for _, pusher := range listener.AllowedPushers { + if webhook.Pusher.Name == pusher { + pusherIsAllowed = true + break + } + } + if !pusherIsAllowed { + return + } + } + + args := strings.Split(listener.Command, " ") + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = listener.Directory + + out, err := cmd.Output() + if err != nil { + fmt.Println(err) + } + fmt.Println(string(out)) + + _, err = w.Write([]byte("Hello")) if err != nil { return } diff --git a/structs/Config.go b/structs/Config.go index 7e502e1..1a3a864 100644 --- a/structs/Config.go +++ b/structs/Config.go @@ -7,6 +7,7 @@ type Config struct { type Listener struct { // required properties + Name string `json:"name"` // a unique name for the webhook Endpoint string `json:"endpoint"` // endpoint where http server will listen for hooks Directory string `json:"directory"` // the directory in which the command will be run Command string `json:"command"` // the command to run @@ -14,8 +15,8 @@ type Listener struct { // in your scripts like in node.js or a .sh file, and execute it. //additional filters - Branch string `json:"branch"` // execute only if push is on this branch - Authors []string `json:"authors"` // execute only if head commit author is one of these + Branch string `json:"branch"` // execute only if push is on this branch + AllowedPushers []string `json:"allowedPushers"` // execute only if pusher is one of these (username) NotifyDiscord bool `json:"notifyDiscord"` From 07aece1451442506c86a213f5f4b8f750c4064df Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Mon, 31 Jan 2022 15:18:29 +0530 Subject: [PATCH 21/47] implement command execution and filters --- handlers/webhookHandler.go | 41 +++++++++++++++++++++++++++++++++----- router/main.go | 2 +- structs/Config.go | 5 +++-- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 97fe66d..5d6dc34 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -6,19 +6,50 @@ import ( "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" "net/http" + "os/exec" + "strings" ) -func CreateWebhookHandler(directory string, command string) func(w http.ResponseWriter, r *http.Request) { +func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) - var data structs.GithubWebhook - if err := decoder.Decode(&data); err != nil { + var webhook structs.GithubWebhook + if err := decoder.Decode(&webhook); err != nil { logger.Error(err, false) } - fmt.Println(data) - _, err := w.Write([]byte("Hello")) + // run filters + if listener.Branch != "" { + branch := webhook.Ref[11:] + if listener.Branch != branch { + return + } + } + if len(listener.AllowedPushers) > 0 { + pusherIsAllowed := false + for _, pusher := range listener.AllowedPushers { + if webhook.Pusher.Name == pusher { + pusherIsAllowed = true + break + } + } + if !pusherIsAllowed { + return + } + } + + args := strings.Split(listener.Command, " ") + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = listener.Directory + + out, err := cmd.Output() + if err != nil { + fmt.Println(err) + } + fmt.Println(string(out)) + + _, err = w.Write([]byte("Hello")) if err != nil { return } diff --git a/router/main.go b/router/main.go index d3708b2..81b6dee 100644 --- a/router/main.go +++ b/router/main.go @@ -14,7 +14,7 @@ func CreateRouter(listeners []structs.Listener) *chi.Mux { router.Use(middleware.Logger) for _, listener := range listeners { - router.Post(listener.Endpoint, handlers.CreateWebhookHandler(listener.Directory, listener.Command)) + router.Post(listener.Endpoint, handlers.CreateWebhookHandler(listener)) } return router diff --git a/structs/Config.go b/structs/Config.go index 7e502e1..1a3a864 100644 --- a/structs/Config.go +++ b/structs/Config.go @@ -7,6 +7,7 @@ type Config struct { type Listener struct { // required properties + Name string `json:"name"` // a unique name for the webhook Endpoint string `json:"endpoint"` // endpoint where http server will listen for hooks Directory string `json:"directory"` // the directory in which the command will be run Command string `json:"command"` // the command to run @@ -14,8 +15,8 @@ type Listener struct { // in your scripts like in node.js or a .sh file, and execute it. //additional filters - Branch string `json:"branch"` // execute only if push is on this branch - Authors []string `json:"authors"` // execute only if head commit author is one of these + Branch string `json:"branch"` // execute only if push is on this branch + AllowedPushers []string `json:"allowedPushers"` // execute only if pusher is one of these (username) NotifyDiscord bool `json:"notifyDiscord"` From 78fd10aad3a554b4a9ed8a32798bd7fa6a5a470e Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 20:10:03 +0530 Subject: [PATCH 22/47] basic working!!!! --- config/validateConfig.go | 13 ++- go.mod | 9 +- handlers/webhookHandler.go | 47 ++++++++-- structs/DiscordNotificationManager.go | 128 ++++++++++++++++++++++++++ structs/colors.go | 5 + 5 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 structs/DiscordNotificationManager.go create mode 100644 structs/colors.go diff --git a/config/validateConfig.go b/config/validateConfig.go index 3bb70e5..0061f24 100644 --- a/config/validateConfig.go +++ b/config/validateConfig.go @@ -43,8 +43,17 @@ func ValidateConfig(config *structs.Config) error { } // discord - if listener.NotifyDiscord && listener.Discord.Webhook == "" { - return errors.New(fmt.Sprintf("Discord.Webhook for listeners[%d] must be provided when NotifyDiscord is true\n", i)) + if listener.NotifyDiscord { + if listener.Discord.Webhook == "" { + match, err := regexp.MatchString(structs.DiscordWebhookRegex, listener.Discord.Webhook) + if err != nil { + return err + } + if !match { + return errors.New("please provide a valid Discord webhook url") + } + return errors.New(fmt.Sprintf("Discord.Webhook for listeners[%d] must be provided when NotifyDiscord is true\n", i)) + } } } return nil diff --git a/go.mod b/go.mod index c6400f5..7ce60b9 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,11 @@ module github.com/DeathVenom54/github-deploy-inator go 1.17 -require github.com/go-chi/chi/v5 v5.0.7 // indirect +require ( + github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820 // indirect + github.com/clinet/discordgo-embed v0.0.0-20220113222025-bafe0c917646 // indirect + github.com/go-chi/chi/v5 v5.0.7 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect + golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect +) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 5d6dc34..fc4df61 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -2,7 +2,6 @@ package handlers import ( "encoding/json" - "fmt" "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" "net/http" @@ -23,6 +22,7 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, if listener.Branch != "" { branch := webhook.Ref[11:] if listener.Branch != branch { + reply(w) return } } @@ -35,23 +35,56 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, } } if !pusherIsAllowed { + reply(w) return } } + m := structs.DiscordNotificationManager{ + Webhook: structs.DiscordWebhookData{ + Url: listener.Discord.Webhook, + }, + } + if listener.NotifyDiscord { + err := m.Setup() + handleErr(err) + + if listener.Discord.NotifyBeforeRun { + err := m.SendPreRunNotification(&listener, &webhook) + handleErr(err) + } + } + + // run command args := strings.Split(listener.Command, " ") cmd := exec.Command(args[0], args[1:]...) cmd.Dir = listener.Directory out, err := cmd.Output() if err != nil { - fmt.Println(err) + if listener.NotifyDiscord { + err := m.SendErrorMessage(&listener, &err, &webhook) + handleErr(err) + } + handleErr(err) + } else if listener.NotifyDiscord { + // send notification + output := string(out) + err := m.SendSuccessMessage(&listener, &output, &webhook) + handleErr(err) } - fmt.Println(string(out)) - _, err = w.Write([]byte("Hello")) - if err != nil { - return - } + reply(w) } } + +func reply(w http.ResponseWriter) { + w.WriteHeader(200) +} + +func handleErr(err error) { + if err != nil { + logger.Error(err, false) + } + +} diff --git a/structs/DiscordNotificationManager.go b/structs/DiscordNotificationManager.go new file mode 100644 index 0000000..b3cefe1 --- /dev/null +++ b/structs/DiscordNotificationManager.go @@ -0,0 +1,128 @@ +package structs + +import ( + "errors" + "fmt" + "github.com/bwmarrin/discordgo" + embed "github.com/clinet/discordgo-embed" + "regexp" + "time" +) + +const DiscordWebhookRegex = `^https:\/\/discord.com\/api\/webhooks\/(?P\d+)\/(?P\w+)$` + +type DiscordNotificationManager struct { + Session *discordgo.Session + Webhook DiscordWebhookData + NotifyBeforeRun bool + SendOutput bool +} + +type DiscordWebhookData struct { + Url string + Id string + Token string +} + +func (m *DiscordNotificationManager) Setup() error { + // parse ID and Token + if m.Webhook.Url == "" { + return errors.New("no webhook.url found") + } + r, err := regexp.Compile(DiscordWebhookRegex) + if err != nil { + return err + } + + matches := r.FindStringSubmatch(m.Webhook.Url) + m.Webhook.Id = matches[1] + m.Webhook.Token = matches[2] + + // create session + m.Session, err = discordgo.New() + if err != nil { + return err + } + + return nil +} + +func (m *DiscordNotificationManager) SendPreRunNotification(listener *Listener, ghWebhook *GithubWebhook) error { + t := time.Now() + formattedTime := fmt.Sprintf("%02d/%02d/%02d at %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) + preRunEmbed := embed.NewEmbed(). + SetColor(blurple). + SetTitle(fmt.Sprintf("Deploying %s...", listener.Name)). + AddField("Repository", ghWebhook.Repository.FullName). + AddField("Pusher", ghWebhook.Pusher.Name). + AddField("Branch", ghWebhook.Ref[11:]). + AddField("Command", listener.Command). + SetFooter(formattedTime). + MessageEmbed + + webhookParams := discordgo.WebhookParams{Embeds: []*discordgo.MessageEmbed{preRunEmbed}} + + _, err := m.Session.WebhookExecute(m.Webhook.Id, m.Webhook.Token, false, &webhookParams) + if err != nil { + return err + } + return nil +} + +func (m *DiscordNotificationManager) SendSuccessMessage(listener *Listener, output *string, ghWebhook *GithubWebhook) error { + t := time.Now() + formattedTime := fmt.Sprintf("%02d/%02d/%02d at %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) + + successEmbed := embed.NewEmbed(). + SetTitle(fmt.Sprintf("Succesfully deployed %s", listener.Name)). + SetColor(green). + SetFooter(formattedTime) + + if !listener.Discord.NotifyBeforeRun { + successEmbed = successEmbed. + AddField("Repository", ghWebhook.Repository.FullName). + AddField("Pusher", ghWebhook.Pusher.Name). + AddField("Branch", ghWebhook.Ref[11:]). + AddField("Command", listener.Command) + } + + if listener.Discord.SendOutput { + successEmbed = successEmbed. + AddField("Output", *output) + } + + webhookParams := discordgo.WebhookParams{Embeds: []*discordgo.MessageEmbed{successEmbed.MessageEmbed}} + _, err := m.Session.WebhookExecute(m.Webhook.Id, m.Webhook.Token, false, &webhookParams) + if err != nil { + return err + } + + return nil +} + +func (m *DiscordNotificationManager) SendErrorMessage(listener *Listener, error *error, ghWebhook *GithubWebhook) error { + t := time.Now() + formattedTime := fmt.Sprintf("%02d/%02d/%02d at %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) + + errorEmbed := embed.NewEmbed(). + SetTitle(fmt.Sprintf("There was an error while deploying %s", listener.Name)). + SetColor(red). + AddField("Error", (*error).Error()). + SetFooter(formattedTime) + + if !listener.Discord.NotifyBeforeRun { + errorEmbed = errorEmbed. + AddField("Repository", ghWebhook.Repository.FullName). + AddField("Pusher", ghWebhook.Pusher.Name). + AddField("Branch", ghWebhook.Ref[11:]). + AddField("Command", listener.Command) + } + + webhookParams := discordgo.WebhookParams{Embeds: []*discordgo.MessageEmbed{errorEmbed.MessageEmbed}} + _, err := m.Session.WebhookExecute(m.Webhook.Id, m.Webhook.Token, false, &webhookParams) + if err != nil { + return err + } + + return nil +} diff --git a/structs/colors.go b/structs/colors.go new file mode 100644 index 0000000..ae33354 --- /dev/null +++ b/structs/colors.go @@ -0,0 +1,5 @@ +package structs + +const blurple = 0x5865F2 +const green = 0x57F287 +const red = 0xED4245 From 2393151fb4855e97f6a7f986195a4e5110674454 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 21:09:40 +0530 Subject: [PATCH 23/47] change config structure to differentiate on repo name not endpoint --- config/validateConfig.go | 23 +++++++++++++++++------ structs/Config.go | 17 +++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/config/validateConfig.go b/config/validateConfig.go index 0061f24..a7c156d 100644 --- a/config/validateConfig.go +++ b/config/validateConfig.go @@ -8,14 +8,14 @@ import ( ) func ValidateConfig(config *structs.Config) error { - // validate port - portRegex := `^:\d+$` - match, err := regexp.MatchString(portRegex, config.Port) + // validate port and endpoint + err := shouldMatchRegex("port", config.Port, `^:\d+$`) if err != nil { return err } - if !match { - return errors.New("invalid port in config.json, should be in format \":0000\"") + err = shouldMatchRegex("endpoint", config.Endpoint, `^\/[\w-\/]*$`) + if err != nil { + return err } // validate listeners @@ -29,7 +29,7 @@ func ValidateConfig(config *structs.Config) error { if err != nil { return err } - err = shouldExist("endpoint", listener.Endpoint, i) + err = shouldMatchRegex(fmt.Sprintf("listeners[%d].repository", i), listener.Repository, `[\w-]+\/[\w-]+`) if err != nil { return err } @@ -59,6 +59,17 @@ func ValidateConfig(config *structs.Config) error { return nil } +func shouldMatchRegex(field, value, regex string) error { + match, err := regexp.MatchString(regex, value) + if err != nil { + return err + } + if !match { + return errors.New(fmt.Sprintf("invalid %s in config.json, should be in format %s", field, regex)) + } + return nil +} + func shouldExist(paramName string, paramValue string, listenerIndex int) error { if paramValue == "" { return errors.New(fmt.Sprintf("Invalid %s for listeners[%d]: \"%s\"", paramName, listenerIndex, paramValue)) diff --git a/structs/Config.go b/structs/Config.go index 1a3a864..a748386 100644 --- a/structs/Config.go +++ b/structs/Config.go @@ -1,26 +1,27 @@ package structs type Config struct { - Port string `json:"port"` // example: ":8080" + Port string `json:"port"` // example: ":8080" + Endpoint string `json:"endpoint"` // endpoint where http server will listen for hooks Listeners []Listener `json:"listeners"` } type Listener struct { // required properties - Name string `json:"name"` // a unique name for the webhook - Endpoint string `json:"endpoint"` // endpoint where http server will listen for hooks - Directory string `json:"directory"` // the directory in which the command will be run - Command string `json:"command"` // the command to run + Name string `json:"name"` // a unique name for the webhook + Repository string `json:"repository"` // the repository name in the format "username/repo-name" + Directory string `json:"directory"` // the directory in which the command will be run + Command string `json:"command"` // the command to run // for your deployment, it is suggested to put the various commands // in your scripts like in node.js or a .sh file, and execute it. - //additional filters + // additional filters Branch string `json:"branch"` // execute only if push is on this branch AllowedPushers []string `json:"allowedPushers"` // execute only if pusher is one of these (username) + // notification options NotifyDiscord bool `json:"notifyDiscord"` - - Discord struct { + Discord struct { Webhook string `json:"webhook"` NotifyBeforeRun bool `json:"notifyBeforeRun"` SendOutput bool `json:"sendOutput"` From 9155d13cab1e2efb12ccd36d77c66eb9382ecb50 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 21:17:45 +0530 Subject: [PATCH 24/47] better error handling --- handlers/webhookHandler.go | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index fc4df61..85e30f4 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -19,10 +19,12 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, } // run filters + if strings.ToLower(listener.Repository) != strings.ToLower(webhook.Repository.FullName) { + return + } if listener.Branch != "" { branch := webhook.Ref[11:] if listener.Branch != branch { - reply(w) return } } @@ -35,7 +37,6 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, } } if !pusherIsAllowed { - reply(w) return } } @@ -47,11 +48,15 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, } if listener.NotifyDiscord { err := m.Setup() - handleErr(err) + if err != nil { + logger.Error(err, false) + } if listener.Discord.NotifyBeforeRun { err := m.SendPreRunNotification(&listener, &webhook) - handleErr(err) + if err != nil { + logger.Error(err, false) + } } } @@ -64,27 +69,23 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, if err != nil { if listener.NotifyDiscord { err := m.SendErrorMessage(&listener, &err, &webhook) - handleErr(err) + if err != nil { + logger.Error(err, false) + } + } + if err != nil { + logger.Error(err, false) + return } - handleErr(err) } else if listener.NotifyDiscord { // send notification output := string(out) err := m.SendSuccessMessage(&listener, &output, &webhook) - handleErr(err) + if err != nil { + logger.Error(err, false) + } } - reply(w) + w.WriteHeader(200) } } - -func reply(w http.ResponseWriter) { - w.WriteHeader(200) -} - -func handleErr(err error) { - if err != nil { - logger.Error(err, false) - } - -} From d5177c1d122027416253265bde02862854f0b3cf Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 21:18:40 +0530 Subject: [PATCH 25/47] add go.sum --- go.sum | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 go.sum diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8e1b04a --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820 h1:MIW5DnBVJAgAy4LYBqWwIMBB0ezklvh8b7DsYvHZHb0= +github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/clinet/discordgo-embed v0.0.0-20220113222025-bafe0c917646 h1:WOA+0wBHL/ZkiIQ8ctBAO9d5nnf5I7cgE531zhxGTOY= +github.com/clinet/discordgo-embed v0.0.0-20220113222025-bafe0c917646/go.mod h1:p2/vBoWL0mBfu/3eXnLHKRD5HHlaqGBJqe+et80Z0cQ= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 4863cf2c5d4b4bc088eb74500589e83630361008 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 21:20:50 +0530 Subject: [PATCH 26/47] compare lowercase pushers --- handlers/webhookHandler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 85e30f4..0f74b02 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -31,7 +31,7 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, if len(listener.AllowedPushers) > 0 { pusherIsAllowed := false for _, pusher := range listener.AllowedPushers { - if webhook.Pusher.Name == pusher { + if strings.ToLower(webhook.Pusher.Name) == strings.ToLower(pusher) { pusherIsAllowed = true break } From c5c13c429df004d95daf8950a3ec9003dc880d6a Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 21:25:18 +0530 Subject: [PATCH 27/47] update config on new format --- config.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config.json b/config.json index 629184a..3104f1d 100644 --- a/config.json +++ b/config.json @@ -1,15 +1,17 @@ { "port": ":4567", + "endpoint": "/github/webhook", "listeners": [ { - "name": "listener-name", - "endpoint": "/github/webhook", + "name": "example-listener", + "repository": "DeathVenom54/github-deploy-inator", "directory": "/home/dv/projects", "command": "ls", "notifyDiscord": true, "discord": { "webhook": "webhook-goes-here", - "notifyBeforeRun": true + "notifyBeforeRun": true, + "sendOutput": true } } ] From df074422719c2f555976f8444f3d9628f091862e Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 21:53:12 +0530 Subject: [PATCH 28/47] use new config pattern to run code --- config/executeConfig.go | 2 +- config/readConfig.go | 10 +++++----- handlers/webhookHandler.go | 25 +++++++++++++++++++------ router/main.go | 6 ++---- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/config/executeConfig.go b/config/executeConfig.go index 8c73faf..5ad4517 100644 --- a/config/executeConfig.go +++ b/config/executeConfig.go @@ -14,7 +14,7 @@ func ExecuteConfig() error { } // start server - r := router.CreateRouter(config.Listeners) + r := router.CreateRouter(config) logger.Log(fmt.Sprintf("Listening for Github webhooks at %s", config.Port)) err = http.ListenAndServe(config.Port, r) diff --git a/config/readConfig.go b/config/readConfig.go index 705746d..eb5a394 100644 --- a/config/readConfig.go +++ b/config/readConfig.go @@ -6,22 +6,22 @@ import ( "io/ioutil" ) -func ReadConfig() (structs.Config, error) { +func ReadConfig() (*structs.Config, error) { configText, err := ioutil.ReadFile("config.json") if err != nil { - return structs.Config{}, err + return &structs.Config{}, err } var config structs.Config err = json.Unmarshal(configText, &config) if err != nil { - return structs.Config{}, err + return &structs.Config{}, err } err = ValidateConfig(&config) if err != nil { - return structs.Config{}, err + return &structs.Config{}, err } - return config, nil + return &config, nil } diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 0f74b02..781debd 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -2,6 +2,8 @@ package handlers import ( "encoding/json" + "errors" + "fmt" "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" "net/http" @@ -9,19 +11,30 @@ import ( "strings" ) -func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, r *http.Request) { +func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var webhook structs.GithubWebhook if err := decoder.Decode(&webhook); err != nil { logger.Error(err, false) + return } - // run filters - if strings.ToLower(listener.Repository) != strings.ToLower(webhook.Repository.FullName) { + // get the correct listener + var listener *structs.Listener + for _, l := range config.Listeners { + if strings.ToLower(l.Repository) == strings.ToLower(webhook.Repository.FullName) { + listener = &l + break + } + } + if listener == nil { + logger.Error(errors.New(fmt.Sprintf("No listener found for webhook from %s", webhook.Repository.FullName)), false) return } + + // run filters if listener.Branch != "" { branch := webhook.Ref[11:] if listener.Branch != branch { @@ -53,7 +66,7 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, } if listener.Discord.NotifyBeforeRun { - err := m.SendPreRunNotification(&listener, &webhook) + err := m.SendPreRunNotification(listener, &webhook) if err != nil { logger.Error(err, false) } @@ -68,7 +81,7 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, out, err := cmd.Output() if err != nil { if listener.NotifyDiscord { - err := m.SendErrorMessage(&listener, &err, &webhook) + err := m.SendErrorMessage(listener, &err, &webhook) if err != nil { logger.Error(err, false) } @@ -80,7 +93,7 @@ func CreateWebhookHandler(listener structs.Listener) func(w http.ResponseWriter, } else if listener.NotifyDiscord { // send notification output := string(out) - err := m.SendSuccessMessage(&listener, &output, &webhook) + err := m.SendSuccessMessage(listener, &output, &webhook) if err != nil { logger.Error(err, false) } diff --git a/router/main.go b/router/main.go index 81b6dee..7446bc5 100644 --- a/router/main.go +++ b/router/main.go @@ -7,15 +7,13 @@ import ( "github.com/go-chi/chi/v5/middleware" ) -func CreateRouter(listeners []structs.Listener) *chi.Mux { +func CreateRouter(config *structs.Config) *chi.Mux { router := chi.NewRouter() router.Use(middleware.Recoverer) router.Use(middleware.Logger) - for _, listener := range listeners { - router.Post(listener.Endpoint, handlers.CreateWebhookHandler(listener)) - } + router.Post(config.Endpoint, handlers.CreateWebhookHandler(config)) return router } From 22fd5a97652f2a19082789aedf7862a89a44f824 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 22:49:23 +0530 Subject: [PATCH 29/47] fix validation bug --- config/validateConfig.go | 8 ++------ structs/DiscordNotificationManager.go | 3 +++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/config/validateConfig.go b/config/validateConfig.go index a7c156d..a9b7737 100644 --- a/config/validateConfig.go +++ b/config/validateConfig.go @@ -44,15 +44,11 @@ func ValidateConfig(config *structs.Config) error { // discord if listener.NotifyDiscord { - if listener.Discord.Webhook == "" { - match, err := regexp.MatchString(structs.DiscordWebhookRegex, listener.Discord.Webhook) + if listener.Discord.Webhook != "" { + err := shouldMatchRegex(fmt.Sprintf("listeners[%d].discord.webhook", i), listener.Discord.Webhook, structs.DiscordWebhookRegex) if err != nil { return err } - if !match { - return errors.New("please provide a valid Discord webhook url") - } - return errors.New(fmt.Sprintf("Discord.Webhook for listeners[%d] must be provided when NotifyDiscord is true\n", i)) } } } diff --git a/structs/DiscordNotificationManager.go b/structs/DiscordNotificationManager.go index b3cefe1..9baf0d3 100644 --- a/structs/DiscordNotificationManager.go +++ b/structs/DiscordNotificationManager.go @@ -35,6 +35,9 @@ func (m *DiscordNotificationManager) Setup() error { } matches := r.FindStringSubmatch(m.Webhook.Url) + if len(matches) < 3 { + return errors.New("the provided webhook url is invalid") + } m.Webhook.Id = matches[1] m.Webhook.Token = matches[2] From e4838871a60e4e5460f4a3afeaf8cd20a4c0fb33 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Tue, 1 Feb 2022 23:12:30 +0530 Subject: [PATCH 30/47] remove logger middleware --- router/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/router/main.go b/router/main.go index 7446bc5..d5433f7 100644 --- a/router/main.go +++ b/router/main.go @@ -11,7 +11,6 @@ func CreateRouter(config *structs.Config) *chi.Mux { router := chi.NewRouter() router.Use(middleware.Recoverer) - router.Use(middleware.Logger) router.Post(config.Endpoint, handlers.CreateWebhookHandler(config)) From 94298d2187fcc77a83c5c9e9f4f0d2e214f70c40 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Wed, 2 Feb 2022 17:57:35 +0530 Subject: [PATCH 31/47] use panic instead of logger.Error --- handlers/webhookHandler.go | 37 ++++++++++++++----------------------- router/main.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 781debd..9eae3f6 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -2,9 +2,7 @@ package handlers import ( "encoding/json" - "errors" "fmt" - "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" "net/http" "os/exec" @@ -16,10 +14,8 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r decoder := json.NewDecoder(r.Body) var webhook structs.GithubWebhook - if err := decoder.Decode(&webhook); err != nil { - logger.Error(err, false) - return - } + err := decoder.Decode(&webhook) + handleErr(err) // get the correct listener var listener *structs.Listener @@ -30,7 +26,7 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r } } if listener == nil { - logger.Error(errors.New(fmt.Sprintf("No listener found for webhook from %s", webhook.Repository.FullName)), false) + panic(fmt.Errorf("no listener found for webhook from %s", webhook.Repository.FullName)) return } @@ -61,15 +57,11 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r } if listener.NotifyDiscord { err := m.Setup() - if err != nil { - logger.Error(err, false) - } + handleErr(err) if listener.Discord.NotifyBeforeRun { err := m.SendPreRunNotification(listener, &webhook) - if err != nil { - logger.Error(err, false) - } + handleErr(err) } } @@ -82,23 +74,22 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r if err != nil { if listener.NotifyDiscord { err := m.SendErrorMessage(listener, &err, &webhook) - if err != nil { - logger.Error(err, false) - } - } - if err != nil { - logger.Error(err, false) - return + handleErr(err) } + handleErr(err) } else if listener.NotifyDiscord { // send notification output := string(out) err := m.SendSuccessMessage(listener, &output, &webhook) - if err != nil { - logger.Error(err, false) - } + handleErr(err) } w.WriteHeader(200) } } + +func handleErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/router/main.go b/router/main.go index d5433f7..a23b34c 100644 --- a/router/main.go +++ b/router/main.go @@ -1,18 +1,42 @@ package router import ( + "fmt" "github.com/DeathVenom54/github-deploy-inator/handlers" + "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" + "net/http" ) func CreateRouter(config *structs.Config) *chi.Mux { router := chi.NewRouter() - router.Use(middleware.Recoverer) + router.Use(recovery) router.Post(config.Endpoint, handlers.CreateWebhookHandler(config)) return router } + +func recovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + err := recover() + if err != nil { + logger.Error(fmt.Errorf("error: %v", err), false) + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write([]byte("500 Internal server error")) + if err != nil { + logger.Error(err, false) + } + } + + }() + + next.ServeHTTP(w, r) + + }) +} From 4d8fa8c855306dd0556ae50e407fdc0cf92bf30f Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Wed, 2 Feb 2022 20:31:49 +0530 Subject: [PATCH 32/47] wrap output with ``` --- structs/DiscordNotificationManager.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structs/DiscordNotificationManager.go b/structs/DiscordNotificationManager.go index 9baf0d3..95ae43e 100644 --- a/structs/DiscordNotificationManager.go +++ b/structs/DiscordNotificationManager.go @@ -91,7 +91,7 @@ func (m *DiscordNotificationManager) SendSuccessMessage(listener *Listener, outp if listener.Discord.SendOutput { successEmbed = successEmbed. - AddField("Output", *output) + AddField("Output", fmt.Sprintf("```\n%s\n```", *output)) } webhookParams := discordgo.WebhookParams{Embeds: []*discordgo.MessageEmbed{successEmbed.MessageEmbed}} From 89652c76fc9915040487ce7b75a06eebf9822216 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Wed, 2 Feb 2022 20:43:13 +0530 Subject: [PATCH 33/47] webhook test --- handlers/webhookHandler.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 9eae3f6..479fcc7 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -1,6 +1,8 @@ package handlers import ( + "crypto/sha1" + "encoding/hex" "encoding/json" "fmt" "github.com/DeathVenom54/github-deploy-inator/structs" @@ -30,6 +32,18 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r return } + // verify signature if secret is provided + if listener.Secret != "" { + hash := sha1.New() + hash.Write([]byte(listener.Secret)) + signature := "sha1=" + hex.EncodeToString(hash.Sum(nil)) + fmt.Println(signature) + fmt.Println(r.Header.Get("X-Hub-Signature")) + if signature != r.Header.Get("X-Hub-Signature") { + panic(fmt.Sprintf("received webhook from %s but signature does not match secret", webhook.Repository.FullName)) + } + } + // run filters if listener.Branch != "" { branch := webhook.Ref[11:] From 8dcddd3cd6f6671315b91dbc34c2fe685171ab7e Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Wed, 2 Feb 2022 21:15:53 +0530 Subject: [PATCH 34/47] add signature verification --- handlers/webhookHandler.go | 24 ++++++++++++++++-------- structs/Config.go | 1 + 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index 479fcc7..de0bb0c 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -1,11 +1,12 @@ package handlers import ( - "crypto/sha1" - "encoding/hex" + "crypto/hmac" + "crypto/sha256" "encoding/json" "fmt" "github.com/DeathVenom54/github-deploy-inator/structs" + "io/ioutil" "net/http" "os/exec" "strings" @@ -34,12 +35,11 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r // verify signature if secret is provided if listener.Secret != "" { - hash := sha1.New() - hash.Write([]byte(listener.Secret)) - signature := "sha1=" + hex.EncodeToString(hash.Sum(nil)) - fmt.Println(signature) - fmt.Println(r.Header.Get("X-Hub-Signature")) - if signature != r.Header.Get("X-Hub-Signature") { + body, err := ioutil.ReadAll(r.Body) + handleErr(err) + + providedSignature := r.Header.Get("X-Hub-Signature") + if verifySignature(listener.Secret, string(body), providedSignature) { panic(fmt.Sprintf("received webhook from %s but signature does not match secret", webhook.Repository.FullName)) } } @@ -102,6 +102,14 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r } } +func verifySignature(key, payload, provided string) bool { + mac := hmac.New(sha256.New, []byte(key)) + mac.Write([]byte(payload)) + + expected := mac.Sum(nil) + return hmac.Equal(expected, []byte(provided)) +} + func handleErr(err error) { if err != nil { panic(err) diff --git a/structs/Config.go b/structs/Config.go index a748386..05d433d 100644 --- a/structs/Config.go +++ b/structs/Config.go @@ -16,6 +16,7 @@ type Listener struct { // in your scripts like in node.js or a .sh file, and execute it. // additional filters + Secret string `json:"secret"` Branch string `json:"branch"` // execute only if push is on this branch AllowedPushers []string `json:"allowedPushers"` // execute only if pusher is one of these (username) From 560666f84083b10d0d70342880b2a57de21eb642 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Fri, 4 Feb 2022 19:21:50 +0530 Subject: [PATCH 35/47] add better time system using Discord formatting --- structs/DiscordNotificationManager.go | 33 ++++++++++++--------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/structs/DiscordNotificationManager.go b/structs/DiscordNotificationManager.go index 95ae43e..cf1b827 100644 --- a/structs/DiscordNotificationManager.go +++ b/structs/DiscordNotificationManager.go @@ -51,16 +51,16 @@ func (m *DiscordNotificationManager) Setup() error { } func (m *DiscordNotificationManager) SendPreRunNotification(listener *Listener, ghWebhook *GithubWebhook) error { - t := time.Now() - formattedTime := fmt.Sprintf("%02d/%02d/%02d at %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) + t := time.Now().UTC().Unix() + preRunEmbed := embed.NewEmbed(). SetColor(blurple). SetTitle(fmt.Sprintf("Deploying %s...", listener.Name)). - AddField("Repository", ghWebhook.Repository.FullName). - AddField("Pusher", ghWebhook.Pusher.Name). - AddField("Branch", ghWebhook.Ref[11:]). + AddField("Repository", fmt.Sprintf("[%s](%s)", ghWebhook.Repository.FullName, ghWebhook.Repository.URL)). + AddField("Pusher", fmt.Sprintf("[%s](%s)", ghWebhook.Pusher.Name, "https://github.com/"+ghWebhook.Pusher.Name)). + AddField("Branch", fmt.Sprintf("[%s](%s)", ghWebhook.Ref[11:], ghWebhook.Repository.URL+"/tree/"+ghWebhook.Ref[11:])). AddField("Command", listener.Command). - SetFooter(formattedTime). + AddField("Time", fmt.Sprintf("", t)). MessageEmbed webhookParams := discordgo.WebhookParams{Embeds: []*discordgo.MessageEmbed{preRunEmbed}} @@ -73,26 +73,24 @@ func (m *DiscordNotificationManager) SendPreRunNotification(listener *Listener, } func (m *DiscordNotificationManager) SendSuccessMessage(listener *Listener, output *string, ghWebhook *GithubWebhook) error { - t := time.Now() - formattedTime := fmt.Sprintf("%02d/%02d/%02d at %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) + t := time.Now().UTC().Unix() successEmbed := embed.NewEmbed(). SetTitle(fmt.Sprintf("Succesfully deployed %s", listener.Name)). - SetColor(green). - SetFooter(formattedTime) + SetColor(green) if !listener.Discord.NotifyBeforeRun { successEmbed = successEmbed. - AddField("Repository", ghWebhook.Repository.FullName). - AddField("Pusher", ghWebhook.Pusher.Name). - AddField("Branch", ghWebhook.Ref[11:]). + AddField("Repository", fmt.Sprintf("[%s](%s)", ghWebhook.Repository.FullName, ghWebhook.Repository.URL)). + AddField("Pusher", fmt.Sprintf("[%s](%s)", ghWebhook.Pusher.Name, "https://github.com/"+ghWebhook.Pusher.Name)). + AddField("Branch", fmt.Sprintf("[%s](%s)", ghWebhook.Ref[11:], ghWebhook.Repository.URL+"/tree/"+ghWebhook.Ref[11:])). AddField("Command", listener.Command) } - if listener.Discord.SendOutput { successEmbed = successEmbed. AddField("Output", fmt.Sprintf("```\n%s\n```", *output)) } + successEmbed = successEmbed.AddField("Time", fmt.Sprintf("", t)) webhookParams := discordgo.WebhookParams{Embeds: []*discordgo.MessageEmbed{successEmbed.MessageEmbed}} _, err := m.Session.WebhookExecute(m.Webhook.Id, m.Webhook.Token, false, &webhookParams) @@ -104,14 +102,12 @@ func (m *DiscordNotificationManager) SendSuccessMessage(listener *Listener, outp } func (m *DiscordNotificationManager) SendErrorMessage(listener *Listener, error *error, ghWebhook *GithubWebhook) error { - t := time.Now() - formattedTime := fmt.Sprintf("%02d/%02d/%02d at %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) + t := time.Now().UTC().Unix() errorEmbed := embed.NewEmbed(). SetTitle(fmt.Sprintf("There was an error while deploying %s", listener.Name)). SetColor(red). - AddField("Error", (*error).Error()). - SetFooter(formattedTime) + AddField("Error", (*error).Error()) if !listener.Discord.NotifyBeforeRun { errorEmbed = errorEmbed. @@ -120,6 +116,7 @@ func (m *DiscordNotificationManager) SendErrorMessage(listener *Listener, error AddField("Branch", ghWebhook.Ref[11:]). AddField("Command", listener.Command) } + errorEmbed = errorEmbed.AddField("Time", fmt.Sprintf("", t)) webhookParams := discordgo.WebhookParams{Embeds: []*discordgo.MessageEmbed{errorEmbed.MessageEmbed}} _, err := m.Session.WebhookExecute(m.Webhook.Id, m.Webhook.Token, false, &webhookParams) From 7bd6ba3f8e3d0d378812c729003d05714dd38d74 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 6 Feb 2022 14:59:01 +0530 Subject: [PATCH 36/47] improve logger --- config/executeConfig.go | 3 +-- go.mod | 9 ++++--- go.sum | 4 +++ logger/logger.go | 48 ++++++----------------------------- logger/logger_test.go | 43 ------------------------------- main.go | 30 ++++++++++++++++++++-- router/{main.go => router.go} | 5 ++-- 7 files changed, 49 insertions(+), 93 deletions(-) delete mode 100644 logger/logger_test.go rename router/{main.go => router.go} (90%) diff --git a/config/executeConfig.go b/config/executeConfig.go index 5ad4517..9a86bba 100644 --- a/config/executeConfig.go +++ b/config/executeConfig.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/router" "net/http" @@ -16,7 +15,7 @@ func ExecuteConfig() error { // start server r := router.CreateRouter(config) - logger.Log(fmt.Sprintf("Listening for Github webhooks at %s", config.Port)) + logger.All.Printf("Listening for Github webhooks on port %s\n", config.Port) err = http.ListenAndServe(config.Port, r) if err != nil { return err diff --git a/go.mod b/go.mod index 7ce60b9..31fa8e1 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module github.com/DeathVenom54/github-deploy-inator go 1.17 require ( - github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820 // indirect - github.com/clinet/discordgo-embed v0.0.0-20220113222025-bafe0c917646 // indirect - github.com/go-chi/chi/v5 v5.0.7 // indirect + github.com/bwmarrin/discordgo v0.23.3-0.20211228023845-29269347e820 + github.com/clinet/discordgo-embed v0.0.0-20220113222025-bafe0c917646 + github.com/go-chi/chi/v5 v5.0.7 +) + +require ( github.com/gorilla/websocket v1.4.2 // indirect golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect diff --git a/go.sum b/go.sum index 8e1b04a..de9f059 100644 --- a/go.sum +++ b/go.sum @@ -10,9 +10,13 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/logger/logger.go b/logger/logger.go index 87dd593..58dee33 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -1,49 +1,17 @@ package logger import ( - "fmt" + "io" "log" "os" ) -func writeToFile(file, text string) error { - f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - return err - } - - defer func(f *os.File) { - err := f.Close() - if err != nil { - log.Printf("Error while closing file %s: %s\n", file, err) - } - }(f) - - log.SetOutput(f) - log.Println(text) - log.SetOutput(os.Stderr) - - return nil -} - -func Log(message string) { - fmt.Println(message) - - err := writeToFile("all.log", message) - if err != nil { - log.Fatalf("Error while writing log to normal.log: %s", err) - } -} - -func Error(errorMsg error, fatal bool) { - err := writeToFile("error.log", errorMsg.Error()) - if err != nil { - log.Fatalf("Error while writing log to errorMsg.log: %s", err) - } +var ( + All *log.Logger + Err *log.Logger +) - if fatal { - log.Fatalln(errorMsg) - } else { - log.Println(errorMsg) - } +func Setuplogger(allFile, errFile *os.File) { + All = log.New(io.MultiWriter(allFile, os.Stdout), " ", log.LstdFlags) // blank space to pad error logs + Err = log.New(io.MultiWriter(errFile, os.Stderr), "ERROR ", log.LstdFlags) } diff --git a/logger/logger_test.go b/logger/logger_test.go deleted file mode 100644 index 7ee87ad..0000000 --- a/logger/logger_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package logger - -import ( - "errors" - "fmt" - "io/ioutil" - "strings" - "testing" -) - -func TestLogger(t *testing.T) { - runLogTest(t, "This is a test log.", "all.log") - runLogTest(t, "This is a test error.", "error.log") -} - -func runLogTest(t *testing.T, message, filename string) { - t.Run(fmt.Sprintf("should add \"%s\" to %s", message, filename), func(t *testing.T) { - if filename == "all.log" { - Log(message) - } else { - Error(errors.New(message), false) - } - - // read file to make sure it has the log at the end - log, err := ioutil.ReadFile(filename) - handle(t, err) - - success := strings.HasSuffix(string(log), message+"\n") - if !success { - t.Errorf("%s doesn't end with \"%s\\n\"", filename, message) - } - - // write a different log to prevent - // a false positive on the next test run - Log("Reset") - }) -} - -func handle(t *testing.T, err error) { - if err != nil { - t.Error(err) - } -} diff --git a/main.go b/main.go index 7183532..6197ea3 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,37 @@ package main import ( "github.com/DeathVenom54/github-deploy-inator/config" "github.com/DeathVenom54/github-deploy-inator/logger" + "os" ) func main() { - err := config.ExecuteConfig() + // setting up logger + allFile, err := os.OpenFile("all.log", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644) if err != nil { - logger.Error(err, true) + panic(err) + } + errFile, err := os.OpenFile("error.log", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0644) + if err != nil { + panic(err) + } + + // close file + defer func(allFile, errFile *os.File) { + err := allFile.Close() + if err != nil { + panic(err) + } + err = errFile.Close() + if err != nil { + panic(err) + } + }(allFile, errFile) + + logger.Setuplogger(allFile, errFile) + + // finally, run app + err = config.ExecuteConfig() + if err != nil { + logger.Err.Fatalln(err) } } diff --git a/router/main.go b/router/router.go similarity index 90% rename from router/main.go rename to router/router.go index a23b34c..a75b877 100644 --- a/router/main.go +++ b/router/router.go @@ -1,7 +1,6 @@ package router import ( - "fmt" "github.com/DeathVenom54/github-deploy-inator/handlers" "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" @@ -24,13 +23,13 @@ func recovery(next http.Handler) http.Handler { defer func() { err := recover() if err != nil { - logger.Error(fmt.Errorf("error: %v", err), false) + logger.Err.Println(err) w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusInternalServerError) _, err := w.Write([]byte("500 Internal server error")) if err != nil { - logger.Error(err, false) + logger.Err.Println(err) } } From 3fbd6cdb366c1b58a78e6eb39472e7f06da1256c Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 6 Feb 2022 16:30:33 +0530 Subject: [PATCH 37/47] add logs --- handlers/webhookHandler.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/handlers/webhookHandler.go b/handlers/webhookHandler.go index de0bb0c..5afee56 100644 --- a/handlers/webhookHandler.go +++ b/handlers/webhookHandler.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "encoding/json" "fmt" + "github.com/DeathVenom54/github-deploy-inator/logger" "github.com/DeathVenom54/github-deploy-inator/structs" "io/ioutil" "net/http" @@ -29,7 +30,7 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r } } if listener == nil { - panic(fmt.Errorf("no listener found for webhook from %s", webhook.Repository.FullName)) + panic(fmt.Sprintf("no listener found for webhook from %s", webhook.Repository.FullName)) return } @@ -48,6 +49,7 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r if listener.Branch != "" { branch := webhook.Ref[11:] if listener.Branch != branch { + panic(fmt.Sprintf("received webhook from %s but branch does not match branch \"%s\" on listener %s", webhook.Repository.FullName, listener.Branch, listener.Name)) return } } @@ -60,6 +62,7 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r } } if !pusherIsAllowed { + panic(fmt.Sprintf("received webhook from %s but pusher %s is not a part of listener.pushers", webhook.Repository.FullName, webhook.Pusher.Name)) return } } @@ -99,6 +102,7 @@ func CreateWebhookHandler(config *structs.Config) func(w http.ResponseWriter, r } w.WriteHeader(200) + logger.All.Printf("Successfully executed webhook from %s\n", webhook.Repository.FullName) } } From 00855581c431613856ab0bc11781248de47cd288 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 6 Feb 2022 17:01:47 +0530 Subject: [PATCH 38/47] start writing readme --- README.md | 10 +++++++-- docs/deploy.svg | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 docs/deploy.svg diff --git a/README.md b/README.md index b03de70..45312ec 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ -# pm2-deploy-inator -A Go app to automatically deploy your pm2 apps +# GitHub Deploy-inator + +[![Discord](https://img.shields.io/discord/873232757508157470?color=%235865F2&label=support&style=for-the-badge)](https://discord.gg/qJnrRvt7wW) + +![project logo](docs/deploy.svg) + +GitHub Deploy-inator is a Go program, compiled to binary, which can listen +for GitHub webhooks and run commands in specific directories based on it. diff --git a/docs/deploy.svg b/docs/deploy.svg new file mode 100644 index 0000000..52c568a --- /dev/null +++ b/docs/deploy.svg @@ -0,0 +1,58 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + generic logo inc + + From 993db4ce88d1285c23724f1e48d6b02771f39b6e Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Sun, 6 Feb 2022 17:04:02 +0530 Subject: [PATCH 39/47] use png logo --- README.md | 2 +- docs/deploy.svg | 58 ------------------------------------------------ docs/logo.png | Bin 0 -> 18288 bytes 3 files changed, 1 insertion(+), 59 deletions(-) delete mode 100644 docs/deploy.svg create mode 100644 docs/logo.png diff --git a/README.md b/README.md index 45312ec..ff1abca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Discord](https://img.shields.io/discord/873232757508157470?color=%235865F2&label=support&style=for-the-badge)](https://discord.gg/qJnrRvt7wW) -![project logo](docs/deploy.svg) +![project logo](docs/logo.png) GitHub Deploy-inator is a Go program, compiled to binary, which can listen for GitHub webhooks and run commands in specific directories based on it. diff --git a/docs/deploy.svg b/docs/deploy.svg deleted file mode 100644 index 52c568a..0000000 --- a/docs/deploy.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - generic logo inc - - diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c7010e1abe204329f06f89bce31f97c495706256 GIT binary patch literal 18288 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≪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 eZns%^|N0-Df;LNT783yg0000 Date: Sun, 6 Feb 2022 17:05:44 +0530 Subject: [PATCH 40/47] resize logo --- docs/logo.png | Bin 18288 -> 8577 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/logo.png b/docs/logo.png index c7010e1abe204329f06f89bce31f97c495706256..709d89d98a472bb6911f6aeb663487cc8a76c862 100644 GIT binary patch literal 8577 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 AqW}N^ literal 18288 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≪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 eZns%^|N0-Df;LNT783yg0000 Date: Sun, 6 Feb 2022 17:07:54 +0530 Subject: [PATCH 41/47] resize logo --- docs/logo.png | Bin 8577 -> 5992 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/logo.png b/docs/logo.png index 709d89d98a472bb6911f6aeb663487cc8a76c862..51db74a87fae17d46bb316e0a0b0795cb1f4f3aa 100644 GIT binary patch literal 5992 zcmV-u7nkUXP)1^@s6O5q)900009a7bBm000GH z000GH0Rh;3@c;k-8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H17WGL) zK~#90?VWpcTg9EnzrVRxj-8h+IRpX?X&@xD;jt`9BpYaxEl?hXc9)7+B1v1IIfMX9 z&$gR(OIzq3C>*-GrMq1g==MQCa^eL_Llepdk`k~bV@e4m5MEi{kl2zEhxm2x{Pqvq zfxNBJ9bL(u&pAGJWc_BovF=whnmhCR5lWosCa$n{24t$p&C5d(4lcp7Ce z4Dwkf+)B1}b3>|Qxp2jei8F$4`XxsA(P?xgTd`EfO~R5dF|ZKED27905$Nk|@hkC` z%rY?hG665TS)y&3YWvvi69D}XK%m(D8_JprCfpiJXIDj=->xcl=S4M3bltYGwu3bQ zK3B|s4a1HLZEMVf=db2b#q6}GX0fa6I|0-cvroe?g>ha@ZSIz0c3CvD#8cTX16WY3 z{tP1k=Ia~MnX`)3T~W*uUC9=MZ!1P$h7pAz#=&lgdQMT6j_oPgOTn1odBcS5U~B=9 z0jP(;St9f>uzTO93KFfJ_ium@qE+(#Z-m5L$)G9$R5LINKnTDI048}XAslaMKRZ!3 zVXepQ?nhC~!i=RJto1n{tYt}SEbz0vY`xS}U!5tuSwkTt<^-Pv@Z?aR|MlmxHQn9r z43L;b!1(~qMS-iL47wVCcC%<-uWo4Vm`sx1ETFGGfPdjg{%KSFgm=BvX7HbA>$an8 z8TdB9*FoJh-Hwf;nr4M|c&Tk)w2v$##U+69O{9!qTzr3B?bDu$GL#HWb0&TO;M)z& z9S@SARp_6tll2mN-Pnd>9wAH(Q1|jZSB(m)G7sA#aj|I zlnx}Idgf?)w0Vch6mH09HH&oZ6O{r9&n4&7Zt_@;p$tgQsoey^a|)qk!BM;3hKN?P zF!6M?K>X2TDTZ=_@u*6eAfa}>4Yhrpje~K5S{$pqvlxa6L~5_U@|*Jz(Y+dWv{Dq& z%O2}6lo8CdMJW#K_3nVZ&}x@}pNhmtrUzMs@9Y1%R7x9I0JzLq@Mzh*eg? z-fpU&+~v6zL)q|~c~iRqypw-qIh%z)k=HU>V@A*3*P@ffG_#fH}07yMQ-g4l28_~Frea#FWhD)J2=kis8HKLkuA zDgQT~%G?{@kW(2k4S7K-Z<={uZ~36lEd5(u6n2qv363m#AO;q4KyHeqvtRLB%y5R) zu7mO%S>~=+PH)uhsSrI(0`o($RCZ;oZRZ5P#SBLXP+8>Bx{KbIykUo-jM7nZ6ps61 zJe^(Sw~}E<$z2DfXqGaAk7VZGHne0`MVs4?@mq~SAI2;mL4+^aLcADHWfuFDG0~tG zV-|0zVc@PxD6#alAVZe%up<6(k%AvM9kXeQjcC7u1# zf@ijSp9acMbc|W_bEfNB&&69Z%c3jUR0}9$j9H396%%f?r(~aLXwIJIw-|#O#w^8z zaT*CuH>5MSMmMq6tr-UIF=i<~0b(q(JF{)`Tia**Eygf(j9L8SWFbX!Jk@cB+5TT1 z7_<0`&7kYXx|JInTC#Kf7GpRx#w?`(<9H%|q9K*JE4psmSij{M4vaBN=|BW5wyW$H zVyTXE{1#)_A7hr%dvre$PZ(dl7*wTa5puivw!Qi5<@@6ETzp+#Qb0^ zm0cNqvQum362sxqW|rdu?M1IsK78S*UD0a}nH?Uj*PJ%9>;}fEMUu%Gc0gz%GygW0 z$~``iFVdj#|?ovWrC_%&ikeOPX?#nRa|@u#y>{1&4FQpy_!^h#H8Mss+ynME+0X_$V#2#Yrl z$3tcewHbz`eBB)$eScuWWTiNkW@O2OJ%`L%vm0W~?OKcZ6cw5yP=?Q8b)E2FiCx{c`aoG@>hansv(`ZRp(NwRxGLdrm1w@4FxS` z8JIh(RQ=kO3{N!EFmDJDV_8iw+ZJzbpX0Zfd^vG;=v}C#@;zzc)14E&)&uAoS%gsC z&-5CIN-k|@SwlF9g?Kui>bPU!^B;`yTh36qkw^kinTiS5?!CxsJ%A3g^xr7^nqqXb zmr@LSuo-ln(^auCp32ViTg=emv{Dr4a<7#D8q5+;=Pm}IlqZXspY&LcVQ-FO@Z$}s z%w3m1wO!l&2=5RZl)@PGv2;hB$6~xS%lWH$6hq#w7~bm!NU8q0Auou4#a*`b!u(Xn zxqgc|G`*_^N1JAac6h1Ba7d;I&>v%|?8-&yohpTL zwUdk-`5^$s*#^&fIhFlqFZFn17G{dKblgb5;sP4^4^Olh@`R&(?7SFDbzI`Nk^{ng zr)nq>z87!lxX~*q1@BsoKa)F-UA_z8vsK$*Ak`QS)|cu^SS+1+8jO0sb?AqhoL+lr z)91!*_gl#zbJwvwsVch>z-J0*=Jx`;^!`Y#Lb>pdQ5#v9>9SNuB$n#<6}$4~LW}~y z5mNqrsTyTu!BM-SZ%aItDPV@+E$iozBf;0Buu5PTOQP4}EtxwT(z#ixjSOjIiKRX~ zQgC1z6J|1VJ&5K4@MLQ^5x$=Yhi>trEnSS9k!3Gdv*4>5=hVLKw-5k`w`Bj7iNEDh zb8i5&o=7$mV^eQ$#p^>EO(~Yjv>o8_vFJbPIQXTDyOQDHl2RX4MyOc;U*z0;HRwdUqy`B62n{eQ#!mi9tcrV6GpQu{xGc^U{e1IQJtU#-(H z0(LX77NFlba#v$h{p7MdGHj+{`_WCzxAlz=-O@CZdi@jEi=*vX-0yL&W z(q$Hxz`r2i2lq#6cWP+As2H;x7y{Nz4+pk8yvu5 z&~*$fjHNPv0MP@^n>DMNE+tn}e}A3N`)!oyO1A7Nxr@Mj2^?I=prEe4beUz4 zAb~RN%x`UEA6vrrlA4|uClZa(= ztR;JG(fc+i!N68xd2S*STGUiuo$*skLreBLAw@HQnZ@qg259XkA0U>*L^S}92WapP zzSqZ510eo+ES)(r84lgxaoYy(Ud^W!h>1jK$z z(nH!C7HxjJDqxSD0heb2I8zeNCcvxwOiPoI(0_W=mKH|hY8V5rk)$h=^}#>+tpxz$ z>C83E_;-(*+YG|vko=UiY4gtQ!B^K@K!;64uDnG*p30rh#8-iN5dig??t~r*3anV-&{D!^$ll+!x(=T7Edx)$BDVs_|7l{S=D z!xE92X90XO{!H#C?7H^=^fyWkF-8gEI{?g8)nf3B7nsE5jp3RDZLakV!TfVT-6#w^ zY}xLT`(_>SenC142?l_~?3y=bghS^5e1~!)jL*l?IW@Bxyr7Q>w>jg2vr5e<@s{?p z0ent1oKG5uH&ohSc9#Np!V5iyp~FDR0$*`|xOTJO zS`Gx0SXw~%?TJXxvvc^KE*+2R+IckyN@+KV=)&?y&ogZ3BF1;!w}NNhA1O_vMB6ge z04`7rf7W>`_&qQ6cxsm4%$wQ`=4DF3j8WYIah{iY3Y%W_Xk)P-&<gTg+h*^m)a|bYnQA zmZ0wtcz*|J8`gi*hg2#zjn!2yU?jqfM;s|n zN<_-fDACq!N1;D)N`5dld$CM>AoOj~CvF8J@6`W}-FrL$Q=_FI=tClTZ8B2Zz*w#sjaQPYq09dG92ZHc|*IEYAHnNbU z*H|Ta3t^J?YDNU_6KF{y5`5TiAzr}Xqg3LE^wlKQHnK>IlrkAIXtdW_hB?f@-2!~# za4`x1kYuInz=7IzHx#suEW&X*S*UoCwPoiRZZNPFLVUAvPR-+fYw;Q;wKvTQkhTOV z+C~w2dq_QK!nCFP3H+b{IA>%PSh| zCTLsrDFql*rIa||&S^7?wvnaRCZ&-@m}%I-aF}pgE)zU`xElokh`CZB%+;C5QQOE8 zL~wT}vU$fb6<%Oz7_yNdE>DJQp7mQuu~57Y_RD7_;c-O$lDSA`*Jc zZy|=lFgGRj!3^A8<+vxWh}0UR3P@e0O^#PW({{e)J(r5N;JMwY_qW5R8@Y;ab>7-d*fiX%%o9sYV=vbcOjq~_0l z3mJx#+;yOBWLZBKYU%!&mCKqo`(~#$9T>t==u~`86KqZPSuK*aX2sEM7Nrv33_90qC5G}rg1@K|$AYw$E7xikX{m`PAiSV? z$270?7)qaLTjp5RlHLI)P{wB28hir`S1c-m_{`=Wn~KI2Kn$N)qBF4o#H(^EZ$ zl4M~zH&qZl47ClEU}n|pY{+OgizK&@v_GGMq*N@G`QGJEZ6D{EC_~9`{%Rf-Z^
  • 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 AqW}N^ From 19fb79ec4898c08ae580054c69be281b158b4f6a Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Thu, 17 Feb 2022 21:24:33 +0530 Subject: [PATCH 42/47] write config and contributing in README --- README.md | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff1abca..942e80c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,111 @@ -# GitHub Deploy-inator -[![Discord](https://img.shields.io/discord/873232757508157470?color=%235865F2&label=support&style=for-the-badge)](https://discord.gg/qJnrRvt7wW) +

    + Project logo GitHub Deploy-inator

    -![project logo](docs/logo.png) + + +> Automatic deployment app based on GitHub webhooks + +## 💡 Motivation + +I code and maintain a lot of Discord bots and other projects, all +running on my web server. Every time I pushed an update to a bot, I'd +have to SSH in, pull the code from Github, build it and restart the +process. Although I managed to boil this down into a sweet `yarn deploy`, +that still needed me to SSH into the server. I tried implementing git hooks +but to no avail. + +And then, this project was born. + +## 🛠️ Installation and Setup + +Note to future me: Write out this part + +## 📝 config.json + +All the required data must be provided in a config.json file, placed in the current +working directory. + +### Format + +- `port`: The port on which the application will listen for webhooks + - type: `string` + - format: `":DDDD"`, where D is a digit + - example: `":8000"`, `":440"` +

    + +- `endpoint`: The endpoint where the webhooks will be sent + - type: `string` + - format: `"/*"` + - example: `"/webhooks/github"`, `"/github/listener"` +

    + +- `listeners`: Settings for individual listeners + - type: `Listener[]` + +#### Listener + +- `name`: [required] A unique name for the listener. This is mentioned when a webhook is received, executed or failed. + - type: `string` + - example: `my-chat-app` (try not to include spaces) +

    + +- `repository`: [required] The full name of the repository for which this webhook will be executed. + - type: `string` + - format: `"author-name/repository-name"` + - example: `"DeathVenom54/github-deploy-inator"` +

    + +- `directory`: [required] The absolute path to the directory (folder) where the command will be executed. + - type: `string` + - example: `"E:/projects/github-deploy-inator"`, `"/home/dv/projects/github-deploy-inator"` +

    + +- `command`: [required] The command to run when the webhook is received. + - type: `string` + - example: `"yarn deploy"`, `"git pull origin main"` +

    +- `secret`: The secret token set for your webhook. This makes sure that the webhook is from GitHub and is highly recommended to set. + - type: `string` + - example: `j4g34O3TK2JF4jrnjrkj34nt3i4` +

    + +- `branch`: Execute the command only if the push was to this branch. + - type: `string` + - example: `"main"`, `"dev"` +

    + +- `allowedPushers`: Execute the command only if this array contains the pusher's GitHub username + - type: `string[]` + - example: `["DeathVenom54", "webnoob"]` +

    + +- `notifyDiscord`: If you want to receive a notification on Discord (via webhook) + - type: `boolean` +

    + +- `discord`: This contains information needed for sending Discord notifications + - type: `Discord` + +#### Discord + +- `webhook`: [required] The url of the webhook where notifications should be sent + - type: `string` + - example: `"https://discord.com/api/webhooks/938275411766720533/s4nhfM-8XH1hMu9WYqSBUFaSD_erXSn6qqfdazzieCwtlINZho4teSvdlnEYgBM1E1IO"` +

    + +- `notifyBeforeRun`: Whether a notification should be sent before running the command + - type: `boolean` +

    + +- `sendOutput`: Whether the notification should contain the output sent by the command + - type: `boolean` + +## 💻 Contributing to this project + +If you find any bug in this project, have a suggestion or wish to contribute to it, feel free to [open an issue](https://github.com/DeathVenom54/github-deploy-inator/issues/new). -GitHub Deploy-inator is a Go program, compiled to binary, which can listen -for GitHub webhooks and run commands in specific directories based on it. From e55fc8a29ba22766425d3ee52905913274a9b33b Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Thu, 17 Feb 2022 21:39:31 +0530 Subject: [PATCH 43/47] final readme --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 942e80c..d79b1e5 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,10 @@ And then, this project was born. ## 🛠️ Installation and Setup -Note to future me: Write out this part +I have no idea. [Use systemd perhaps](https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6). + +Download the built version from [releases](https://github.com/DeathVenom54/github-deploy-inator/releases) and unzip it to a directory. +Edit the config.json to your requirements and run the executable. ## 📝 config.json From be6a68f7100e4ae99e57b3ba0111dc3f80cd2505 Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Fri, 18 Feb 2022 10:29:13 +0530 Subject: [PATCH 44/47] add build script --- build.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 build.sh diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..e326dc4 --- /dev/null +++ b/build.sh @@ -0,0 +1,10 @@ +# build for linux +GOOS=linux GOARCH=amd64 go build -o build/github_deploy-inator_linux_x64 main.go && echo "Built linux x64" +GOOS=linux GOARCH=386 go build -o build/github_deploy-inator_linux_x86 main.go && echo "Built linux x86" + +# build for windows +GOOS=windows GOARCH=amd64 go build -o build/github_deploy-inator_windows_x64.exe main.go && echo "Built windows x64" +GOOS=windows GOARCH=386 go build -o build/github_deploy-inator_windows_x86.exe main.go && echo "Built windows x86" + +# build for macos +GOOS=darwin GOARCH=amd64 go build -o build/github_deploy-inator_macos_x64.exe main.go && echo "Built macos x64" \ No newline at end of file From 815d78c47bd3bcaf5cdf30ef06e4a1136a0af2db Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Fri, 18 Feb 2022 10:32:04 +0530 Subject: [PATCH 45/47] add build script --- .gitignore | 1 + build.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b87c03c..037c9ab 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.dll *.so *.dylib +build/ # Test binary, built with `go test -c` *.test diff --git a/build.sh b/build.sh index e326dc4..cee4aa3 100644 --- a/build.sh +++ b/build.sh @@ -7,4 +7,4 @@ GOOS=windows GOARCH=amd64 go build -o build/github_deploy-inator_windows_x64.exe GOOS=windows GOARCH=386 go build -o build/github_deploy-inator_windows_x86.exe main.go && echo "Built windows x86" # build for macos -GOOS=darwin GOARCH=amd64 go build -o build/github_deploy-inator_macos_x64.exe main.go && echo "Built macos x64" \ No newline at end of file +GOOS=darwin GOARCH=amd64 go build -o build/github_deploy-inator_macos_x64 main.go && echo "Built macos x64" \ No newline at end of file From f1e8eb8d055e5d06d1c6db824033729d7afa7d3d Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Fri, 18 Feb 2022 10:32:04 +0530 Subject: [PATCH 46/47] add build script --- .gitignore | 1 + build.sh | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index b87c03c..037c9ab 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.dll *.so *.dylib +build/ # Test binary, built with `go test -c` *.test diff --git a/build.sh b/build.sh index e326dc4..7f21dc7 100644 --- a/build.sh +++ b/build.sh @@ -1,10 +1,27 @@ # build for linux -GOOS=linux GOARCH=amd64 go build -o build/github_deploy-inator_linux_x64 main.go && echo "Built linux x64" -GOOS=linux GOARCH=386 go build -o build/github_deploy-inator_linux_x86 main.go && echo "Built linux x86" +GOOS=linux GOARCH=amd64 go build -o build/github_deploy-inator_linux_x64 main.go && echo "Built linux x64" || exit +GOOS=linux GOARCH=386 go build -o build/github_deploy-inator_linux_x86 main.go && echo "Built linux x86" || exit # build for windows -GOOS=windows GOARCH=amd64 go build -o build/github_deploy-inator_windows_x64.exe main.go && echo "Built windows x64" -GOOS=windows GOARCH=386 go build -o build/github_deploy-inator_windows_x86.exe main.go && echo "Built windows x86" +GOOS=windows GOARCH=amd64 go build -o build/github_deploy-inator_windows_x64.exe main.go && echo "Built windows x64" || exit +GOOS=windows GOARCH=386 go build -o build/github_deploy-inator_windows_x86.exe main.go && echo "Built windows x86" || exit # build for macos -GOOS=darwin GOARCH=amd64 go build -o build/github_deploy-inator_macos_x64.exe main.go && echo "Built macos x64" \ No newline at end of file +GOOS=darwin GOARCH=amd64 go build -o build/github_deploy-inator_macos_x64 main.go && echo "Built macos x64" || exit + +echo "Built all platforms, zipping files..." + +cd build || exit + +zip -r linux_x64.zip github_deploy-inator_linux_x64 config.json || exit +zip -r linux_x86.zip github_deploy-inator_linux_x86 config.json || exit + +zip -r windows_x64.zip github_deploy-inator_windows_x64.exe config.json || exit +zip -r windows_x86.zip github_deploy-inator_windows_x86.exe config.json || exit + +zip -r macos_x64.zip github_deploy-inator_macos_x64 config.json || exit +zip -r macos_x86.zip github_deploy-inator_macos_x86 config.json || exit + +echo "Successfully zipped all files" + +cd .. \ No newline at end of file From cacf7e3f740a664d2c2966fcac3bd312e5aa1a2b Mon Sep 17 00:00:00 2001 From: DeathVenom54 Date: Fri, 18 Feb 2022 11:59:38 +0530 Subject: [PATCH 47/47] fix errors --- build.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sh b/build.sh index 7f21dc7..d8d5968 100644 --- a/build.sh +++ b/build.sh @@ -20,7 +20,6 @@ zip -r windows_x64.zip github_deploy-inator_windows_x64.exe config.json || exit zip -r windows_x86.zip github_deploy-inator_windows_x86.exe config.json || exit zip -r macos_x64.zip github_deploy-inator_macos_x64 config.json || exit -zip -r macos_x86.zip github_deploy-inator_macos_x86 config.json || exit echo "Successfully zipped all files"