From ee15eca5d2870d68da19bc8ebdbf57780fae6af1 Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Wed, 31 Jul 2024 16:09:14 +0100 Subject: [PATCH 1/9] Refactor and use go only --- Dockerfile | 25 ++--- action.yml | 15 +-- default-message.md.gotmpl | 7 ++ entrypoint.sh | 96 ------------------- github.go | 185 +++++++++++++++++++++++++++++++++++++ go.mod | 7 ++ go.sum | 4 + main.go | 190 ++++++++++++++++++++++++++++++++++++++ message.go | 176 +++++++++++++++++++++++++++-------- notify-pr.sh | 40 -------- 10 files changed, 548 insertions(+), 197 deletions(-) create mode 100644 default-message.md.gotmpl delete mode 100755 entrypoint.sh create mode 100644 github.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go delete mode 100644 notify-pr.sh diff --git a/Dockerfile b/Dockerfile index 615407c..a6b6bad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,16 @@ FROM okteto/okteto:latest as okteto -FROM golang:1.16 as message-builder -RUN go env -w GO111MODULE=off -RUN go get github.com/machinebox/graphql -COPY message.go . -RUN go build -o /message . -RUN curl -L https://github.com/jqlang/jq/releases/download/jq-1.6/jq-linux64 > /usr/bin/jq && chmod +x /usr/bin/jq +FROM golang:1.22 as builder +WORKDIR /app +ENV GO111MODULE=ON +COPY . . +RUN go build -o /deploy-preview . -FROM ruby:3-slim-buster +FROM gcr.io/distroless/static-debian11 -RUN gem install octokit faraday-retry +COPY --from=builder /deploy-preview /deploy-preview +COPY --from=okteto /usr/local/bin/okteto /okteto -COPY notify-pr.sh /notify-pr.sh -RUN chmod +x notify-pr.sh -COPY --from=message-builder /usr/bin/jq /usr/bin/jq -COPY entrypoint.sh /entrypoint.sh -COPY --from=message-builder /message /message -COPY --from=okteto /usr/local/bin/okteto /usr/local/bin/okteto +ENV PATH=/ -ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/deploy-preview"] \ No newline at end of file diff --git a/action.yml b/action.yml index 2cf500d..e41049a 100644 --- a/action.yml +++ b/action.yml @@ -23,17 +23,20 @@ inputs: log-level: description: "Log level string. Valid options are debug, info, warn, error" required: false + comment: + description: "Specify custom comment. Prefix with @ to read from a file." + required: false runs: using: "docker" image: "Dockerfile" args: - ${{ inputs.name }} - - ${{ inputs.timeout }} - - ${{ inputs.scope }} - - ${{ inputs.variables }} - - ${{ inputs.file }} - - ${{ inputs.branch }} - - ${{ inputs.log-level }} + - --timeout=${{ inputs.timeout }} + - --scope=${{ inputs.scope }} + - --variables=${{ inputs.variables }} + - --file=${{ inputs.file }} + - --branch=${{ inputs.branch }} + - --log-level=${{ inputs.log-level }} branding: color: 'green' icon: 'grid' diff --git a/default-message.md.gotmpl b/default-message.md.gotmpl new file mode 100644 index 0000000..9cd54af --- /dev/null +++ b/default-message.md.gotmpl @@ -0,0 +1,7 @@ +Your preview environment [{{ .PreviewName }}]({{ .PreviewURL }}) has been deployed +{{- if ne .PreviewSuccess true }} :warning::rotating_light: **but encountered errors** :rotating_light::warning:{{ end }}. + +Preview environment endpoint is available at: +{{ range $index, $url := .EndpointsMap -}} + * [{{ $index }}]({{ $url }}) +{{ end }} diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 90cfba1..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/sh -set -e - -name=$1 -timeout=$2 -scope=$3 -variables=$4 -file=$5 -branch=$6 -log_level=$7 - -if [ -z $name ]; then - echo "Preview environment name is required" - exit 1 -fi - -if [ -z $scope ]; then - scope=global -fi - -if [ ! -z "$OKTETO_CA_CERT" ]; then - echo "Custom certificate is provided" - echo "$OKTETO_CA_CERT" > /usr/local/share/ca-certificates/okteto_ca_cert.crt - update-ca-certificates -fi - -if [ -z "$branch" ]; then - if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then - branch=${GITHUB_HEAD_REF} - else - branch=${GITHUB_REF#refs/heads/} - fi -fi - -if [ -z "$branch" ]; then - echo "fail to detect branch name" - exit 1 -fi - -repository=$GITHUB_REPOSITORY - -if [ ! -z $timeout ]; then -params="${params} --timeout=$timeout" -fi - -variable_params="" -if [ ! -z "${variables}" ]; then - for ARG in $(echo "${variables}" | tr ',' '\n'); do - variable_params="${variable_params} --var ${ARG}" - done - - params="${params} $variable_params" -fi - -if [ ! -z "$file" ]; then -params="${params} --file $file" -fi - -export OKTETO_DISABLE_SPINNER=1 -if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then - number=$(jq '[ .number ][0]' $GITHUB_EVENT_PATH) -elif [ "${GITHUB_EVENT_NAME}" = "repository_dispatch" ]; then - number=$(jq '[ .client_payload.pull_request.number ][0]' $GITHUB_EVENT_PATH) -fi - -if [ ! -z "$log_level" ]; then - log_level="--log-level ${log_level}" -fi - -# https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging -# https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables -if [ "${RUNNER_DEBUG}" = "1" ]; then - log_level="--log-level debug" -fi - -echo running: okteto preview deploy $name $log_level --scope $scope --branch="${branch}" --repository="${GITHUB_SERVER_URL}/${repository}" --sourceUrl="${GITHUB_SERVER_URL}/${repository}/pull/${number}" ${params} --wait -ret=0 -okteto preview deploy $name $log_level --scope $scope --branch="${branch}" --repository="${GITHUB_SERVER_URL}/${repository}" --sourceUrl="${GITHUB_SERVER_URL}/${repository}/pull/${number}" ${params} --wait || ret=1 - -if [ -z "$number" ] || [ "$number" = "null" ]; then - echo "No pull-request defined, skipping notification." - exit 0 -fi - -if [ -n "$GITHUB_TOKEN" ]; then - if [ $ret = 1 ]; then - message=$(/message $name 1) - else - message=$(/message $name 0) - fi - /notify-pr.sh "$message" $GITHUB_TOKEN $name -fi - -if [ $ret = 1 ]; then - exit 1 -fi diff --git a/github.go b/github.go new file mode 100644 index 0000000..e09f4aa --- /dev/null +++ b/github.go @@ -0,0 +1,185 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "strings" +) + +type GitHubEvent struct { + Number int `json:"number"` + ClientPayload struct { + PullRequest struct { + Number int `json:"number"` + } `json:"pull_request"` + } `json:"client_payload"` +} + +type GitHubComment struct { + ID int `json:"id"` + Body string `json:"body"` +} + +type github struct { + repository string + sourceURL string + prNumber int + defaultBranch string +} + +type ciInfo interface { + Repository() string + SourceURL() string + DefaultBranch() string + Notify(message string) error +} + +var _ ciInfo = (*github)(nil) + +func newGitHub() (ciInfo, error) { + event, err := os.Open(os.Getenv("GITHUB_EVENT_PATH")) + if err != nil { + log.Printf("failed to read GITHUB_EVENT_PATH: %s", os.Getenv("GITHUB_EVENT_PATH")) + return nil, err + } + defer event.Close() + + var payload GitHubEvent + err = json.NewDecoder(event).Decode(&payload) + if err != nil { + log.Printf("failed to parse GITHUB_EVENT_PATH: %s", os.Getenv("GITHUB_EVENT_PATH")) + return nil, err + } + + gh := &github{} + + switch os.Getenv("GITHUB_EVENT_NAME") { + case "pull_request": + + gh.prNumber = payload.Number + gh.defaultBranch = os.Getenv("GITHUB_HEAD_REF") + case "repository_dispatch": + gh.prNumber = payload.ClientPayload.PullRequest.Number + gh.defaultBranch = strings.TrimPrefix(os.Getenv("GITHUB_REF"), "refs/heads/") + } + + gh.repository = fmt.Sprintf("%s/%s", os.Getenv("GITHUB_SERVER_URL"), os.Getenv("GITHUB_REPOSITORY")) + gh.sourceURL = fmt.Sprintf("%s/%s", gh.repository, gh.prNumber) + + return gh, nil +} + +func (gh *github) Repository() string { + return gh.repository +} + +func (gh *github) SourceURL() string { + return gh.sourceURL +} + +func (gh *github) DefaultBranch() string { + return gh.defaultBranch +} + +func (gh *github) PRNumber() int { + return gh.prNumber +} + +func (gh *github) Notify(message string) error { + githubToken := os.Getenv("GITHUB_TOKEN") + githubRepository := os.Getenv("GITHUB_REPOSITORY") + + if githubToken == "" { + log.Println("failed to set message as no GITHUB_TOKEN found") + return errors.New("missing GITHUB_TOKEN") + } + + resp, err := gh.callGitHub(githubToken, "GET", githubRepository, nil, "issues", fmt.Sprintf("%d", gh.prNumber), "comments") + if err != nil { + return fmt.Errorf("failed to retrieve PR comments: %s", err) + } + + defer resp.Body.Close() + + var comments []GitHubComment + json.NewDecoder(resp.Body).Decode(&comments) + + identifier := gh.previewIdentifier() + var comment *GitHubComment + for _, c := range comments { + if strings.Contains(c.Body, identifier) { + comment = &c + break + } + } + + msgBodyBuf, err := gh.getGitHubCommentMessage(message) + if err != nil { + return err + } + if comment == nil { + _, err = gh.callGitHub(githubToken, "POST", githubRepository, msgBodyBuf, "issues", fmt.Sprintf("%d", gh.prNumber), "comments") + if err != nil { + return fmt.Errorf("failed to create new comment: %s", err) + } + return nil + } + + fmt.Println("Message already exists in the PR. Updating") + _, err = gh.callGitHub(githubToken, "PATCH", githubRepository, msgBodyBuf, "issues", "comments", fmt.Sprintf("%d", comment.ID)) + if err != nil { + return fmt.Errorf("failed to update comment: %s", err) + } + return nil +} + +func (gh *github) getGitHubCommentMessage(message string) (*bytes.Buffer, error) { + msgBody := struct { + Body string `json:"body"` + }{ + Body: message + gh.previewIdentifier(), + } + + msgBodyStr, err := json.Marshal(msgBody) + if err != nil { + return nil, fmt.Errorf("failed to encode message body: %v", msgBody) + } + + return bytes.NewBuffer(msgBodyStr), nil +} + +func (gh *github) callGitHub(token string, method string, repo string, body io.Reader, path ...string) (*http.Response, error) { + path = append([]string{repo}, path...) + uri, _ := url.JoinPath("https://api.github.com/repos/", path...) + req, _ := http.NewRequest(method, uri, body) + req.Header.Set("Authorization", "token "+token) + r, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + if r.StatusCode < http.StatusOK || r.StatusCode >= http.StatusBadRequest { + var errResp = struct { + Message string `json:"message"` + }{} + err = json.NewDecoder(r.Body).Decode(&errResp) + if err != nil { + return nil, fmt.Errorf("failed to decode body: %s", err) + } + + return nil, errors.New(errResp.Message) + } + + return r, nil +} + +func (gh *github) previewIdentifier() string { + return fmt.Sprintf("", gh.prNumber) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f224895 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module github.com/okteto/deploy-preview + +go 1.22 + +require golang.org/x/text v0.16.0 + +require github.com/hoshsadiq/godotenv v1.0.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f76a738 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/hoshsadiq/godotenv v1.0.0 h1:Hjx9hW+vqSOm5LN/UwMktxAekKvuXIg+BzIUmGQ7wXQ= +github.com/hoshsadiq/godotenv v1.0.0/go.mod h1:BZLGi0xKHU92H+AKkNoy/BsSFrZUUN3C8SdvyF3gt+c= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b4dcc8f --- /dev/null +++ b/main.go @@ -0,0 +1,190 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "github.com/hoshsadiq/godotenv" + "log" + "os" + "os/exec" + "strings" + "time" +) + +type sliceString []string + +func (i *sliceString) String() string { + if i == nil { + return "" + } + return strings.Join(*i, ";\n") + ";\n" +} + +func (i *sliceString) Set(value string) error { + vars, err := godotenv.Unmarshal(value) + if err != nil { + return err + } + + for k, v := range vars { + *i = append(*i, fmt.Sprintf("%s=%s", k, v)) + } + return nil +} + +type DeployOptions struct { + branch string + file string + logLevel string + name string + scope string + timeout time.Duration + variables sliceString + wait bool + comment string + + ci ciInfo +} + +func exitIfErr(err error) { + if err != nil { + fmt.Println(err) + os.Exit(2) + } +} + +/* +todo: the follow code is left: + + if [ ! -z "$OKTETO_CA_CERT" ]; then + echo "Custom certificate is provided" + echo "$OKTETO_CA_CERT" > /usr/local/share/ca-certificates/okteto_ca_cert.crt + update-ca-certificates + fi +*/ +func main() { + ci, err := getCIInfo() + exitIfErr(err) + + opts := DeployOptions{ + ci: ci, + } + flagSet := flag.NewFlagSet("deploy-preview", flag.ContinueOnError) + flagSet.StringVar(&opts.branch, "branch", ci.DefaultBranch(), "the branch to deploy (defaults to the current branch)") + flagSet.StringVar(&opts.scope, "scope", "global", "the scope of preview environment to create. Accepted values are ['personal', 'global']") + flagSet.StringVar(&opts.logLevel, "log-level", getLogLevel("warn"), "amount of information output (debug, info, warn, error)") + flagSet.DurationVar(&opts.timeout, "timeout", 5*time.Minute, "the length of time to wait for completion, zero means never. Any other values should contain a corresponding time unit e.g. 1s, 2m, 3h ") + flagSet.Var(&opts.variables, "var", "set a preview environment variable, this will be parsed as an env file, but can be set more than once") + flagSet.BoolVar(&opts.wait, "wait", false, "wait until the preview environment deployment finishes (defaults to false)") + flagSet.StringVar(&opts.file, "file", "", "relative path within the repository to the okteto manifest (default to okteto.yaml or .okteto/okteto.yaml)") + flagSet.StringVar(&opts.comment, "comment", "", "Specify custom comment. Prefix with @ to read from a file") + err = flagSet.Parse(os.Args) + + if err != nil { + if errors.Is(err, flag.ErrHelp) { + // pflag already takes care of printing the usage. + os.Exit(0) + } + + exitIfErr(err) + } + + err = validateInput(flagSet, &opts) + exitIfErr(err) + + err = deployPreview(opts) + if err != nil { + log.Printf("deploy failed due to: %s", err) + } + var success = err == nil + + message, err := generateMessage(opts.name, success, opts.comment) + exitIfErr(err) + + err = notify(ci, message) + exitIfErr(err) + + if !success { + os.Exit(1) + } +} + +func notify(ci ciInfo, message string) error { + switch { + case os.Getenv("GITHUB_ACTIONS") == "true": + return ci.Notify(message) + } + + log.Printf("Not notifying anything, CI not supported") + return nil +} + +func getCIInfo() (ciInfo, error) { + switch { + case os.Getenv("GITHUB_ACTIONS") == "true": + return newGitHub() + } + + return nil, errors.New("unsupported CI environment") +} + +func getLogLevel(def string) string { + // https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging + // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + if os.Getenv("RUNNER_DEBUG") == "1" { + return "debug" + } + + return def +} + +func validateInput(flagSet *flag.FlagSet, opts *DeployOptions) error { + if flagSet.NArg() != 2 { + return errors.New("preview environment name is required") + } + + if opts.branch == "" { + // this essentially means that retrieveDefaultBranch was unable to find a value + return errors.New("failed to detect branch") + } + + opts.name = flagSet.Arg(1) + return nil +} + +func deployPreview(opts DeployOptions) error { + args := []string{"preview", "deploy", opts.name} + args = append(args, fmt.Sprintf("--scope=%s", opts.scope)) + args = append(args, fmt.Sprintf("--branch=%s", opts.branch)) + args = append(args, fmt.Sprintf("--repository=%s", opts.ci.Repository())) + args = append(args, fmt.Sprintf("--sourceUrl=%s", opts.ci.SourceURL())) + + if opts.timeout > 0 { + args = append(args, fmt.Sprintf("--timeout=%s", opts.timeout.String())) + } + + if opts.file != "" { + args = append(args, fmt.Sprintf("--file=%s", opts.file)) + } + + if logLevel := getLogLevel(opts.logLevel); logLevel != "" { + args = append(args, fmt.Sprintf("--log-level=%s", logLevel)) + } + + for _, variable := range opts.variables { + args = append(args, fmt.Sprintf("--var=%s", variable)) + } + + args = append(args, "--wait") + + cmd := exec.Command("echo", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "OKTETO_DISABLE_SPINNER=1") + + log.Printf("running: okteto %s", strings.Join(args, " ")) + + return cmd.Run() +} diff --git a/message.go b/message.go index 20847cc..c23a505 100755 --- a/message.go +++ b/message.go @@ -1,86 +1,169 @@ package main import ( + "bytes" + _ "embed" "encoding/json" "fmt" - "io/ioutil" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "io" + "net/url" "os" "os/exec" "path/filepath" + "strings" + "text/template" ) +//go:embed default-message.md.gotmpl +var defaultCommentTemplate string + type contexts struct { Current string `json:"current-context"` Contexts map[string]context `json:"contexts"` } type context struct { - Name string `json:"name"` + Name *url.URL `json:"name"` +} + +func (p *context) UnmarshalJSON(data []byte) error { + type Context context + + tmp := struct { + Name string `json:"name"` + *Context + }{ + Context: (*Context)(p), + } + + err := json.Unmarshal(data, &tmp) + if err != nil { + return err + } + + p.Name, err = url.Parse(tmp.Name) + if err != nil { + return err + } + + return nil } -//Endpoint represents an Okteto statefulset +// Endpoint represents an Okteto statefulset type Endpoint struct { - URL string `json:"url"` - Divert bool `json:"divert"` - Private bool `json:"private"` + URL *url.URL `json:"url"` + Divert bool `json:"divert"` + Private bool `json:"private"` } -func main() { - previewName := os.Args[1] - previewCommandExitCode := os.Args[2] +func (p *Endpoint) UnmarshalJSON(data []byte) error { + type endpoint Endpoint - oktetoURL, err := getOktetoURL() + tmp := struct { + URL string `json:"url"` + *endpoint + }{ + endpoint: (*endpoint)(p), + } + + err := json.Unmarshal(data, &tmp) + if err != nil { + return err + } + + p.URL, err = url.Parse(tmp.URL) if err != nil { - return + return err } - previewURL := fmt.Sprintf("%s/previews/%s", oktetoURL, previewName) + return nil +} + +func generateMessage(previewName string, previewSucceeded bool, commentTemplate string) (string, error) { + oktetoURL, err := getOktetoURL() + if err != nil { + return "", err + } - var firstLine string - if previewCommandExitCode == "0" { - firstLine = fmt.Sprintf("Your preview environment [%s](%s) has been deployed.", previewName, previewURL) - } else { - firstLine = fmt.Sprintf("Your preview environment [%s](%s) has been deployed with errors.", previewName, previewURL) + commentTemplate, err = getCommentTemplate(commentTemplate) + if err != nil { + return "", err } - fmt.Println(firstLine) endpoints, err := getEndpoints(previewName) if err != nil { - return - } - if len(endpoints) == 1 { - fmt.Printf("\n Preview environment endpoint is available [here](%s)", endpoints[0]) - } else if len(endpoints) > 1 { - endpoints = translateEndpoints(endpoints) - fmt.Printf("\n Preview environment endpoints are available at:") - for _, endpoint := range endpoints { - fmt.Printf("\n * %s", endpoint) + return "", err + } + + previewURLSuffix := fmt.Sprintf("%s.%s", previewName, oktetoURL.Host) + templateVars := map[string]interface{}{ + "OktetoURL": oktetoURL.String(), + "PreviewURL": fmt.Sprintf("%s/#/previews/%s", oktetoURL, previewName), + "PreviewName": previewName, + "PreviewURLSuffix": previewURLSuffix, + "PreviewSuccess": previewSucceeded, + "Endpoints": endpoints, + "EndpointsMap": getEndpointsMap(previewURLSuffix, endpoints), + } + + return parseTemplate(commentTemplate, templateVars) +} + +func getCommentTemplate(commentTemplate string) (string, error) { + if commentTemplate == "" { + commentTemplate = defaultCommentTemplate + } + + if commentTemplate[0:1] == "@" { + file, err := os.Open(commentTemplate[1:]) + if err != nil { + return "", err } + + fileContents, err := io.ReadAll(file) + if err != nil { + return "", err + } + + return string(fileContents), nil } + return commentTemplate, nil } -func getOktetoURL() (string, error) { +func getEndpointsMap(previewURLSuffix string, endpoints []*url.URL) map[string]string { + endpointsMap := make(map[string]string, len(endpoints)) + for _, endpoint := range endpoints { + name := strings.TrimSuffix(endpoint.Host, "-"+previewURLSuffix) + endpointsMap[name] = endpoint.String() + } + + return endpointsMap +} + +func getOktetoURL() (*url.URL, error) { contextsPath := filepath.Join(os.Getenv("HOME"), ".okteto", "context", "config.json") - b, err := ioutil.ReadFile(contextsPath) + b, err := os.ReadFile(contextsPath) if err != nil { - return "", err + return nil, err } contexts := &contexts{} if err := json.Unmarshal(b, contexts); err != nil { - return "", err + return nil, err } if val, ok := contexts.Contexts[contexts.Current]; ok { return val.Name, nil } - return "", fmt.Errorf("context %s is missing", contexts.Current) + return nil, fmt.Errorf("context %s is missing", contexts.Current) } -func getEndpoints(name string) ([]string, error) { - cmd := exec.Command("okteto", "preview", "endpoints", name, "-o", "json") +func getEndpoints(name string) ([]*url.URL, error) { + cmd := exec.Command("/home/hsadiq/.zinit/plugins/okteto---okteto/okteto", "preview", "endpoints", name, "-o", "json") cmd.Env = os.Environ() o, err := cmd.CombinedOutput() if err != nil { @@ -92,17 +175,30 @@ func getEndpoints(name string) ([]string, error) { if err != nil { return nil, err } - endpointURLs := make([]string, 0) + endpointURLs := make([]*url.URL, 0, len(endpoints)) for _, e := range endpoints { endpointURLs = append(endpointURLs, e.URL) } return endpointURLs, nil } -func translateEndpoints(endpoints []string) []string { - result := make([]string, 0) - for _, endpoint := range endpoints { - result = append(result, fmt.Sprintf("[%s](%s)", endpoint, endpoint)) +func parseTemplate(templateText string, vars map[string]interface{}) (string, error) { + var output bytes.Buffer + + tmpl, err := template.New("template").Funcs(map[string]interface{}{ + "Contains": strings.Contains, + "HasPrefix": strings.HasPrefix, + "HasSuffix": strings.HasSuffix, + "Title": cases.Title(language.English).String, + "Trim": strings.TrimSpace, + }).Parse(templateText) + if err != nil { + return "", err + } + err = tmpl.Execute(&output, vars) + if err != nil { + return "", err } - return result + + return output.String(), nil } diff --git a/notify-pr.sh b/notify-pr.sh deleted file mode 100644 index 668bf7d..0000000 --- a/notify-pr.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env ruby -require "octokit" -require "json" - -if ENV["GITHUB_EVENT_NAME"] != "pull_request" && ENV["GITHUB_EVENT_NAME"] != "repository_dispatch" - puts "This action only supports either pull_request or repository_dispatch events." - exit(1) -end - -if !message = ARGV[1] - puts "Missing GITHUB_TOKEN" - exit(1) -end - -message = ARGV[0] -preview_name = ARGV[2] -repo = ENV["GITHUB_REPOSITORY"] - -json = File.read(ENV.fetch("GITHUB_EVENT_PATH")) -event = JSON.parse(json) -if ENV["GITHUB_EVENT_NAME"] == "pull_request" - pr = event["number"] -else - pr = event["client_payload"]["pull_request"]["number"] -end - -github = Octokit::Client.new(:access_token => ENV["GITHUB_TOKEN"]) -comments = github.issue_comments(repo, pr) -comment = comments.find do |c| - c["body"].start_with?("Your preview environment") && - c["body"].include?("[#{preview_name}]") -end - -if comment - puts "Message already exists in the PR. Updating" - github.update_comment(repo, comment["id"], message) - exit(0) -end - -github.add_comment(repo, pr, message) From 6f92d484e9bd743db67b4d73677b645e302c3803 Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Sun, 4 Aug 2024 23:49:05 +0100 Subject: [PATCH 2/9] Reference okteto properly --- github.go | 1 - main.go | 2 +- message.go | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/github.go b/github.go index e09f4aa..704522a 100644 --- a/github.go +++ b/github.go @@ -62,7 +62,6 @@ func newGitHub() (ciInfo, error) { switch os.Getenv("GITHUB_EVENT_NAME") { case "pull_request": - gh.prNumber = payload.Number gh.defaultBranch = os.Getenv("GITHUB_HEAD_REF") case "repository_dispatch": diff --git a/main.go b/main.go index b4dcc8f..3d04dbd 100644 --- a/main.go +++ b/main.go @@ -178,7 +178,7 @@ func deployPreview(opts DeployOptions) error { args = append(args, "--wait") - cmd := exec.Command("echo", args...) + cmd := exec.Command("okteto", args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/message.go b/message.go index c23a505..0e668ba 100755 --- a/message.go +++ b/message.go @@ -163,7 +163,7 @@ func getOktetoURL() (*url.URL, error) { } func getEndpoints(name string) ([]*url.URL, error) { - cmd := exec.Command("/home/hsadiq/.zinit/plugins/okteto---okteto/okteto", "preview", "endpoints", name, "-o", "json") + cmd := exec.Command("okteto", "preview", "endpoints", name, "-o", "json") cmd.Env = os.Environ() o, err := cmd.CombinedOutput() if err != nil { From 57ba551490366046a4fcc74d2145b471b88eb7bf Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Mon, 5 Aug 2024 09:37:30 +0100 Subject: [PATCH 3/9] Use arg for GO111MODULE + fix value --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a6b6bad..1a59da2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM okteto/okteto:latest as okteto FROM golang:1.22 as builder WORKDIR /app -ENV GO111MODULE=ON +ARG GO111MODULE=on COPY . . RUN go build -o /deploy-preview . @@ -13,4 +13,4 @@ COPY --from=okteto /usr/local/bin/okteto /okteto ENV PATH=/ -ENTRYPOINT ["/deploy-preview"] \ No newline at end of file +ENTRYPOINT ["/deploy-preview"] From 7ad1f5ea29024a1936f83fb0103e5c31aa2cef24 Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Tue, 13 Aug 2024 16:35:25 +0100 Subject: [PATCH 4/9] build without CGO --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1a59da2..75376c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM golang:1.22 as builder WORKDIR /app ARG GO111MODULE=on COPY . . -RUN go build -o /deploy-preview . +RUN CGO_ENABLED=0 go build -o /deploy-preview . FROM gcr.io/distroless/static-debian11 From a8b77a4583186f2701652d543ef633aa9d3c1e8c Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Tue, 13 Aug 2024 18:03:45 +0100 Subject: [PATCH 5/9] Use pflag + additional fixes --- action.yml | 2 +- github.go | 36 ++++++++++++++++++++---------------- go.mod | 7 +++++-- go.sum | 2 ++ main.go | 11 +++++++---- 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/action.yml b/action.yml index e41049a..135629e 100644 --- a/action.yml +++ b/action.yml @@ -33,7 +33,7 @@ runs: - ${{ inputs.name }} - --timeout=${{ inputs.timeout }} - --scope=${{ inputs.scope }} - - --variables=${{ inputs.variables }} + - --var=${{ inputs.variables }} - --file=${{ inputs.file }} - --branch=${{ inputs.branch }} - --log-level=${{ inputs.log-level }} diff --git a/github.go b/github.go index 704522a..877eb45 100644 --- a/github.go +++ b/github.go @@ -14,7 +14,11 @@ import ( ) type GitHubEvent struct { - Number int `json:"number"` + Number int `json:"number"` + Repository struct { + HtmlURL string `json:"html_url"` + FullName string `json:"full_name"` + } `json:"repository"` ClientPayload struct { PullRequest struct { Number int `json:"number"` @@ -28,14 +32,14 @@ type GitHubComment struct { } type github struct { - repository string - sourceURL string - prNumber int - defaultBranch string + repositoryURL string + prNumber int + defaultBranch string + repositoryName string } type ciInfo interface { - Repository() string + RepositoryURL() string SourceURL() string DefaultBranch() string Notify(message string) error @@ -64,23 +68,24 @@ func newGitHub() (ciInfo, error) { case "pull_request": gh.prNumber = payload.Number gh.defaultBranch = os.Getenv("GITHUB_HEAD_REF") + gh.repositoryURL = payload.Repository.HtmlURL + gh.repositoryName = payload.Repository.FullName case "repository_dispatch": gh.prNumber = payload.ClientPayload.PullRequest.Number gh.defaultBranch = strings.TrimPrefix(os.Getenv("GITHUB_REF"), "refs/heads/") + gh.repositoryURL = payload.Repository.HtmlURL + gh.repositoryName = payload.Repository.FullName } - gh.repository = fmt.Sprintf("%s/%s", os.Getenv("GITHUB_SERVER_URL"), os.Getenv("GITHUB_REPOSITORY")) - gh.sourceURL = fmt.Sprintf("%s/%s", gh.repository, gh.prNumber) - return gh, nil } -func (gh *github) Repository() string { - return gh.repository +func (gh *github) RepositoryURL() string { + return gh.repositoryURL } func (gh *github) SourceURL() string { - return gh.sourceURL + return fmt.Sprintf("%s/pulls/%d", gh.repositoryURL, gh.prNumber) } func (gh *github) DefaultBranch() string { @@ -93,14 +98,13 @@ func (gh *github) PRNumber() int { func (gh *github) Notify(message string) error { githubToken := os.Getenv("GITHUB_TOKEN") - githubRepository := os.Getenv("GITHUB_REPOSITORY") if githubToken == "" { log.Println("failed to set message as no GITHUB_TOKEN found") return errors.New("missing GITHUB_TOKEN") } - resp, err := gh.callGitHub(githubToken, "GET", githubRepository, nil, "issues", fmt.Sprintf("%d", gh.prNumber), "comments") + resp, err := gh.callGitHub(githubToken, "GET", gh.repositoryName, nil, "issues", fmt.Sprintf("%d", gh.prNumber), "comments") if err != nil { return fmt.Errorf("failed to retrieve PR comments: %s", err) } @@ -124,7 +128,7 @@ func (gh *github) Notify(message string) error { return err } if comment == nil { - _, err = gh.callGitHub(githubToken, "POST", githubRepository, msgBodyBuf, "issues", fmt.Sprintf("%d", gh.prNumber), "comments") + _, err = gh.callGitHub(githubToken, "POST", gh.repositoryName, msgBodyBuf, "issues", fmt.Sprintf("%d", gh.prNumber), "comments") if err != nil { return fmt.Errorf("failed to create new comment: %s", err) } @@ -132,7 +136,7 @@ func (gh *github) Notify(message string) error { } fmt.Println("Message already exists in the PR. Updating") - _, err = gh.callGitHub(githubToken, "PATCH", githubRepository, msgBodyBuf, "issues", "comments", fmt.Sprintf("%d", comment.ID)) + _, err = gh.callGitHub(githubToken, "PATCH", gh.repositoryName, msgBodyBuf, "issues", "comments", fmt.Sprintf("%d", comment.ID)) if err != nil { return fmt.Errorf("failed to update comment: %s", err) } diff --git a/go.mod b/go.mod index f224895..cfe1412 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/okteto/deploy-preview go 1.22 -require golang.org/x/text v0.16.0 +require ( + github.com/spf13/pflag v1.0.5 + golang.org/x/text v0.16.0 +) -require github.com/hoshsadiq/godotenv v1.0.0 // indirect +require github.com/hoshsadiq/godotenv v1.0.0 diff --git a/go.sum b/go.sum index f76a738..750dace 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/hoshsadiq/godotenv v1.0.0 h1:Hjx9hW+vqSOm5LN/UwMktxAekKvuXIg+BzIUmGQ7wXQ= github.com/hoshsadiq/godotenv v1.0.0/go.mod h1:BZLGi0xKHU92H+AKkNoy/BsSFrZUUN3C8SdvyF3gt+c= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= diff --git a/main.go b/main.go index 3d04dbd..3e54aa9 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,9 @@ package main import ( "errors" - "flag" "fmt" "github.com/hoshsadiq/godotenv" + flag "github.com/spf13/pflag" "log" "os" "os/exec" @@ -33,6 +33,10 @@ func (i *sliceString) Set(value string) error { return nil } +func (i *sliceString) Type() string { + return "var" +} + type DeployOptions struct { branch string file string @@ -111,8 +115,7 @@ func main() { } func notify(ci ciInfo, message string) error { - switch { - case os.Getenv("GITHUB_ACTIONS") == "true": + if ci != nil { return ci.Notify(message) } @@ -157,7 +160,7 @@ func deployPreview(opts DeployOptions) error { args := []string{"preview", "deploy", opts.name} args = append(args, fmt.Sprintf("--scope=%s", opts.scope)) args = append(args, fmt.Sprintf("--branch=%s", opts.branch)) - args = append(args, fmt.Sprintf("--repository=%s", opts.ci.Repository())) + args = append(args, fmt.Sprintf("--repository=%s", opts.ci.RepositoryURL())) args = append(args, fmt.Sprintf("--sourceUrl=%s", opts.ci.SourceURL())) if opts.timeout > 0 { From fcb8a1187ca12175c44b253404fef0fcf0bbd500 Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Tue, 13 Aug 2024 18:09:13 +0100 Subject: [PATCH 6/9] get head ref from event --- github.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/github.go b/github.go index 877eb45..1817b1a 100644 --- a/github.go +++ b/github.go @@ -14,7 +14,12 @@ import ( ) type GitHubEvent struct { - Number int `json:"number"` + Number int `json:"number"` + PullRequest struct { + Head struct { + Ref string `json:"ref"` + } `json:"head"` + } `json:"pull_request"` Repository struct { HtmlURL string `json:"html_url"` FullName string `json:"full_name"` @@ -67,7 +72,7 @@ func newGitHub() (ciInfo, error) { switch os.Getenv("GITHUB_EVENT_NAME") { case "pull_request": gh.prNumber = payload.Number - gh.defaultBranch = os.Getenv("GITHUB_HEAD_REF") + gh.defaultBranch = payload.PullRequest.Head.Ref gh.repositoryURL = payload.Repository.HtmlURL gh.repositoryName = payload.Repository.FullName case "repository_dispatch": From 53ec27405d4dd24feabe9564f8db80e56afc5bd2 Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Tue, 13 Aug 2024 18:18:44 +0100 Subject: [PATCH 7/9] fix branch detection --- github.go | 8 ++++---- main.go | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/github.go b/github.go index 1817b1a..944d974 100644 --- a/github.go +++ b/github.go @@ -14,9 +14,9 @@ import ( ) type GitHubEvent struct { - Number int `json:"number"` PullRequest struct { - Head struct { + Number int `json:"number"` + Head struct { Ref string `json:"ref"` } `json:"head"` } `json:"pull_request"` @@ -70,8 +70,8 @@ func newGitHub() (ciInfo, error) { gh := &github{} switch os.Getenv("GITHUB_EVENT_NAME") { - case "pull_request": - gh.prNumber = payload.Number + case "pull_request", "pull_request_target": + gh.prNumber = payload.PullRequest.Number gh.defaultBranch = payload.PullRequest.Head.Ref gh.repositoryURL = payload.Repository.HtmlURL gh.repositoryName = payload.Repository.FullName diff --git a/main.go b/main.go index 3e54aa9..18a6a20 100644 --- a/main.go +++ b/main.go @@ -147,6 +147,7 @@ func validateInput(flagSet *flag.FlagSet, opts *DeployOptions) error { return errors.New("preview environment name is required") } + opts.branch = opts.ci.DefaultBranch() if opts.branch == "" { // this essentially means that retrieveDefaultBranch was unable to find a value return errors.New("failed to detect branch") From 2753b826b3bcea1855f69f092d43d061a1b5f6e2 Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Tue, 13 Aug 2024 18:56:57 +0100 Subject: [PATCH 8/9] actually pass through the comment --- action.yml | 1 + main.go | 17 ++++++++--------- message.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/action.yml b/action.yml index 135629e..35685e0 100644 --- a/action.yml +++ b/action.yml @@ -37,6 +37,7 @@ runs: - --file=${{ inputs.file }} - --branch=${{ inputs.branch }} - --log-level=${{ inputs.log-level }} + - --comment=${{ inputs.comment }} branding: color: 'green' icon: 'grid' diff --git a/main.go b/main.go index 18a6a20..45d763f 100644 --- a/main.go +++ b/main.go @@ -103,10 +103,7 @@ func main() { } var success = err == nil - message, err := generateMessage(opts.name, success, opts.comment) - exitIfErr(err) - - err = notify(ci, message) + err = notify(ci, success, opts.name, opts.comment) exitIfErr(err) if !success { @@ -114,13 +111,15 @@ func main() { } } -func notify(ci ciInfo, message string) error { - if ci != nil { - return ci.Notify(message) +func notify(ci ciInfo, success bool, name string, comment string) error { + if ci == nil { + log.Printf("Not notifying anything, CI not supported") } - log.Printf("Not notifying anything, CI not supported") - return nil + message, err := generateMessage(success, name, comment) + exitIfErr(err) + + return ci.Notify(message) } func getCIInfo() (ciInfo, error) { diff --git a/message.go b/message.go index 0e668ba..f333a50 100755 --- a/message.go +++ b/message.go @@ -81,7 +81,7 @@ func (p *Endpoint) UnmarshalJSON(data []byte) error { return nil } -func generateMessage(previewName string, previewSucceeded bool, commentTemplate string) (string, error) { +func generateMessage(previewSucceeded bool, previewName string, commentTemplate string) (string, error) { oktetoURL, err := getOktetoURL() if err != nil { return "", err From 496e47ce0e5ab3ce9fd7c3aefb72564b2ce07b2b Mon Sep 17 00:00:00 2001 From: Hosh Sadiq Date: Thu, 22 Aug 2024 09:51:03 +0100 Subject: [PATCH 9/9] Fix pull URL --- github.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github.go b/github.go index 944d974..048fe47 100644 --- a/github.go +++ b/github.go @@ -90,7 +90,7 @@ func (gh *github) RepositoryURL() string { } func (gh *github) SourceURL() string { - return fmt.Sprintf("%s/pulls/%d", gh.repositoryURL, gh.prNumber) + return fmt.Sprintf("%s/pull/%d", gh.repositoryURL, gh.prNumber) } func (gh *github) DefaultBranch() string {