@datadog/datadog-ci 1.1.0 → 1.2.0
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/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 +5 -3
|
@@ -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,
|
|
@@ -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');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AxiosError } from 'axios';
|
|
2
|
-
import { APIConfiguration,
|
|
2
|
+
import { APIConfiguration, APIHelper } from './interfaces';
|
|
3
3
|
interface BackendError {
|
|
4
4
|
errors: string[];
|
|
5
5
|
}
|
|
@@ -9,17 +9,8 @@ export declare class EndpointError extends Error {
|
|
|
9
9
|
constructor(message: string, status: number);
|
|
10
10
|
}
|
|
11
11
|
export declare const formatBackendErrors: (requestError: AxiosError<BackendError>) => string;
|
|
12
|
+
export declare const isForbiddenError: (error: AxiosError | EndpointError) => boolean;
|
|
12
13
|
export declare const isNotFoundError: (error: AxiosError | EndpointError) => boolean;
|
|
13
14
|
export declare const is5xxError: (error: AxiosError | EndpointError) => boolean | 0 | undefined;
|
|
14
|
-
export declare const apiConstructor: (configuration: APIConfiguration) =>
|
|
15
|
-
getPresignedURL: (testIds: string[]) => Promise<{
|
|
16
|
-
url: string;
|
|
17
|
-
}>;
|
|
18
|
-
getTest: (testId: string) => Promise<Test>;
|
|
19
|
-
pollResults: (resultIds: string[]) => Promise<{
|
|
20
|
-
results: PollResult[];
|
|
21
|
-
}>;
|
|
22
|
-
searchTests: (query: string) => Promise<TestSearchResult>;
|
|
23
|
-
triggerTests: (data: Payload) => Promise<Trigger>;
|
|
24
|
-
};
|
|
15
|
+
export declare const apiConstructor: (configuration: APIConfiguration) => APIHelper;
|
|
25
16
|
export {};
|
|
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.apiConstructor = exports.is5xxError = exports.isNotFoundError = exports.formatBackendErrors = exports.EndpointError = void 0;
|
|
12
|
+
exports.apiConstructor = exports.is5xxError = exports.isNotFoundError = exports.isForbiddenError = exports.formatBackendErrors = exports.EndpointError = void 0;
|
|
13
13
|
const querystring_1 = require("querystring");
|
|
14
14
|
const utils_1 = require("../../helpers/utils");
|
|
15
15
|
const utils_2 = require("./utils");
|
|
@@ -43,6 +43,7 @@ exports.formatBackendErrors = formatBackendErrors;
|
|
|
43
43
|
const triggerTests = (request) => (data) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
44
|
const resp = yield retryRequest({
|
|
45
45
|
data,
|
|
46
|
+
headers: { 'X-Trigger-App': utils_2.ciTriggerApp },
|
|
46
47
|
method: 'POST',
|
|
47
48
|
url: '/synthetics/tests/trigger/ci',
|
|
48
49
|
}, request);
|
|
@@ -88,6 +89,8 @@ const retryOn5xxErrors = (retries, error) => {
|
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
91
|
const getErrorHttpStatus = (error) => { var _a; return 'status' in error ? error.status : (_a = error.response) === null || _a === void 0 ? void 0 : _a.status; };
|
|
92
|
+
const isForbiddenError = (error) => getErrorHttpStatus(error) === 403;
|
|
93
|
+
exports.isForbiddenError = isForbiddenError;
|
|
91
94
|
const isNotFoundError = (error) => getErrorHttpStatus(error) === 404;
|
|
92
95
|
exports.isNotFoundError = isNotFoundError;
|
|
93
96
|
const is5xxError = (error) => {
|
|
@@ -142,6 +142,10 @@ class RunTestCommand extends clipanion_1.Command {
|
|
|
142
142
|
reporter.log('No test to run.\n');
|
|
143
143
|
break;
|
|
144
144
|
// Critical command errors
|
|
145
|
+
case 'AUTHORIZATION_ERROR':
|
|
146
|
+
reporter.error(`\n${chalk_1.default.bgRed.bold(' ERROR: authorization error ')}\n${error.message}\n\n`);
|
|
147
|
+
reporter.log('Credentials refused, make sure `apiKey`, `appKey` and `datadogSite` are correct.\n');
|
|
148
|
+
break;
|
|
145
149
|
case 'MISSING_APP_KEY':
|
|
146
150
|
reporter.error(`Missing ${chalk_1.default.red.bold('DATADOG_APP_KEY')} in your environment.\n`);
|
|
147
151
|
break;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
declare const nonCriticalErrorCodes: readonly ["NO_TESTS_TO_RUN", "NO_RESULTS_TO_POLL"];
|
|
2
|
-
declare type NonCriticalCiErrorCode = typeof nonCriticalErrorCodes[number];
|
|
3
|
-
declare const criticalErrorCodes: readonly ["
|
|
4
|
-
declare type CriticalCiErrorCode = typeof criticalErrorCodes[number];
|
|
5
|
-
declare type CiErrorCode = NonCriticalCiErrorCode | CriticalCiErrorCode;
|
|
2
|
+
export declare type NonCriticalCiErrorCode = typeof nonCriticalErrorCodes[number];
|
|
3
|
+
declare const criticalErrorCodes: readonly ["AUTHORIZATION_ERROR", "MISSING_API_KEY", "MISSING_APP_KEY", "POLL_RESULTS_FAILED", "TRIGGER_TESTS_FAILED", "TUNNEL_START_FAILED", "UNAVAILABLE_TEST_CONFIG", "UNAVAILABLE_TUNNEL_CONFIG"];
|
|
4
|
+
export declare type CriticalCiErrorCode = typeof criticalErrorCodes[number];
|
|
5
|
+
export declare type CiErrorCode = NonCriticalCiErrorCode | CriticalCiErrorCode;
|
|
6
6
|
export declare class CiError extends Error {
|
|
7
7
|
code: CiErrorCode;
|
|
8
8
|
constructor(code: CiErrorCode);
|
|
@@ -4,13 +4,14 @@ exports.CriticalError = exports.CiError = void 0;
|
|
|
4
4
|
/* tslint:disable:max-classes-per-file */
|
|
5
5
|
const nonCriticalErrorCodes = ['NO_TESTS_TO_RUN', 'NO_RESULTS_TO_POLL'];
|
|
6
6
|
const criticalErrorCodes = [
|
|
7
|
-
'
|
|
7
|
+
'AUTHORIZATION_ERROR',
|
|
8
8
|
'MISSING_API_KEY',
|
|
9
9
|
'MISSING_APP_KEY',
|
|
10
|
-
'UNAVAILABLE_TUNNEL_CONFIG',
|
|
11
|
-
'TUNNEL_START_FAILED',
|
|
12
|
-
'TRIGGER_TESTS_FAILED',
|
|
13
10
|
'POLL_RESULTS_FAILED',
|
|
11
|
+
'TRIGGER_TESTS_FAILED',
|
|
12
|
+
'TUNNEL_START_FAILED',
|
|
13
|
+
'UNAVAILABLE_TEST_CONFIG',
|
|
14
|
+
'UNAVAILABLE_TUNNEL_CONFIG',
|
|
14
15
|
];
|
|
15
16
|
class CiError extends Error {
|
|
16
17
|
constructor(code) {
|
|
@@ -253,12 +253,9 @@ export interface ConfigOverride {
|
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
255
|
export interface Payload {
|
|
256
|
-
metadata?:
|
|
256
|
+
metadata?: Metadata;
|
|
257
257
|
tests: TestPayload[];
|
|
258
258
|
}
|
|
259
|
-
export declare type SyntheticsMetadata = Metadata & {
|
|
260
|
-
trigger_app: string;
|
|
261
|
-
};
|
|
262
259
|
export interface TestPayload extends ConfigOverride {
|
|
263
260
|
executionRule: ExecutionRule;
|
|
264
261
|
public_id: string;
|
|
@@ -195,7 +195,7 @@ class DefaultReporter {
|
|
|
195
195
|
this.write(error);
|
|
196
196
|
}
|
|
197
197
|
initErrors(errors) {
|
|
198
|
-
this.write(errors.join('\n') + '\n');
|
|
198
|
+
this.write(errors.join('\n') + '\n\n');
|
|
199
199
|
}
|
|
200
200
|
log(log) {
|
|
201
201
|
this.write(log);
|
|
@@ -246,7 +246,7 @@ class DefaultReporter {
|
|
|
246
246
|
testsList.push('…');
|
|
247
247
|
}
|
|
248
248
|
const testsDisplay = chalk_1.default.gray(`(${testsList.join(', ')})`);
|
|
249
|
-
this.write(
|
|
249
|
+
this.write(`Waiting for ${chalk_1.default.bold.cyan(tests.length)} test result${tests.length > 1 ? 's' : ''} ${testsDisplay}…\n`);
|
|
250
250
|
}
|
|
251
251
|
testTrigger(test, testId, executionRule, config) {
|
|
252
252
|
const idDisplay = `[${chalk_1.default.bold.dim(testId)}]`;
|
|
@@ -62,15 +62,5 @@ export declare const getTestsList: (api: APIHelper, config: SyntheticsCIConfig,
|
|
|
62
62
|
id: string;
|
|
63
63
|
suite: string | undefined;
|
|
64
64
|
}[]>;
|
|
65
|
-
export declare const getApiHelper: (config: SyntheticsCIConfig) =>
|
|
66
|
-
getPresignedURL: (testIds: string[]) => Promise<{
|
|
67
|
-
url: string;
|
|
68
|
-
}>;
|
|
69
|
-
getTest: (testId: string) => Promise<Test>;
|
|
70
|
-
pollResults: (resultIds: string[]) => Promise<{
|
|
71
|
-
results: PollResult[];
|
|
72
|
-
}>;
|
|
73
|
-
searchTests: (query: string) => Promise<import("./interfaces").TestSearchResult>;
|
|
74
|
-
triggerTests: (data: import("./interfaces").Payload) => Promise<Trigger>;
|
|
75
|
-
};
|
|
65
|
+
export declare const getApiHelper: (config: SyntheticsCIConfig) => APIHelper;
|
|
76
66
|
export declare const getDatadogHost: (useIntake: boolean | undefined, config: SyntheticsCIConfig) => string;
|
|
@@ -32,7 +32,7 @@ const executeTests = (reporter, config) => __awaiter(void 0, void 0, void 0, fun
|
|
|
32
32
|
testsToTrigger = yield exports.getTestsList(api, config, reporter);
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
|
-
throw new errors_1.CriticalError('UNAVAILABLE_TEST_CONFIG');
|
|
35
|
+
throw new errors_1.CriticalError(api_1.isForbiddenError(error) ? 'AUTHORIZATION_ERROR' : 'UNAVAILABLE_TEST_CONFIG');
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
if (!testsToTrigger.length) {
|
|
@@ -43,7 +43,10 @@ const executeTests = (reporter, config) => __awaiter(void 0, void 0, void 0, fun
|
|
|
43
43
|
testsToTriggerResult = yield utils_1.getTestsToTrigger(api, testsToTrigger, reporter);
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
46
|
-
|
|
46
|
+
if (error instanceof errors_1.CiError) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
throw new errors_1.CriticalError(api_1.isForbiddenError(error) ? 'AUTHORIZATION_ERROR' : 'UNAVAILABLE_TEST_CONFIG');
|
|
47
50
|
}
|
|
48
51
|
const { tests, overriddenTestsToTrigger, summary } = testsToTriggerResult;
|
|
49
52
|
// All tests have been skipped or are missing.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { APIHelper, ConfigOverride, ExecutionRule, InternalTest, MainReporter, PollResult, Reporter, Result, Suite, Summary, TestPayload, Trigger, TriggerConfig, TriggerResponse, TriggerResult } from './interfaces';
|
|
2
2
|
import { Tunnel } from './tunnel';
|
|
3
|
+
export declare let ciTriggerApp: string;
|
|
3
4
|
export declare const handleConfig: (test: InternalTest, publicId: string, reporter: MainReporter, config?: ConfigOverride | undefined) => TestPayload;
|
|
4
5
|
export declare const setCiTriggerApp: (source: string) => void;
|
|
5
6
|
export declare const getExecutionRule: (test: InternalTest, configOverride?: ConfigOverride | undefined) => ExecutionRule;
|