Skip to content

Commit 6061196

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 6061196

File tree

4 files changed

+181
-33
lines changed

4 files changed

+181
-33
lines changed

.github/workflows/coder.yaml

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -165,39 +165,11 @@ 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 }}
192-
DD_GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
193-
DD_GIT_COMMIT_AUTHOR_NAME: ${{ github.event.head_commit.author.name }}
194-
DD_GIT_COMMIT_AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }}
195-
DD_GIT_COMMIT_COMMITTER_NAME: ${{ github.event.head_commit.committer.name }}
196-
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
172+
run: go run scripts/datadog-cireport/main.go gotests.xml
201173

202174
- uses: codecov/codecov-action@v2
203175
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: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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

Comments
 (0)