|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "compress/gzip" |
| 6 | + "context" |
| 7 | + "encoding/json" |
| 8 | + "fmt" |
| 9 | + "log" |
| 10 | + "mime/multipart" |
| 11 | + "net/http" |
| 12 | + "net/textproto" |
| 13 | + "os" |
| 14 | + "os/exec" |
| 15 | + "path/filepath" |
| 16 | + "regexp" |
| 17 | + "runtime" |
| 18 | + "strings" |
| 19 | +) |
| 20 | + |
| 21 | +// The DataDog "cireport" API is not publicly documented, |
| 22 | +// but implementation is available in their open-source CLI |
| 23 | +// built for CI: https://github.com/DataDog/datadog-ci |
| 24 | +// |
| 25 | +// It's built using node, and took ~3 minutes to install and |
| 26 | +// run on our Windows runner, and ~1 minute on all others. |
| 27 | +// |
| 28 | +// This script models that code as much as possible. |
| 29 | +func main() { |
| 30 | + apiKey := os.Getenv("DATADOG_API_KEY") |
| 31 | + if apiKey == "" { |
| 32 | + log.Fatal("DATADOG_API_KEY must be set!") |
| 33 | + } |
| 34 | + if len(os.Args) <= 1 { |
| 35 | + log.Fatal("You must supply a filename to upload!") |
| 36 | + } |
| 37 | + |
| 38 | + // Code (almost) verbatim translated from: |
| 39 | + // https://github.com/DataDog/datadog-ci/blob/78d0da28e1c1af44333deabf1c9486e2ad66b8af/src/helpers/ci.ts#L194-L229 |
| 40 | + var ( |
| 41 | + githubServerURL = os.Getenv("GITHUB_SERVER_URL") |
| 42 | + githubRepository = os.Getenv("GITHUB_REPOSITORY") |
| 43 | + githubSHA = os.Getenv("GITHUB_SHA") |
| 44 | + githubRunID = os.Getenv("GITHUB_RUN_ID") |
| 45 | + pipelineURL = fmt.Sprintf("%s/%s/actions/runs/%s", githubServerURL, githubRepository, githubRunID) |
| 46 | + jobURL = fmt.Sprintf("%s/%s/commit/%s/checks", githubServerURL, githubRepository, githubSHA) |
| 47 | + ) |
| 48 | + if os.Getenv("GITHUB_RUN_ATTEMPT") != "" { |
| 49 | + pipelineURL += fmt.Sprintf("/attempts/%s", os.Getenv("GITHUB_RUN_ATTEMPT")) |
| 50 | + } |
| 51 | + |
| 52 | + commitMessage, err := exec.Command("git", "show", "-s", "--format=%s").CombinedOutput() |
| 53 | + if err != nil { |
| 54 | + log.Fatalf("Get commit message: %s", err) |
| 55 | + } |
| 56 | + commitData, err := exec.Command("git", "show", "-s", "--format=%an,%ae,%cn,%ce,%cd").CombinedOutput() |
| 57 | + if err != nil { |
| 58 | + log.Fatalf("Get commit data: %s", err) |
| 59 | + } |
| 60 | + commitParts := strings.Split(string(commitData), ",") |
| 61 | + |
| 62 | + tags := map[string]string{ |
| 63 | + "service": "coder", |
| 64 | + "_dd.cireport_version": "2", |
| 65 | + |
| 66 | + // Additional tags found in DataDog docs. See: |
| 67 | + // https://docs.datadoghq.com/continuous_integration/setup_tests/junit_upload/#collecting-environment-configuration-metadata |
| 68 | + "os.platform": runtime.GOOS, |
| 69 | + "os.architecture": runtime.GOARCH, |
| 70 | + |
| 71 | + "ci.job.url": jobURL, |
| 72 | + "ci.pipeline.id": githubRunID, |
| 73 | + "ci.pipeline.name": os.Getenv("GITHUB_WORKFLOW"), |
| 74 | + "ci.pipeline.number": os.Getenv("GITHUB_RUN_NUMBER"), |
| 75 | + "ci.pipeline.url": pipelineURL, |
| 76 | + "ci.provider.name": "github", |
| 77 | + "ci.workspace_path": os.Getenv("GITHUB_WORKSPACE"), |
| 78 | + |
| 79 | + "git.branch": os.Getenv("GITHUB_HEAD_REF"), |
| 80 | + "git.commit.sha": githubSHA, |
| 81 | + "git.repository_url": fmt.Sprintf("%s/%s.git", githubServerURL, githubRepository), |
| 82 | + |
| 83 | + "git.commit.message": strings.TrimSpace(string(commitMessage)), |
| 84 | + "git.commit.author.name": commitParts[0], |
| 85 | + "git.commit.author.email": commitParts[1], |
| 86 | + "git.commit.author.date": commitParts[2], |
| 87 | + "git.commit.committer.name": commitParts[3], |
| 88 | + "git.commit.committer.email": commitParts[4], |
| 89 | + "git.commit.committer.date": commitParts[5], |
| 90 | + } |
| 91 | + |
| 92 | + xmlFilePath := filepath.Clean(os.Args[1]) |
| 93 | + xmlFileData, err := os.ReadFile(xmlFilePath) |
| 94 | + if err != nil { |
| 95 | + log.Fatalf("Read %q: %s", xmlFilePath, err) |
| 96 | + } |
| 97 | + // https://github.com/DataDog/datadog-ci/blob/78d0da28e1c1af44333deabf1c9486e2ad66b8af/src/commands/junit/api.ts#L53 |
| 98 | + var xmlCompressedBuffer bytes.Buffer |
| 99 | + xmlGzipWriter := gzip.NewWriter(&xmlCompressedBuffer) |
| 100 | + _, err = xmlGzipWriter.Write(xmlFileData) |
| 101 | + if err != nil { |
| 102 | + log.Fatalf("Write xml: %s", err) |
| 103 | + } |
| 104 | + err = xmlGzipWriter.Close() |
| 105 | + if err != nil { |
| 106 | + log.Fatalf("Close xml gzip writer: %s", err) |
| 107 | + } |
| 108 | + |
| 109 | + // Represents FormData. See: |
| 110 | + // https://github.com/DataDog/datadog-ci/blob/78d0da28e1c1af44333deabf1c9486e2ad66b8af/src/commands/junit/api.ts#L27 |
| 111 | + var multipartBuffer bytes.Buffer |
| 112 | + multipartWriter := multipart.NewWriter(&multipartBuffer) |
| 113 | + |
| 114 | + // Adds the event data. See: |
| 115 | + // https://github.com/DataDog/datadog-ci/blob/78d0da28e1c1af44333deabf1c9486e2ad66b8af/src/commands/junit/api.ts#L42 |
| 116 | + eventMimeHeader := make(textproto.MIMEHeader) |
| 117 | + eventMimeHeader.Set("Content-Disposition", `form-data; name="event"; filename="event.json"`) |
| 118 | + eventMimeHeader.Set("Content-Type", "application/json") |
| 119 | + eventMultipartWriter, err := multipartWriter.CreatePart(eventMimeHeader) |
| 120 | + if err != nil { |
| 121 | + log.Fatalf("Create event multipart: %s", err) |
| 122 | + } |
| 123 | + eventJSON, err := json.Marshal(tags) |
| 124 | + if err != nil { |
| 125 | + log.Fatalf("Marshal tags: %s", err) |
| 126 | + } |
| 127 | + _, err = eventMultipartWriter.Write(eventJSON) |
| 128 | + if err != nil { |
| 129 | + log.Fatalf("Write event JSON: %s", err) |
| 130 | + } |
| 131 | + |
| 132 | + // This seems really strange, but better to follow the implementation. See: |
| 133 | + // https://github.com/DataDog/datadog-ci/blob/78d0da28e1c1af44333deabf1c9486e2ad66b8af/src/commands/junit/api.ts#L44-L55 |
| 134 | + xmlFilename := fmt.Sprintf("%s-coder-%s-%s-%s", filepath.Base(xmlFilePath), githubSHA, pipelineURL, jobURL) |
| 135 | + xmlFilename = regexp.MustCompile("[^a-z0-9]").ReplaceAllString(xmlFilename, "_") |
| 136 | + |
| 137 | + xmlMimeHeader := make(textproto.MIMEHeader) |
| 138 | + xmlMimeHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="junit_xml_report_file"; filename="%s.xml.gz"`, xmlFilename)) |
| 139 | + xmlMimeHeader.Set("Content-Type", "application/octet-stream") |
| 140 | + inputWriter, err := multipartWriter.CreatePart(xmlMimeHeader) |
| 141 | + if err != nil { |
| 142 | + log.Fatalf("Create xml.gz multipart: %s", err) |
| 143 | + } |
| 144 | + _, err = inputWriter.Write(xmlCompressedBuffer.Bytes()) |
| 145 | + if err != nil { |
| 146 | + log.Fatalf("Write xml.gz: %s", err) |
| 147 | + } |
| 148 | + err = multipartWriter.Close() |
| 149 | + if err != nil { |
| 150 | + log.Fatalf("Close: %s", err) |
| 151 | + } |
| 152 | + |
| 153 | + ctx := context.Background() |
| 154 | + req, err := http.NewRequestWithContext(ctx, "POST", "https://cireport-intake.datadoghq.com/api/v2/cireport", &multipartBuffer) |
| 155 | + if err != nil { |
| 156 | + log.Fatalf("Create request: %s", err) |
| 157 | + } |
| 158 | + req.Header.Set("Content-Type", multipartWriter.FormDataContentType()) |
| 159 | + req.Header.Set("DD-API-KEY", apiKey) |
| 160 | + |
| 161 | + res, err := http.DefaultClient.Do(req) |
| 162 | + if err != nil { |
| 163 | + log.Fatalf("Do request: %s", err) |
| 164 | + } |
| 165 | + defer res.Body.Close() |
| 166 | + var msg json.RawMessage |
| 167 | + err = json.NewDecoder(res.Body).Decode(&msg) |
| 168 | + if err != nil { |
| 169 | + log.Fatalf("Decode response: %s", err) |
| 170 | + } |
| 171 | + msg, err = json.MarshalIndent(msg, "", "\t") |
| 172 | + if err != nil { |
| 173 | + log.Fatalf("Pretty print: %s", err) |
| 174 | + } |
| 175 | + _, _ = fmt.Printf("Status code: %d\nResponse: %s\n", res.StatusCode, msg) |
| 176 | +} |
0 commit comments