Skip to content

Commit c7f65c1

Browse files
committed
ci: Replace DataDog CI with custom upload script
This will reduce CI time by ~6 minutes across all of our runners. It's a bit janky, but I believe worth the slight maintainance burden.
1 parent 1796dc6 commit c7f65c1

File tree

4 files changed

+166
-28
lines changed

4 files changed

+166
-28
lines changed

.github/workflows/coder.yaml

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -165,39 +165,16 @@ jobs:
165165
-covermode=atomic -coverprofile="gotests.coverage" -timeout=3m
166166
-count=1 -race -parallel=2
167167

168-
- name: Setup Node for DataDog CLI
169-
uses: actions/setup-node@v2
170-
if: always() && github.actor != 'dependabot[bot]'
171-
with:
172-
node-version: "14"
173-
174-
- name: Cache DataDog CLI
175-
if: always() && github.actor != 'dependabot[bot]'
176-
uses: actions/cache@v2
177-
with:
178-
path: |
179-
~/.npm
180-
%LocalAppData%\npm-cache
181-
key: datadogci-
182-
restore-keys: datadogci-
183-
184168
- name: Upload DataDog Trace
185169
if: always() && github.actor != 'dependabot[bot]'
186-
# See: https://docs.datadoghq.com/continuous_integration/setup_tests/junit_upload/#collecting-environment-configuration-metadata
187170
env:
188171
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
189-
DD_GIT_REPOSITORY_URL: ${{ github.repositoryUrl }}
190-
DD_GIT_BRANCH: ${{ github.head_ref }}
191-
DD_GIT_COMMIT_SHA: ${{ github.sha }}
192172
DD_GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
193173
DD_GIT_COMMIT_AUTHOR_NAME: ${{ github.event.head_commit.author.name }}
194174
DD_GIT_COMMIT_AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }}
195175
DD_GIT_COMMIT_COMMITTER_NAME: ${{ github.event.head_commit.committer.name }}
196176
DD_GIT_COMMIT_COMMITTER_EMAIL: ${{ github.event.head_commit.committer.email }}
197-
DD_TAGS: ${{ format('os.platform:{0},os.architecture:{1}', runner.os, runner.arch) }}
198-
run: |
199-
npm install -g @datadog/datadog-ci
200-
datadog-ci junit upload --service coder gotests.xml
177+
run: go run scripts/datadog-cireport/main.go gotests.xml
201178

202179
- uses: codecov/codecov-action@v2
203180
if: github.actor != 'dependabot[bot]'

codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ ignore:
3131
- peerbroker/proto
3232
- provisionerd/proto
3333
- provisionersdk/proto
34+
- scripts/datadog-cireport

rules.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,17 @@ func xerrors(m dsl.Matcher) {
1010
m.Import("errors")
1111
m.Import("fmt")
1212
m.Import("golang.org/x/xerrors")
13-
msg := "Use xerrors to provide additional stacktrace information!"
1413

1514
m.Match("fmt.Errorf($*args)").
1615
Suggest("xerrors.New($args)").
17-
Report(msg)
16+
Report("Use xerrors to provide additional stacktrace information!")
1817

1918
m.Match("fmt.Errorf($*args)").
2019
Suggest("xerrors.Errorf($args)").
21-
Report(msg)
20+
Report("Use xerrors to provide additional stacktrace information!")
2221

2322
m.Match("errors.New($msg)").
2423
Where(m["msg"].Type.Is("string")).
2524
Suggest("xerrors.New($msg)").
26-
Report(msg)
25+
Report("Use xerrors to provide additional stacktrace information!")
2726
}

scripts/datadog-cireport/main.go

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

0 commit comments

Comments
 (0)