diff --git a/bitrise/bitrise.go b/bitrise/bitrise.go index 17db868..36183d0 100644 --- a/bitrise/bitrise.go +++ b/bitrise/bitrise.go @@ -4,8 +4,10 @@ import ( "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" + "os" "strings" "time" @@ -46,6 +48,27 @@ type StartResponse struct { TriggeredWorkflow string `json:"triggered_workflow"` } +// BuildArtifactsResponse ... +type BuildArtifactsResponse struct { + ArtifactSlugs []BuildArtifactSlug `json:"data"` +} + +// BuildArtifactSlug ... +type BuildArtifactSlug struct { + ArtifactSlug string `json:"slug"` +} + +// BuildArtifactResponse ... +type BuildArtifactResponse struct { + Artifact BuildArtifact `json:"data"` +} + +// BuildArtifact ... +type BuildArtifact struct { + DownloadURL string `json:"expiring_download_url"` + Title string `json:"title"` +} + // Environment ... type Environment struct { MappedTo string `json:"mapped_to"` @@ -220,6 +243,118 @@ func (app App) StartBuild(workflow string, buildParams json.RawMessage, buildNum return response, nil } +// GetBuildArtifacts ... +func (build Build) GetBuildArtifacts(app App) (BuildArtifactsResponse, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v0.1/apps/%s/builds/%s/artifacts", app.BaseURL, app.Slug, build.Slug), nil) + if err != nil { + return BuildArtifactsResponse{}, nil + } + req.Header.Add("Authorization", "token "+app.AccessToken) + + retryReq, err := retryablehttp.FromRequest(req) + if err != nil { + return BuildArtifactsResponse{}, fmt.Errorf("failed to create retryable request: %s", err) + } + + retryClient := NewRetryableClient(app.IsDebugRetryTimings) + + resp, err := retryClient.Do(retryReq) + if err != nil { + return BuildArtifactsResponse{}, nil + } + + defer func() { + if cerr := resp.Body.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return BuildArtifactsResponse{}, nil + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return BuildArtifactsResponse{}, fmt.Errorf("failed to get response, statuscode: %d, body: %s", resp.StatusCode, respBody) + } + + var response BuildArtifactsResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return BuildArtifactsResponse{}, fmt.Errorf("failed to decode response, body: %s, error: %s", respBody, err) + } + return response, nil +} + +// GetBuildArtifact ... +func (build Build) GetBuildArtifact(app App, artifactSlug string) (BuildArtifactResponse, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v0.1/apps/%s/builds/%s/artifacts/%s", app.BaseURL, app.Slug, build.Slug, artifactSlug), nil) + if err != nil { + return BuildArtifactResponse{}, nil + } + req.Header.Add("Authorization", "token "+app.AccessToken) + + retryReq, err := retryablehttp.FromRequest(req) + if err != nil { + return BuildArtifactResponse{}, fmt.Errorf("failed to create retryable request: %s", err) + } + + retryClient := NewRetryableClient(app.IsDebugRetryTimings) + + resp, err := retryClient.Do(retryReq) + if err != nil { + return BuildArtifactResponse{}, nil + } + + defer func() { + if cerr := resp.Body.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return BuildArtifactResponse{}, nil + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return BuildArtifactResponse{}, fmt.Errorf("failed to get response, statuscode: %d, body: %s", resp.StatusCode, respBody) + } + + var response BuildArtifactResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return BuildArtifactResponse{}, fmt.Errorf("failed to decode response, body: %s, error: %s", respBody, err) + } + return response, nil +} + +// DownloadArtifact ... +func (artifact BuildArtifact) DownloadArtifact(filepath string) error { + resp, err := http.Get(artifact.DownloadURL) + if err != nil { + return err + } + + defer func() { + if err := resp.Body.Close(); err != nil { + fmt.Println("Failed to close body, error:", err) + } + }() + + out, err := os.Create(filepath) + if err != nil { + return err + } + + defer func() { + if err := out.Close(); err != nil { + fmt.Println("Failed to close output stream, error:", err) + } + }() + + _, err = io.Copy(out, resp.Body) + return err +} + // WaitForBuilds ... func (app App) WaitForBuilds(buildSlugs []string, statusChangeCallback func(build Build)) error { failed := false diff --git a/main.go b/main.go index ea753ba..044e07d 100644 --- a/main.go +++ b/main.go @@ -15,14 +15,15 @@ const envBuildSlugs = "ROUTER_STARTED_BUILD_SLUGS" // Config ... type Config struct { - AppSlug string `env:"BITRISE_APP_SLUG,required"` - BuildSlug string `env:"BITRISE_BUILD_SLUG,required"` - BuildNumber string `env:"BITRISE_BUILD_NUMBER,required"` - AccessToken stepconf.Secret `env:"access_token,required"` - WaitForBuilds string `env:"wait_for_builds"` - Workflows string `env:"workflows,required"` - Environments string `env:"environment_key_list"` - IsVerboseLog bool `env:"verbose,required"` + AppSlug string `env:"BITRISE_APP_SLUG,required"` + BuildSlug string `env:"BITRISE_BUILD_SLUG,required"` + BuildNumber string `env:"BITRISE_BUILD_NUMBER,required"` + AccessToken stepconf.Secret `env:"access_token,required"` + WaitForBuilds string `env:"wait_for_builds"` + BuildArtifactsSavePath string `env:"build_artifacts_save_path"` + Workflows string `env:"workflows,required"` + Environments string `env:"environment_key_list"` + IsVerboseLog bool `env:"verbose,required"` } func failf(s string, a ...interface{}) { @@ -86,6 +87,26 @@ func main() { case 4: log.Infof("- %s cancelled", build.TriggeredWorkflow) } + if build.Status != 0 { + if strings.TrimSpace(cfg.BuildArtifactsSavePath) != "" { + artifactsResponse, err := build.GetBuildArtifacts(app) + if err != nil { + log.Warnf("failed to get build artifacts, error: %s", err) + } + for _, artifactSlug := range artifactsResponse.ArtifactSlugs { + artifactObj, err := build.GetBuildArtifact(app, artifactSlug.ArtifactSlug) + if err != nil { + log.Warnf("failed to get build artifact, error: %s", err) + } + + downloadErr := artifactObj.Artifact.DownloadArtifact(strings.TrimSpace(cfg.BuildArtifactsSavePath) + artifactObj.Artifact.Title) + if downloadErr != nil { + log.Warnf("failed to download artifact, error: %s", downloadErr) + } + log.Donef("Downloaded: " + artifactObj.Artifact.Title + " to path " + strings.TrimSpace(cfg.BuildArtifactsSavePath)) + } + } + } }); err != nil { failf("An error occoured: %s", err) } diff --git a/step.yml b/step.yml index 0d6d20f..d2de5ac 100644 --- a/step.yml +++ b/step.yml @@ -57,6 +57,14 @@ inputs: value_options: - "false" - "true" + - build_artifacts_save_path: + opts: + title: Path to save the Artifacts in (leave empty to not save artifacts) + summary: This is where the artifacts will be saved to if wait_for_builds == true + description: | + This is where the artifacts will be saved to so you can access them in the workflow + is_required: false + is_sensitive: false - verbose: "no" opts: title: Enable verbose log?