From 79d5a2e4e6423a409aea36643be4dcc1e9405674 Mon Sep 17 00:00:00 2001 From: Marco Antonio Blanco Date: Tue, 21 Nov 2023 09:19:43 +0100 Subject: [PATCH] feat: post application url and commit hash as a PR comment after every deploy (#148) --- assets/github/onbranch.tmpl | 5 +- go.mod | 2 + go.sum | 5 ++ src/cli/onbranch.go | 20 +++++++- src/services/git/repo.go | 99 +++++++++++++++++++++++++++++++++++++ src/services/k8s/knative.go | 5 +- 6 files changed, 130 insertions(+), 6 deletions(-) diff --git a/assets/github/onbranch.tmpl b/assets/github/onbranch.tmpl index 04ee6803..7a62bfa6 100644 --- a/assets/github/onbranch.tmpl +++ b/assets/github/onbranch.tmpl @@ -1,4 +1,4 @@ -# This file is generate by https://github.com/nearform/initium-cli +# This file is generated by https://github.com/nearform/initium-cli name: Deploy on PR on: @@ -8,6 +8,8 @@ on: permissions: contents: read packages: write + issues: write + pull-requests: write jobs: on_pr: @@ -24,6 +26,7 @@ jobs: with: args: onbranch env: + GITHUB_TOKEN: {{ `${{ secrets.GITHUB_TOKEN }}` }} INITIUM_REGISTRY_USER: {{ `${{ github.actor }}` }} INITIUM_REGISTRY_PASSWORD: {{ `${{ secrets.GITHUB_TOKEN }}` }} INITIUM_CLUSTER_ENDPOINT: {{ `${{ secrets.CLUSTER_ENDPOINT }}` }} diff --git a/go.mod b/go.mod index 8b4c4058..b93ea554 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,8 @@ require ( github.com/google/gnostic v0.6.9 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-containerregistry v0.13.0 // indirect + github.com/google/go-github/v56 v56.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.15 // indirect diff --git a/go.sum b/go.sum index 5d226a1c..6f110aad 100644 --- a/go.sum +++ b/go.sum @@ -140,12 +140,17 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k= github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo= +github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4= +github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/src/cli/onbranch.go b/src/cli/onbranch.go index 67ea4445..34edaf00 100644 --- a/src/cli/onbranch.go +++ b/src/cli/onbranch.go @@ -2,6 +2,8 @@ package cli import ( "fmt" + "net/url" + "os" "github.com/nearform/initium-cli/src/services/git" "github.com/nearform/initium-cli/src/utils" @@ -29,7 +31,23 @@ func (c icli) buildPushDeploy(cCtx *cli.Context) error { if cCtx.Bool(stopOnPushFlag) { return err } - return c.Deploy(cCtx) + + err = c.Deploy(cCtx) + appUrl, urlErr := url.Parse(err.Error()) // Check if it contains the app URL or it's a legit error + if urlErr != nil { + fmt.Println("No app URL available") + return err + } + + // Check if the CI environment variable is set to GitHub Actions + if os.Getenv("CI") == "true" && os.Getenv("GITHUB_ACTIONS") == "true" { + err = git.PublishCommentPRGithub(appUrl.String()) + } else { + fmt.Printf("You can reach the app via %s\n", appUrl.String()) + err = nil + } + + return err } func (c icli) OnBranchCMD() *cli.Command { diff --git a/src/services/git/repo.go b/src/services/git/repo.go index 8fadf25c..35a95969 100644 --- a/src/services/git/repo.go +++ b/src/services/git/repo.go @@ -1,11 +1,17 @@ package git import ( + "context" "fmt" "os" + "regexp" + "strconv" "strings" + "time" git "github.com/go-git/go-git/v5" + github "github.com/google/go-github/v56/github" + oauth2 "golang.org/x/oauth2" ) const ( @@ -108,3 +114,96 @@ func GetGithubOrg() (string, error) { splitRemote := strings.Split(remote, "/") return splitRemote[0], nil } + +func PublishCommentPRGithub (url string) error { + var message, owner, repo string + var prNumber int + commitSha, err := GetHash() + + // Build message + message = fmt.Sprintf("Application URL: %s\n", url) + fmt.Sprintf("Commit hash: %s\n", commitSha) + fmt.Sprintf("Timestamp: %v\n", time.Now()) + + // Check GITHUB_TOKEN + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + return fmt.Errorf("Please set up the GITHUB_TOKEN environment variable") + } + + // Create an authenticated GitHub client + ctx := context.Background() + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + // Get required data to publish a comment + repoInfo := os.Getenv("GITHUB_REPOSITORY") + repoParts := strings.Split(repoInfo, "/") + if len(repoParts) == 2 { + owner = repoParts[0] + repo = repoParts[1] + } else { + return fmt.Errorf("Invalid repository information") + } + + // Check if the workflow was triggered by a pull request event + eventName := os.Getenv("GITHUB_EVENT_NAME") + if eventName == "pull_request" { + // Get the pull request ref + prRef := os.Getenv("GITHUB_REF") + + // Extract the pull request number using a regular expression + re := regexp.MustCompile(`refs/pull/(\d+)/merge`) + matches := re.FindStringSubmatch(prRef) + + if len(matches) == 2 { + prNumber, err = strconv.Atoi(matches[1]) + if err != nil { + return fmt.Errorf("Error converting string to int: %v", err) + } + } else { + return fmt.Errorf("Unable to extract pull request number from GITHUB_REF") + } + } else { + return fmt.Errorf("This workflow was not triggered by a pull request event") + } + + // Create comment with body + comment := &github.IssueComment{ + Body: github.String(message), + } + + // List comments on the PR + comments, _, err := client.Issues.ListComments(ctx, owner, repo, prNumber, nil) + if err != nil { + return err + } + + commentID := findExistingCommentIDPRGithub(comments, "Application URL:") // Search for app URL comment + + if commentID != 0 { + // Update existing comment + updatedComment, _, err := client.Issues.EditComment(ctx, owner, repo, commentID, comment) + if err != nil { + return err + } + fmt.Printf("Comment updated successfully: %s\n", updatedComment.GetHTMLURL()) + } else { + // Publish a new comment + newComment, _, err := client.Issues.CreateComment(ctx, owner, repo, prNumber, comment) + if err != nil { + return err + } + fmt.Printf("Comment published: %s\n", newComment.GetHTMLURL()) + } + + return nil +} + +func findExistingCommentIDPRGithub(comments []*github.IssueComment, targetBody string) int64 { + for _, comment := range comments { + if strings.Contains(comment.GetBody(), targetBody) { + return comment.GetID() + } + } + return 0 +} diff --git a/src/services/k8s/knative.go b/src/services/k8s/knative.go index 5c73b0fb..4fe4af02 100644 --- a/src/services/k8s/knative.go +++ b/src/services/k8s/knative.go @@ -253,14 +253,11 @@ func Apply(serviceManifest *servingv1.Service, config *rest.Config) error { return err } if service.Status.URL != nil { - fmt.Printf("You can reach it via %s\n", service.Status.URL) - break + return fmt.Errorf("%s", service.Status.URL) // Overload error return variable with URL string } time.Sleep(time.Millisecond * 500) } - - return nil } func Clean(namespace string, config *rest.Config, project *project.Project) error {