@datadog/datadog-ci 1.1.1 → 1.3.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/dist/commands/dsyms/__tests__/upload.test.js +219 -16
  3. package/dist/commands/dsyms/__tests__/utils.test.js +86 -46
  4. package/dist/commands/dsyms/interfaces.d.ts +14 -5
  5. package/dist/commands/dsyms/interfaces.js +15 -28
  6. package/dist/commands/dsyms/renderer.d.ts +7 -5
  7. package/dist/commands/dsyms/renderer.js +18 -6
  8. package/dist/commands/dsyms/upload.d.ts +31 -0
  9. package/dist/commands/dsyms/upload.js +127 -13
  10. package/dist/commands/dsyms/utils.d.ts +12 -6
  11. package/dist/commands/dsyms/utils.js +23 -51
  12. package/dist/commands/junit/__tests__/upload.test.js +15 -1
  13. package/dist/commands/junit/api.js +3 -0
  14. package/dist/commands/junit/interfaces.d.ts +1 -0
  15. package/dist/commands/junit/upload.d.ts +1 -0
  16. package/dist/commands/junit/upload.js +12 -0
  17. package/dist/commands/lambda/__tests__/functions/commons.test.js +22 -10
  18. package/dist/commands/lambda/functions/commons.js +4 -1
  19. package/dist/commands/synthetics/__tests__/run-test.test.js +30 -24
  20. package/dist/commands/synthetics/__tests__/utils.test.js +19 -10
  21. package/dist/commands/synthetics/api.d.ts +3 -12
  22. package/dist/commands/synthetics/api.js +4 -1
  23. package/dist/commands/synthetics/command.js +4 -0
  24. package/dist/commands/synthetics/errors.d.ts +4 -4
  25. package/dist/commands/synthetics/errors.js +5 -4
  26. package/dist/commands/synthetics/interfaces.d.ts +1 -4
  27. package/dist/commands/synthetics/reporters/default.js +2 -2
  28. package/dist/commands/synthetics/run-test.d.ts +1 -11
  29. package/dist/commands/synthetics/run-test.js +5 -2
  30. package/dist/commands/synthetics/utils.d.ts +1 -0
  31. package/dist/commands/synthetics/utils.js +6 -5
  32. package/package.json +4 -2
@@ -15,6 +15,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.UploadCommand = void 0;
16
16
  const chalk_1 = __importDefault(require("chalk"));
17
17
  const clipanion_1 = require("clipanion");
18
+ const fs_1 = require("fs");
19
+ const glob_1 = __importDefault(require("glob"));
18
20
  const path_1 = __importDefault(require("path"));
19
21
  const tiny_async_pool_1 = __importDefault(require("tiny-async-pool"));
20
22
  const apikey_1 = require("../../helpers/apikey");
@@ -22,44 +24,155 @@ const errors_1 = require("../../helpers/errors");
22
24
  const metrics_1 = require("../../helpers/metrics");
23
25
  const upload_1 = require("../../helpers/upload");
24
26
  const utils_1 = require("../../helpers/utils");
27
+ const interfaces_1 = require("./interfaces");
25
28
  const renderer_1 = require("./renderer");
26
29
  const utils_2 = require("./utils");
27
30
  class UploadCommand extends clipanion_1.Command {
28
31
  constructor() {
29
- super(...arguments);
32
+ super();
30
33
  this.config = {
31
34
  apiKey: process.env.DATADOG_API_KEY,
32
35
  datadogSite: process.env.DATADOG_SITE || 'datadoghq.com',
33
36
  };
34
37
  this.dryRun = false;
35
38
  this.maxConcurrency = 20;
39
+ this.compressDSYMsToDirectory = (dsyms, directoryPath) => __awaiter(this, void 0, void 0, function* () {
40
+ yield fs_1.promises.mkdir(directoryPath, { recursive: true });
41
+ return Promise.all(dsyms.map((dsym) => __awaiter(this, void 0, void 0, function* () {
42
+ const archivePath = utils_1.buildPath(directoryPath, `${dsym.slices[0].uuid}.zip`);
43
+ yield utils_2.zipDirectoryToArchive(dsym.bundlePath, archivePath);
44
+ return new interfaces_1.CompressedDsym(archivePath, dsym);
45
+ })));
46
+ });
47
+ this.findDSYMsInDirectory = (directoryPath) => __awaiter(this, void 0, void 0, function* () {
48
+ const dsyms = [];
49
+ for (const dSYMPath of glob_1.default.sync(utils_1.buildPath(directoryPath, '**/*.dSYM'))) {
50
+ try {
51
+ const stdout = (yield utils_2.executeDwarfdump(dSYMPath)).stdout;
52
+ const archSlices = this.parseDwarfdumpOutput(stdout);
53
+ dsyms.push({ bundlePath: dSYMPath, slices: archSlices });
54
+ }
55
+ catch (_a) {
56
+ this.context.stdout.write(renderer_1.renderInvalidDsymWarning(dSYMPath));
57
+ }
58
+ }
59
+ return Promise.all(dsyms);
60
+ });
61
+ /**
62
+ * Parses the output of `dwarfdump --uuid` command (ref.: https://www.unix.com/man-page/osx/1/dwarfdump/).
63
+ * It returns one or many arch slices read from the output.
64
+ *
65
+ * Example `dwarfdump --uuid` output:
66
+ * ```
67
+ * $ dwarfdump --uuid DDTest.framework.dSYM
68
+ * UUID: C8469F85-B060-3085-B69D-E46C645560EA (armv7) DDTest.framework.dSYM/Contents/Resources/DWARF/DDTest
69
+ * UUID: 06EE3D68-D605-3E92-B92D-2F48C02A505E (arm64) DDTest.framework.dSYM/Contents/Resources/DWARF/DDTest
70
+ * ```
71
+ */
72
+ this.parseDwarfdumpOutput = (output) => {
73
+ const lineRegexp = /UUID: ([0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}) \(([a-z0-9_]+)\) (.+)/;
74
+ return output
75
+ .split('\n')
76
+ .map((line) => {
77
+ const match = line.match(lineRegexp);
78
+ return match ? [{ arch: match[2], objectPath: match[3], uuid: match[1] }] : [];
79
+ })
80
+ .reduce((acc, nextSlice) => acc.concat(nextSlice), []);
81
+ };
82
+ /**
83
+ * It takes fat dSYM as input and returns multiple dSYMs by extracting **each arch**
84
+ * to separate dSYM file. New files are saved to `intermediatePath` and named by their object uuid (`<uuid>.dSYM`).
85
+ *
86
+ * For example, given `<source path>/Foo.dSYM/Contents/Resources/DWARF/Foo` dSYM with two arch slices: `arm64` (uuid1)
87
+ * and `x86_64` (uuid2), it will:
88
+ * - create `<intermediate path>/<uuid1>.dSYM/Contents/Resources/DWARF/Foo` for `arm64`,
89
+ * - create `<intermediate path>/<uuid2>.dSYM/Contents/Resources/DWARF/Foo` for `x86_64`.
90
+ */
91
+ this.thinDSYM = (dsym, intermediatePath) => __awaiter(this, void 0, void 0, function* () {
92
+ const slimmedDSYMs = [];
93
+ for (const slice of dsym.slices) {
94
+ try {
95
+ const newDSYMBundleName = `${slice.uuid}.dSYM`;
96
+ const newDSYMBundlePath = utils_1.buildPath(intermediatePath, newDSYMBundleName);
97
+ const newObjectPath = utils_1.buildPath(newDSYMBundlePath, path_1.default.relative(dsym.bundlePath, slice.objectPath));
98
+ // Extract arch slice:
99
+ yield fs_1.promises.mkdir(path_1.default.dirname(newObjectPath), { recursive: true });
100
+ yield utils_2.executeLipo(slice.objectPath, slice.arch, newObjectPath);
101
+ // The original dSYM bundle can also include `Info.plist` file, so copy it to the `<uuid>.dSYM` as well.
102
+ // Ref.: https://opensource.apple.com/source/lldb/lldb-179.1/www/symbols.html
103
+ const infoPlistPath = glob_1.default.sync(utils_1.buildPath(dsym.bundlePath, '**/Info.plist'))[0];
104
+ if (infoPlistPath) {
105
+ const newInfoPlistPath = utils_1.buildPath(newDSYMBundlePath, path_1.default.relative(dsym.bundlePath, infoPlistPath));
106
+ yield fs_1.promises.mkdir(path_1.default.dirname(newInfoPlistPath), { recursive: true });
107
+ yield fs_1.promises.copyFile(infoPlistPath, newInfoPlistPath);
108
+ }
109
+ slimmedDSYMs.push({
110
+ bundlePath: newDSYMBundlePath,
111
+ slices: [{ arch: slice.arch, uuid: slice.uuid, objectPath: newObjectPath }],
112
+ });
113
+ }
114
+ catch (_b) {
115
+ this.context.stdout.write(renderer_1.renderDSYMSlimmingFailure(dsym, slice));
116
+ }
117
+ }
118
+ return Promise.all(slimmedDSYMs);
119
+ });
120
+ /**
121
+ * It takes `N` dSYMs and returns `N` or more dSYMs. If a dSYM includes more than one arch slice,
122
+ * it will be thinned by extracting each arch to a new dSYM in `intermediatePath`.
123
+ */
124
+ this.thinDSYMs = (dsyms, intermediatePath) => __awaiter(this, void 0, void 0, function* () {
125
+ yield fs_1.promises.mkdir(intermediatePath, { recursive: true });
126
+ let slimDSYMs = [];
127
+ for (const dsym of dsyms) {
128
+ if (dsym.slices.length > 1) {
129
+ slimDSYMs = slimDSYMs.concat(yield this.thinDSYM(dsym, intermediatePath));
130
+ }
131
+ else {
132
+ slimDSYMs.push(dsym);
133
+ }
134
+ }
135
+ return Promise.all(slimDSYMs);
136
+ });
137
+ this.cliVersion = require('../../../package.json').version;
36
138
  }
37
139
  execute() {
38
140
  return __awaiter(this, void 0, void 0, function* () {
141
+ // Normalizing the basePath to resolve .. and .
39
142
  this.basePath = path_1.default.posix.normalize(this.basePath);
40
- const cliVersion = require('../../../package.json').version;
143
+ this.context.stdout.write(renderer_1.renderCommandInfo(this.basePath, this.maxConcurrency, this.dryRun));
41
144
  const metricsLogger = metrics_1.getMetricsLogger({
42
145
  datadogSite: process.env.DATADOG_SITE,
43
- defaultTags: [`cli_version:${cliVersion}`],
146
+ defaultTags: [`cli_version:${this.cliVersion}`],
44
147
  prefix: 'datadog.ci.dsyms.',
45
148
  });
46
- this.context.stdout.write(renderer_1.renderCommandInfo(this.basePath, this.maxConcurrency, this.dryRun));
47
- const initialTime = Date.now();
48
- let searchPath = this.basePath;
49
- if (yield utils_2.isZipFile(this.basePath)) {
50
- searchPath = yield utils_2.unzipToTmpDir(this.basePath);
51
- }
52
149
  const apiKeyValidator = apikey_1.newApiKeyValidator({
53
150
  apiKey: this.config.apiKey,
54
151
  datadogSite: this.config.datadogSite,
55
152
  metricsLogger: metricsLogger.logger,
56
153
  });
57
- const payloads = yield utils_2.getMatchingDSYMFiles(searchPath, this.context);
58
- const validPayloads = payloads.filter((payload) => payload !== undefined);
154
+ const initialTime = Date.now();
155
+ const tmpDirectory = yield utils_2.createUniqueTmpDirectory();
156
+ const intermediateDirectory = utils_1.buildPath(tmpDirectory, 'datadog-ci', 'dsyms', 'intermediate');
157
+ const uploadDirectory = utils_1.buildPath(tmpDirectory, 'datadog-ci', 'dsyms', 'upload');
158
+ this.context.stdout.write(renderer_1.renderCommandDetail(intermediateDirectory, uploadDirectory));
159
+ // The CLI input path can be a folder or `.zip` archive with `*.dSYM` files.
160
+ // In case of `.zip`, extract it to temporary location, so it can be handled the same way as folder.
161
+ let dSYMsSearchDirectory = this.basePath;
162
+ if (yield utils_2.isZipFile(this.basePath)) {
163
+ yield utils_2.unzipArchiveToDirectory(this.basePath, tmpDirectory);
164
+ dSYMsSearchDirectory = tmpDirectory;
165
+ }
166
+ const dsyms = yield this.findDSYMsInDirectory(dSYMsSearchDirectory);
167
+ // Reduce dSYMs size by extracting arch slices from fat dSYMs to separate single-arch dSYMs in intermediate location.
168
+ // This is to avoid exceeding intake limit whenever possible.
169
+ const slimDSYMs = yield this.thinDSYMs(dsyms, intermediateDirectory);
170
+ // Compress each dSYM into single `.zip` archive.
171
+ const compressedDSYMs = yield this.compressDSYMsToDirectory(slimDSYMs, uploadDirectory);
59
172
  const requestBuilder = this.getRequestBuilder();
60
173
  const uploadDSYM = this.uploadDSYM(requestBuilder, metricsLogger, apiKeyValidator);
61
174
  try {
62
- const results = yield tiny_async_pool_1.default(this.maxConcurrency, validPayloads, uploadDSYM);
175
+ const results = yield tiny_async_pool_1.default(this.maxConcurrency, compressedDSYMs, uploadDSYM);
63
176
  const totalTime = (Date.now() - initialTime) / 1000;
64
177
  this.context.stdout.write(renderer_1.renderSuccessfulCommand(results, totalTime, this.dryRun));
65
178
  metricsLogger.logger.gauge('duration', totalTime);
@@ -74,6 +187,7 @@ class UploadCommand extends clipanion_1.Command {
74
187
  throw error;
75
188
  }
76
189
  finally {
190
+ yield utils_2.deleteDirectory(tmpDirectory);
77
191
  try {
78
192
  yield metricsLogger.flush();
79
193
  }
@@ -94,7 +208,7 @@ class UploadCommand extends clipanion_1.Command {
94
208
  }
95
209
  uploadDSYM(requestBuilder, metricsLogger, apiKeyValidator) {
96
210
  return (dSYM) => __awaiter(this, void 0, void 0, function* () {
97
- const payload = yield dSYM.asMultipartPayload();
211
+ const payload = dSYM.asMultipartPayload();
98
212
  if (this.dryRun) {
99
213
  this.context.stdout.write(`[DRYRUN] ${renderer_1.renderUpload(dSYM)}`);
100
214
  return upload_1.UploadStatus.Success;
@@ -1,9 +1,15 @@
1
- import { BaseContext } from 'clipanion';
2
- import { Dsym } from './interfaces';
3
1
  export declare const isZipFile: (filepath: string) => Promise<boolean>;
4
- export declare const getMatchingDSYMFiles: (absoluteFolderPath: string, context: BaseContext) => Promise<(Dsym | undefined)[]>;
5
- export declare const dwarfdumpUUID: (filePath: string) => Promise<string[]>;
6
- export declare const zipToTmpDir: (sourcePath: string, targetFilename: string) => Promise<string>;
7
- export declare const unzipToTmpDir: (sourcePath: string) => Promise<string>;
2
+ export declare const createUniqueTmpDirectory: () => Promise<string>;
3
+ export declare const deleteDirectory: (directoryPath: string) => Promise<void>;
4
+ export declare const zipDirectoryToArchive: (directoryPath: string, archivePath: string) => Promise<void>;
5
+ export declare const unzipArchiveToDirectory: (archivePath: string, directoryPath: string) => Promise<void>;
6
+ export declare const executeDwarfdump: (dSYMPath: string) => Promise<{
7
+ stderr: string;
8
+ stdout: string;
9
+ }>;
10
+ export declare const executeLipo: (objectPath: string, arch: string, newObjectPath: string) => Promise<{
11
+ stderr: string;
12
+ stdout: string;
13
+ }>;
8
14
  export declare const getBaseIntakeUrl: () => string;
9
15
  export declare const pluralize: (nb: number, singular: string, plural: string) => string;
@@ -12,18 +12,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.pluralize = exports.getBaseIntakeUrl = exports.unzipToTmpDir = exports.zipToTmpDir = exports.dwarfdumpUUID = exports.getMatchingDSYMFiles = exports.isZipFile = void 0;
15
+ exports.pluralize = exports.getBaseIntakeUrl = exports.executeLipo = exports.executeDwarfdump = exports.unzipArchiveToDirectory = exports.zipDirectoryToArchive = exports.deleteDirectory = exports.createUniqueTmpDirectory = exports.isZipFile = void 0;
16
16
  const child_process_1 = require("child_process");
17
17
  const fs_1 = require("fs");
18
- const glob_1 = __importDefault(require("glob"));
19
18
  const os_1 = require("os");
20
19
  const path_1 = __importDefault(require("path"));
20
+ const rimraf_1 = __importDefault(require("rimraf"));
21
21
  const util_1 = require("util");
22
- const interfaces_1 = require("./interfaces");
23
22
  const utils_1 = require("../../helpers/utils");
24
- const renderer_1 = require("./renderer");
25
- const UUID_REGEX = '[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}';
26
- const globAsync = util_1.promisify(glob_1.default);
27
23
  const isZipFile = (filepath) => __awaiter(void 0, void 0, void 0, function* () {
28
24
  try {
29
25
  const stats = yield fs_1.promises.stat(filepath);
@@ -35,54 +31,30 @@ const isZipFile = (filepath) => __awaiter(void 0, void 0, void 0, function* () {
35
31
  }
36
32
  });
37
33
  exports.isZipFile = isZipFile;
38
- const getMatchingDSYMFiles = (absoluteFolderPath, context) => __awaiter(void 0, void 0, void 0, function* () {
39
- const dSYMFiles = yield globAsync(utils_1.buildPath(absoluteFolderPath, '**/*.dSYM'), {});
40
- const allDsyms = dSYMFiles.map((dSYMPath) => __awaiter(void 0, void 0, void 0, function* () {
41
- try {
42
- const uuids = yield exports.dwarfdumpUUID(dSYMPath);
43
- return new interfaces_1.Dsym(dSYMPath, uuids);
44
- }
45
- catch (_a) {
46
- context.stdout.write(renderer_1.renderInvalidDsymWarning(dSYMPath));
47
- return undefined;
48
- }
49
- }));
50
- return Promise.all(allDsyms);
34
+ const createUniqueTmpDirectory = () => __awaiter(void 0, void 0, void 0, function* () {
35
+ const uniqueValue = Math.random() * Number.MAX_SAFE_INTEGER;
36
+ const directoryPath = utils_1.buildPath(os_1.tmpdir(), uniqueValue.toString());
37
+ yield fs_1.promises.mkdir(directoryPath, { recursive: true });
38
+ return directoryPath;
51
39
  });
52
- exports.getMatchingDSYMFiles = getMatchingDSYMFiles;
53
- const dwarfdumpUUID = (filePath) => __awaiter(void 0, void 0, void 0, function* () {
54
- const output = yield execute(`dwarfdump --uuid '${filePath}'`);
55
- const uuids = [];
56
- output.stdout.split('\n').forEach((line) => {
57
- const regexMatches = line.match(UUID_REGEX);
58
- if (regexMatches && regexMatches.length > 0) {
59
- uuids.push(regexMatches[0]);
60
- }
61
- });
62
- return uuids;
63
- });
64
- exports.dwarfdumpUUID = dwarfdumpUUID;
65
- const tmpFolder = utils_1.buildPath(os_1.tmpdir(), 'datadog-ci', 'dsyms');
66
- const zipToTmpDir = (sourcePath, targetFilename) => __awaiter(void 0, void 0, void 0, function* () {
67
- yield fs_1.promises.mkdir(tmpFolder, { recursive: true });
68
- const targetPath = utils_1.buildPath(tmpFolder, targetFilename);
69
- const sourceDir = path_1.default.dirname(sourcePath);
70
- const sourceFile = path_1.default.basename(sourcePath);
71
- // `zip -r foo.zip f1/f2/f3/foo.dSYM`
72
- // this keeps f1/f2/f3 folders in foo.zip, we don't want this
73
- // `cwd: sourceDir` is passed to avoid that
74
- yield execute(`zip -r '${targetPath}' '${sourceFile}'`, sourceDir);
75
- return targetPath;
40
+ exports.createUniqueTmpDirectory = createUniqueTmpDirectory;
41
+ const deleteDirectory = (directoryPath) => __awaiter(void 0, void 0, void 0, function* () { return new Promise((resolve, reject) => rimraf_1.default(directoryPath, () => resolve())); });
42
+ exports.deleteDirectory = deleteDirectory;
43
+ const zipDirectoryToArchive = (directoryPath, archivePath) => __awaiter(void 0, void 0, void 0, function* () {
44
+ const cwd = path_1.default.dirname(directoryPath);
45
+ const directoryName = path_1.default.basename(directoryPath);
46
+ yield execute(`zip -r '${archivePath}' '${directoryName}'`, cwd);
76
47
  });
77
- exports.zipToTmpDir = zipToTmpDir;
78
- const unzipToTmpDir = (sourcePath) => __awaiter(void 0, void 0, void 0, function* () {
79
- const targetPath = utils_1.buildPath(tmpFolder, path_1.default.basename(sourcePath, '.zip'), Date.now().toString());
80
- const dirPath = path_1.default.dirname(targetPath);
81
- yield fs_1.promises.mkdir(dirPath, { recursive: true });
82
- yield execute(`unzip -o '${sourcePath}' -d '${targetPath}'`);
83
- return targetPath;
48
+ exports.zipDirectoryToArchive = zipDirectoryToArchive;
49
+ const unzipArchiveToDirectory = (archivePath, directoryPath) => __awaiter(void 0, void 0, void 0, function* () {
50
+ yield fs_1.promises.mkdir(directoryPath, { recursive: true });
51
+ yield execute(`unzip -o '${archivePath}' -d '${directoryPath}'`);
84
52
  });
85
- exports.unzipToTmpDir = unzipToTmpDir;
53
+ exports.unzipArchiveToDirectory = unzipArchiveToDirectory;
54
+ const executeDwarfdump = (dSYMPath) => __awaiter(void 0, void 0, void 0, function* () { return execute(`dwarfdump --uuid '${dSYMPath}'`); });
55
+ exports.executeDwarfdump = executeDwarfdump;
56
+ const executeLipo = (objectPath, arch, newObjectPath) => __awaiter(void 0, void 0, void 0, function* () { return execute(`lipo '${objectPath}' -thin ${arch} -output '${newObjectPath}'`); });
57
+ exports.executeLipo = executeLipo;
86
58
  const execProc = util_1.promisify(child_process_1.exec);
87
59
  const execute = (cmd, cwd) => execProc(cmd, {
88
60
  cwd,
@@ -172,6 +172,20 @@ describe('upload', () => {
172
172
  key2: 'value2',
173
173
  });
174
174
  }));
175
+ test('should set logsEnabled for each file', () => __awaiter(void 0, void 0, void 0, function* () {
176
+ process.env.DD_CIVISIBILITY_LOGS_ENABLED = 'true';
177
+ const context = createMockContext();
178
+ const command = new upload_1.UploadJUnitXMLCommand();
179
+ const [firstFile, secondFile] = yield command['getMatchingJUnitXMLFiles'].call({
180
+ basePaths: ['./src/commands/junit/__tests__/fixtures'],
181
+ config: {},
182
+ context,
183
+ logs: true,
184
+ service: 'service',
185
+ });
186
+ expect(firstFile.logsEnabled).toBe(true);
187
+ expect(secondFile.logsEnabled).toBe(true);
188
+ }));
175
189
  });
176
190
  });
177
191
  describe('execute', () => {
@@ -179,7 +193,7 @@ describe('execute', () => {
179
193
  const cli = makeCli();
180
194
  const context = createMockContext();
181
195
  process.env = { DATADOG_API_KEY: 'PLACEHOLDER' };
182
- const code = yield cli.run(['junit', 'upload', '--service', 'test-service', '--dry-run', ...paths], context);
196
+ const code = yield cli.run(['junit', 'upload', '--service', 'test-service', '--dry-run', '--logs', ...paths], context);
183
197
  return { context, code };
184
198
  });
185
199
  test('relative path with double dots', () => __awaiter(void 0, void 0, void 0, function* () {
@@ -39,6 +39,9 @@ const uploadJUnitXML = (request) => (jUnitXML, write) => __awaiter(void 0, void
39
39
  fileName = 'default_file_name';
40
40
  }
41
41
  const spanTags = Object.assign(Object.assign({ service: jUnitXML.service }, jUnitXML.spanTags), { '_dd.cireport_version': '2' });
42
+ if (jUnitXML.logsEnabled) {
43
+ spanTags['_dd.junitxml_logs'] = 'true';
44
+ }
42
45
  form.append('event', JSON.stringify(spanTags), { filename: 'event.json' });
43
46
  let uniqueFileName = `${fileName}-${jUnitXML.service}-${spanTags[tags_1.GIT_SHA]}`;
44
47
  if (spanTags[tags_1.CI_PIPELINE_URL]) {
@@ -3,6 +3,7 @@ import { AxiosPromise, AxiosResponse } from 'axios';
3
3
  import { Writable } from 'stream';
4
4
  import { SpanTags } from '../../helpers/interfaces';
5
5
  export interface Payload {
6
+ logsEnabled: boolean;
6
7
  service: string;
7
8
  spanTags: SpanTags;
8
9
  xmlPath: string;
@@ -5,6 +5,7 @@ export declare class UploadJUnitXMLCommand extends Command {
5
5
  private config;
6
6
  private dryRun;
7
7
  private env?;
8
+ private logs;
8
9
  private maxConcurrency;
9
10
  private service?;
10
11
  private tags?;
@@ -51,6 +51,7 @@ class UploadJUnitXMLCommand extends clipanion_1.Command {
51
51
  envVarTags: process.env.DD_TAGS,
52
52
  };
53
53
  this.dryRun = false;
54
+ this.logs = false;
54
55
  this.maxConcurrency = 20;
55
56
  }
56
57
  execute() {
@@ -69,6 +70,11 @@ class UploadJUnitXMLCommand extends clipanion_1.Command {
69
70
  if (!this.config.env) {
70
71
  this.config.env = this.env;
71
72
  }
73
+ if (!this.logs &&
74
+ process.env.DD_CIVISIBILITY_LOGS_ENABLED &&
75
+ !['false', '0'].includes(process.env.DD_CIVISIBILITY_LOGS_ENABLED.toLowerCase())) {
76
+ this.logs = true;
77
+ }
72
78
  const api = this.getApiHelper();
73
79
  // Normalizing the basePath to resolve .. and .
74
80
  // Always using the posix version to avoid \ on Windows.
@@ -113,6 +119,7 @@ class UploadJUnitXMLCommand extends clipanion_1.Command {
113
119
  return true;
114
120
  });
115
121
  return validUniqueFiles.map((jUnitXMLFilePath) => ({
122
+ logsEnabled: this.logs,
116
123
  service: this.service,
117
124
  spanTags,
118
125
  xmlPath: jUnitXMLFilePath,
@@ -168,6 +175,10 @@ UploadJUnitXMLCommand.usage = clipanion_1.Command.Usage({
168
175
  'Upload all jUnit XML test report files in current directory to the datadoghq.eu site',
169
176
  'DATADOG_SITE=datadoghq.eu datadog-ci junit upload --service my-service .',
170
177
  ],
178
+ [
179
+ 'Upload all jUnit XML test report files in current directory while also collecting logs',
180
+ 'datadog-ci junit upload --service my-service --logs .',
181
+ ],
171
182
  ],
172
183
  });
173
184
  UploadJUnitXMLCommand.addPath('junit', 'upload');
@@ -177,3 +188,4 @@ UploadJUnitXMLCommand.addOption('dryRun', clipanion_1.Command.Boolean('--dry-run
177
188
  UploadJUnitXMLCommand.addOption('tags', clipanion_1.Command.Array('--tags'));
178
189
  UploadJUnitXMLCommand.addOption('basePaths', clipanion_1.Command.Rest({ required: 1 }));
179
190
  UploadJUnitXMLCommand.addOption('maxConcurrency', clipanion_1.Command.String('--max-concurrency'));
191
+ UploadJUnitXMLCommand.addOption('logs', clipanion_1.Command.Boolean('--logs'));
@@ -10,8 +10,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  /* tslint:disable:no-string-literal */
13
- jest.mock('aws-sdk');
14
13
  const aws_sdk_1 = require("aws-sdk");
14
+ jest.mock('aws-sdk');
15
+ const aws_sdk_2 = require("aws-sdk");
15
16
  const constants_1 = require("../../constants");
16
17
  const commons_1 = require("../../functions/commons");
17
18
  const instrument_1 = require("../../instrument");
@@ -149,7 +150,7 @@ describe('commons', () => {
149
150
  describe('findLatestLayerVersion', () => {
150
151
  test('finds latests version for Python39', () => __awaiter(void 0, void 0, void 0, function* () {
151
152
  const layer = `arn:aws:lambda:sa-east-1:${constants_1.DEFAULT_LAYER_AWS_ACCOUNT}:layer:Datadog-Python39`;
152
- aws_sdk_1.Lambda.mockImplementation(() => fixtures_1.makeMockLambda({}, {
153
+ aws_sdk_2.Lambda.mockImplementation(() => fixtures_1.makeMockLambda({}, {
153
154
  [`${layer}:1`]: {
154
155
  LayerVersionArn: `${layer}:1`,
155
156
  Version: 1,
@@ -187,7 +188,7 @@ describe('commons', () => {
187
188
  }));
188
189
  test('finds latests version for Node14', () => __awaiter(void 0, void 0, void 0, function* () {
189
190
  const layer = `arn:aws:lambda:us-east-1:${constants_1.DEFAULT_LAYER_AWS_ACCOUNT}:layer:Datadog-Node14-x`;
190
- aws_sdk_1.Lambda.mockImplementation(() => fixtures_1.makeMockLambda({}, {
191
+ aws_sdk_2.Lambda.mockImplementation(() => fixtures_1.makeMockLambda({}, {
191
192
  [`${layer}:1`]: {
192
193
  LayerVersionArn: `${layer}:1`,
193
194
  Version: 1,
@@ -221,7 +222,7 @@ describe('commons', () => {
221
222
  }));
222
223
  test('returns 0 when no layer can be found', () => __awaiter(void 0, void 0, void 0, function* () {
223
224
  ;
224
- aws_sdk_1.Lambda.mockImplementation(() => fixtures_1.makeMockLambda({}, {}));
225
+ aws_sdk_2.Lambda.mockImplementation(() => fixtures_1.makeMockLambda({}, {}));
225
226
  const runtime = 'python3.7';
226
227
  const region = 'us-east-1';
227
228
  const expectedLatestVersion = 0;
@@ -238,17 +239,28 @@ describe('commons', () => {
238
239
  afterAll(() => {
239
240
  process.env = OLD_ENV;
240
241
  });
241
- test('returns true when any AWS credential is missing', () => {
242
+ test('returns true when only AWS_SECRET_ACCESS_KEY env var is set and `~/.aws/credentials` are missing', () => {
242
243
  process.env[constants_1.AWS_SECRET_ACCESS_KEY_ENV_VAR] = 'SOME-AWS-SECRET-ACCESS-KEY';
243
- expect(commons_1.isMissingAWSCredentials()).toBe(true);
244
- // Reset env
245
- process.env = {};
244
+ aws_sdk_1.config.credentials = undefined;
245
+ expect(commons_1.isMissingAWSCredentials()).toBe(true); // We return true since AWS_ACCESS_KEY_ID_ENV_VAR is missing
246
+ });
247
+ test('returns true when only AWS_ACCESS_KEY_ID environment variable is set and `~/.aws/credentials` are missing', () => {
246
248
  process.env[constants_1.AWS_ACCESS_KEY_ID_ENV_VAR] = 'SOME-AWS-ACCESS-KEY-ID';
247
- expect(commons_1.isMissingAWSCredentials()).toBe(true);
249
+ aws_sdk_1.config.credentials = undefined;
250
+ expect(commons_1.isMissingAWSCredentials()).toBe(true); // We return true since AWS_SECRET_ACCESS_KEY_ENV_VAR is missing
248
251
  });
249
- test('returns false when AWS credentials are set', () => {
252
+ test('returns false when AWS credentials via environment variables are set', () => {
250
253
  process.env[constants_1.AWS_ACCESS_KEY_ID_ENV_VAR] = 'SOME-AWS-ACCESS-KEY-ID';
251
254
  process.env[constants_1.AWS_SECRET_ACCESS_KEY_ENV_VAR] = 'SOME-AWS-SECRET-ACCESS-KEY';
255
+ aws_sdk_1.config.credentials = { foo: 'bar' };
256
+ expect(commons_1.isMissingAWSCredentials()).toBe(false);
257
+ });
258
+ test('returns true when both environment variables and `~/.aws/credentials` are missing', () => {
259
+ aws_sdk_1.config.credentials = undefined;
260
+ expect(commons_1.isMissingAWSCredentials()).toBe(true);
261
+ });
262
+ test('returns false when AWS credentials via `~/.aws/credentials` are set', () => {
263
+ aws_sdk_1.config.credentials = { foo: 'bar' };
252
264
  expect(commons_1.isMissingAWSCredentials()).toBe(false);
253
265
  });
254
266
  });
@@ -155,7 +155,10 @@ const findLatestLayerVersion = (layer, region) => __awaiter(void 0, void 0, void
155
155
  return latestVersion;
156
156
  });
157
157
  exports.findLatestLayerVersion = findLatestLayerVersion;
158
- const isMissingAWSCredentials = () => process.env[constants_1.AWS_ACCESS_KEY_ID_ENV_VAR] === undefined || process.env[constants_1.AWS_SECRET_ACCESS_KEY_ENV_VAR] === undefined;
158
+ const isMissingAWSCredentials = () =>
159
+ // If env vars and aws_sdk_config.credentials are not set return true otherwise return false
160
+ (process.env[constants_1.AWS_ACCESS_KEY_ID_ENV_VAR] === undefined || process.env[constants_1.AWS_SECRET_ACCESS_KEY_ENV_VAR] === undefined) &&
161
+ !aws_sdk_1.config.credentials;
159
162
  exports.isMissingAWSCredentials = isMissingAWSCredentials;
160
163
  const isMissingDatadogSiteEnvVar = () => {
161
164
  const site = process.env[constants_1.CI_SITE_ENV_VAR];
@@ -129,30 +129,36 @@ describe('run-test', () => {
129
129
  expect(startTunnelSpy).toHaveBeenCalledTimes(1);
130
130
  expect(stopTunnelSpy).toHaveBeenCalledTimes(1);
131
131
  }));
132
- test('getTestsList throws', () => __awaiter(void 0, void 0, void 0, function* () {
133
- const serverError = new Error('Server Error');
134
- serverError.response = { data: { errors: ['Bad Gateway'] }, status: 502 };
135
- serverError.config = { baseURL: 'baseURL', url: 'url' };
136
- const apiHelper = {
137
- searchTests: jest.fn(() => {
138
- throw serverError;
139
- }),
140
- };
141
- jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
142
- yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { testSearchQuery: 'a-search-query', tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError('UNAVAILABLE_TEST_CONFIG'));
143
- }));
144
- test('getTestsToTrigger throws', () => __awaiter(void 0, void 0, void 0, function* () {
145
- const serverError = new Error('Server Error');
146
- serverError.response = { data: { errors: ['Bad Gateway'] }, status: 502 };
147
- serverError.config = { baseURL: 'baseURL', url: 'url' };
148
- const apiHelper = {
149
- getTest: jest.fn(() => {
150
- throw serverError;
151
- }),
152
- };
153
- jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
154
- yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { publicIds: ['public-id-1'], tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError('UNAVAILABLE_TEST_CONFIG'));
155
- }));
132
+ const cases = [
133
+ [403, 'AUTHORIZATION_ERROR'],
134
+ [502, 'UNAVAILABLE_TEST_CONFIG'],
135
+ ];
136
+ describe.each(cases)('%s triggers %s', (status, error) => {
137
+ test(`getTestsList throws - ${status}`, () => __awaiter(void 0, void 0, void 0, function* () {
138
+ const serverError = new Error('Server Error');
139
+ serverError.response = { data: { errors: ['Error'] }, status };
140
+ serverError.config = { baseURL: 'baseURL', url: 'url' };
141
+ const apiHelper = {
142
+ searchTests: jest.fn(() => {
143
+ throw serverError;
144
+ }),
145
+ };
146
+ jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
147
+ yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { testSearchQuery: 'a-search-query', tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError(error));
148
+ }));
149
+ test(`getTestsToTrigger throws - ${status}`, () => __awaiter(void 0, void 0, void 0, function* () {
150
+ const serverError = new Error('Server Error');
151
+ serverError.response = { data: { errors: ['Bad Gateway'] }, status };
152
+ serverError.config = { baseURL: 'baseURL', url: 'url' };
153
+ const apiHelper = {
154
+ getTest: jest.fn(() => {
155
+ throw serverError;
156
+ }),
157
+ };
158
+ jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
159
+ yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { publicIds: ['public-id-1'], tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError(error));
160
+ }));
161
+ });
156
162
  test('getPresignedURL throws', () => __awaiter(void 0, void 0, void 0, function* () {
157
163
  jest.spyOn(utils, 'getTestsToTrigger').mockReturnValue(Promise.resolve({
158
164
  overriddenTestsToTrigger: [],
@@ -87,27 +87,36 @@ describe('utils', () => {
87
87
  test('runTests sends batch metadata', () => __awaiter(void 0, void 0, void 0, function* () {
88
88
  jest.spyOn(ciHelpers, 'getCIMetadata').mockImplementation(() => undefined);
89
89
  const payloadMetadataSpy = jest.fn();
90
- const axiosMock = jest.spyOn(axios_1.default, 'create');
91
- axiosMock.mockImplementation((() => (e) => {
92
- payloadMetadataSpy(e.data.metadata);
93
- if (e.url === '/synthetics/tests/trigger/ci') {
90
+ jest.spyOn(axios_1.default, 'create').mockImplementation((() => (request) => {
91
+ payloadMetadataSpy(request.data.metadata);
92
+ if (request.url === '/synthetics/tests/trigger/ci') {
94
93
  return { data: fakeTrigger };
95
94
  }
96
95
  }));
97
96
  yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
98
- expect(payloadMetadataSpy).toHaveBeenCalledWith({
99
- ci: { job: {}, pipeline: {}, provider: {}, stage: {} },
100
- git: { commit: { author: {}, committer: {} } },
101
- trigger_app: 'npm_package',
102
- });
97
+ expect(payloadMetadataSpy).toHaveBeenCalledWith(undefined);
103
98
  const metadata = {
104
99
  ci: { job: { name: 'job' }, pipeline: {}, provider: { name: 'jest' }, stage: {} },
105
100
  git: { commit: { author: {}, committer: {}, message: 'test' } },
106
101
  };
107
102
  jest.spyOn(ciHelpers, 'getCIMetadata').mockImplementation(() => metadata);
103
+ yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
104
+ expect(payloadMetadataSpy).toHaveBeenCalledWith(metadata);
105
+ }));
106
+ test('runTests api call includes trigger app header', () => __awaiter(void 0, void 0, void 0, function* () {
107
+ jest.spyOn(ciHelpers, 'getCIMetadata').mockImplementation(() => undefined);
108
+ const headersMetadataSpy = jest.fn();
109
+ jest.spyOn(axios_1.default, 'create').mockImplementation((() => (request) => {
110
+ headersMetadataSpy(request.headers);
111
+ if (request.url === '/synthetics/tests/trigger/ci') {
112
+ return { data: fakeTrigger };
113
+ }
114
+ }));
115
+ yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
116
+ expect(headersMetadataSpy).toHaveBeenCalledWith(expect.objectContaining({ 'X-Trigger-App': 'npm_package' }));
108
117
  utils.setCiTriggerApp('unit_test');
109
118
  yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
110
- expect(payloadMetadataSpy).toHaveBeenCalledWith(Object.assign(Object.assign({}, metadata), { trigger_app: 'unit_test' }));
119
+ expect(headersMetadataSpy).toHaveBeenCalledWith(expect.objectContaining({ 'X-Trigger-App': 'unit_test' }));
111
120
  }));
112
121
  test('should run test with publicId from url', () => __awaiter(void 0, void 0, void 0, function* () {
113
122
  const axiosMock = jest.spyOn(axios_1.default, 'create');