@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.
- package/LICENSE-3rdparty.csv +2 -0
- package/dist/commands/dsyms/__tests__/upload.test.js +219 -16
- package/dist/commands/dsyms/__tests__/utils.test.js +86 -46
- package/dist/commands/dsyms/interfaces.d.ts +14 -5
- package/dist/commands/dsyms/interfaces.js +15 -28
- package/dist/commands/dsyms/renderer.d.ts +7 -5
- package/dist/commands/dsyms/renderer.js +18 -6
- package/dist/commands/dsyms/upload.d.ts +31 -0
- package/dist/commands/dsyms/upload.js +127 -13
- package/dist/commands/dsyms/utils.d.ts +12 -6
- package/dist/commands/dsyms/utils.js +23 -51
- package/dist/commands/junit/__tests__/upload.test.js +15 -1
- package/dist/commands/junit/api.js +3 -0
- package/dist/commands/junit/interfaces.d.ts +1 -0
- package/dist/commands/junit/upload.d.ts +1 -0
- package/dist/commands/junit/upload.js +12 -0
- package/dist/commands/lambda/__tests__/functions/commons.test.js +22 -10
- package/dist/commands/lambda/functions/commons.js +4 -1
- package/dist/commands/synthetics/__tests__/run-test.test.js +30 -24
- package/dist/commands/synthetics/__tests__/utils.test.js +19 -10
- package/dist/commands/synthetics/api.d.ts +3 -12
- package/dist/commands/synthetics/api.js +4 -1
- package/dist/commands/synthetics/command.js +4 -0
- package/dist/commands/synthetics/errors.d.ts +4 -4
- package/dist/commands/synthetics/errors.js +5 -4
- package/dist/commands/synthetics/interfaces.d.ts +1 -4
- package/dist/commands/synthetics/reporters/default.js +2 -2
- package/dist/commands/synthetics/run-test.d.ts +1 -11
- package/dist/commands/synthetics/run-test.js +5 -2
- package/dist/commands/synthetics/utils.d.ts +1 -0
- package/dist/commands/synthetics/utils.js +6 -5
- 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(
|
|
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
|
-
|
|
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
|
|
58
|
-
const
|
|
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,
|
|
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 =
|
|
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
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
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.
|
|
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
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
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]) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
244
|
-
//
|
|
245
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|
|
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');
|