Skip to content

Commit

Permalink
[CI-2145] Add test report upload support (#188)
Browse files Browse the repository at this point in the history
* Share upload file logic

* Add test report upload

* Add comments

* Fix naming

* Update common.go

* Handle all errors

* Add file close error handling

* Handle http type conversion error

* Rename test reports
  • Loading branch information
tothszabi authored Oct 26, 2023
1 parent 357d163 commit 859cc3d
Show file tree
Hide file tree
Showing 17 changed files with 1,006 additions and 8 deletions.
27 changes: 27 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import (
"github.com/bitrise-io/go-utils/v2/errorutil"
"github.com/bitrise-io/go-utils/v2/exitcode"
"github.com/bitrise-io/go-utils/v2/fileutil"
loggerV2 "github.com/bitrise-io/go-utils/v2/log"
pathutil2 "github.com/bitrise-io/go-utils/v2/pathutil"
"github.com/bitrise-io/go-utils/ziputil"
"github.com/bitrise-steplib/steps-deploy-to-bitrise-io/deployment"
"github.com/bitrise-steplib/steps-deploy-to-bitrise-io/fileredactor"
"github.com/bitrise-steplib/steps-deploy-to-bitrise-io/report"
"github.com/bitrise-steplib/steps-deploy-to-bitrise-io/test"
"github.com/bitrise-steplib/steps-deploy-to-bitrise-io/uploaders"
)
Expand Down Expand Up @@ -53,6 +55,7 @@ type Config struct {
DebugMode bool `env:"debug_mode,opt[true,false]"`
BundletoolVersion string `env:"bundletool_version,required"`
UploadConcurrency string `env:"BITRISE_DEPLOY_UPLOAD_CONCURRENCY"`
HTMLReportDir string `env:"BITRISE_HTML_REPORT_DIR"`
}

// PublicInstallPage ...
Expand Down Expand Up @@ -181,6 +184,30 @@ func main() {
if config.AddonAPIToken != "" {
deployTestResults(config)
}

if config.HTMLReportDir != "" {
deployHTMLReports(config)
}
}

func deployHTMLReports(config Config) {
fmt.Println()
log.Infof("Deploying html reports...")

logger := loggerV2.NewLogger()
logger.EnableDebugLog(config.DebugMode)
concurrency := determineConcurrency(Config{})
uploader := report.NewHTMLReportUploader(config.HTMLReportDir, config.BuildURL, config.APIToken, concurrency, logger)

uploadErrors := uploader.DeployReports()
if 0 < len(uploadErrors) {
log.Errorf("Failed to upload html reports:")
for _, err := range uploadErrors {
log.Errorf("- %w", err)
}
} else {
log.Donef("Successful html report upload")
}
}

func loadSecrets() []string {
Expand Down
169 changes: 169 additions & 0 deletions report/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package api

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"

"github.com/bitrise-io/go-utils/retry"
"github.com/bitrise-io/go-utils/v2/log"
"github.com/bitrise-steplib/steps-deploy-to-bitrise-io/uploaders"
)

// ClientAPI ...
type ClientAPI interface {
CreateReport(params CreateReportParameters) (CreateReportResponse, error)
UploadAsset(url, path, contentType string) error
FinishReport(identifier string, allAssetsUploaded bool) error
}

// HTTPClient ...
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}

// TestReportClient ...
type TestReportClient struct {
logger log.Logger
httpClient HTTPClient
buildURL string
authToken string
}

// NewBitriseClient ...
func NewBitriseClient(buildURL, authToken string, logger log.Logger) *TestReportClient {
httpClient := retry.NewHTTPClient().StandardClient()

return &TestReportClient{
logger: logger,
httpClient: httpClient,
buildURL: buildURL,
authToken: authToken,
}
}

// CreateReport ...
func (t *TestReportClient) CreateReport(params CreateReportParameters) (CreateReportResponse, error) {
url := fmt.Sprintf("%s/html_reports.json", t.buildURL)

body, err := json.Marshal(params)
if err != nil {
return CreateReportResponse{}, err
}

req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return CreateReportResponse{}, err
}

resp, err := t.perform(req)
if err != nil {
return CreateReportResponse{}, err
}

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return CreateReportResponse{}, err
}

var response CreateReportResponse
if err := json.Unmarshal(respBody, &response); err != nil {
return CreateReportResponse{}, err
}

return response, nil
}

// UploadAsset ...
func (t *TestReportClient) UploadAsset(url, path, contentType string) error {
return uploaders.UploadArtifact(url, path, contentType)
}

// FinishReport ...
func (t *TestReportClient) FinishReport(identifier string, allAssetsUploaded bool) error {
url := fmt.Sprintf("%s/html_reports/%s.json", t.buildURL, identifier)

type parameters struct {
Uploaded bool `json:"is_uploaded"`
}
params := parameters{Uploaded: allAssetsUploaded}

body, err := json.Marshal(params)
if err != nil {
return err
}

req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(body))
if err != nil {
return err
}

_, err = t.perform(req)
if err != nil {
return err
}

return nil
}

func (t *TestReportClient) perform(request *http.Request) (*http.Response, error) {
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
// Header.Set canonizes the keys, so we need to set the token this way.
request.Header["BUILD_API_TOKEN"] = []string{t.authToken}

dump, err := httputil.DumpRequest(request, false)
if err != nil {
t.logger.Warnf("Request dump failed: %w", err)
} else {
t.logger.Debugf("Request dump: %s", string(dump))
}

resp, err := t.httpClient.Do(request)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
t.logger.Warnf("Failed to close response body: %w", err)
}
}()

dump, err = httputil.DumpResponse(resp, true)
if err != nil {
t.logger.Warnf("Response dump failed: %s", err)
} else {
t.logger.Debugf("Response dump: %s", string(dump))
}

if resp.StatusCode >= 300 || resp.StatusCode < 200 {
message, err := parseErrorMessage(resp)
if err != nil {
t.logger.Warnf("Failed to parse error message from the response: %s", err)
}

return nil, fmt.Errorf("request to %s failed: status code should be 2xx (%d): %s", resp.Request.URL, resp.StatusCode, message)
}

return resp, nil
}

func parseErrorMessage(resp *http.Response) (string, error) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}

type errorResponse struct {
Message string `json:"error_msg"`
}

var response errorResponse
if err := json.Unmarshal(body, &response); err != nil {
return "", err
}

return response.Message, nil
}
Loading

0 comments on commit 859cc3d

Please sign in to comment.