@commercetools-frontend/deployment-cli 0.0.0-FEC-212-react19-20250122084835
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 +21 -0
- package/README.md +60 -0
- package/bin/cli.js +9 -0
- package/cli/dist/commercetools-frontend-deployment-cli-cli.cjs.d.ts +2 -0
- package/cli/dist/commercetools-frontend-deployment-cli-cli.cjs.dev.js +601 -0
- package/cli/dist/commercetools-frontend-deployment-cli-cli.cjs.js +7 -0
- package/cli/dist/commercetools-frontend-deployment-cli-cli.cjs.prod.js +601 -0
- package/cli/dist/commercetools-frontend-deployment-cli-cli.esm.js +581 -0
- package/cli/package.json +4 -0
- package/dist/commercetools-frontend-deployment-cli.cjs.d.ts +2 -0
- package/dist/commercetools-frontend-deployment-cli.cjs.dev.js +2 -0
- package/dist/commercetools-frontend-deployment-cli.cjs.js +7 -0
- package/dist/commercetools-frontend-deployment-cli.cjs.prod.js +2 -0
- package/dist/commercetools-frontend-deployment-cli.esm.js +1 -0
- package/dist/declarations/src/apis.d.ts +76 -0
- package/dist/declarations/src/cli.d.ts +2 -0
- package/dist/declarations/src/helpers.d.ts +66 -0
- package/dist/declarations/src/index.d.ts +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var cac = require('cac');
|
|
6
|
+
var _findInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/find');
|
|
7
|
+
var _forEachInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/for-each');
|
|
8
|
+
var _mapInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/map');
|
|
9
|
+
var _filterInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/filter');
|
|
10
|
+
var _startsWithInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/starts-with');
|
|
11
|
+
var _concatInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/concat');
|
|
12
|
+
var cosmiconfig = require('cosmiconfig');
|
|
13
|
+
var merge = require('lodash/merge');
|
|
14
|
+
var prompts = require('prompts');
|
|
15
|
+
var pRetry = require('p-retry');
|
|
16
|
+
var _slicedToArray = require('@babel/runtime-corejs3/helpers/slicedToArray');
|
|
17
|
+
var _URLSearchParams = require('@babel/runtime-corejs3/core-js-stable/url-search-params');
|
|
18
|
+
var _Object$entries = require('@babel/runtime-corejs3/core-js-stable/object/entries');
|
|
19
|
+
var _Promise = require('@babel/runtime-corejs3/core-js-stable/promise');
|
|
20
|
+
var _JSON$stringify = require('@babel/runtime-corejs3/core-js-stable/json/stringify');
|
|
21
|
+
var node_process = require('node:process');
|
|
22
|
+
|
|
23
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
|
|
24
|
+
|
|
25
|
+
var _findInstanceProperty__default = /*#__PURE__*/_interopDefault(_findInstanceProperty);
|
|
26
|
+
var _forEachInstanceProperty__default = /*#__PURE__*/_interopDefault(_forEachInstanceProperty);
|
|
27
|
+
var _mapInstanceProperty__default = /*#__PURE__*/_interopDefault(_mapInstanceProperty);
|
|
28
|
+
var _filterInstanceProperty__default = /*#__PURE__*/_interopDefault(_filterInstanceProperty);
|
|
29
|
+
var _startsWithInstanceProperty__default = /*#__PURE__*/_interopDefault(_startsWithInstanceProperty);
|
|
30
|
+
var _concatInstanceProperty__default = /*#__PURE__*/_interopDefault(_concatInstanceProperty);
|
|
31
|
+
var merge__default = /*#__PURE__*/_interopDefault(merge);
|
|
32
|
+
var prompts__default = /*#__PURE__*/_interopDefault(prompts);
|
|
33
|
+
var pRetry__default = /*#__PURE__*/_interopDefault(pRetry);
|
|
34
|
+
var _URLSearchParams__default = /*#__PURE__*/_interopDefault(_URLSearchParams);
|
|
35
|
+
var _Object$entries__default = /*#__PURE__*/_interopDefault(_Object$entries);
|
|
36
|
+
var _Promise__default = /*#__PURE__*/_interopDefault(_Promise);
|
|
37
|
+
var _JSON$stringify__default = /*#__PURE__*/_interopDefault(_JSON$stringify);
|
|
38
|
+
|
|
39
|
+
var pkgJson = {
|
|
40
|
+
name: "@commercetools-frontend/deployment-cli",
|
|
41
|
+
version: "2.0.2",
|
|
42
|
+
description: "CLI to manage Custom Applications deployments in Google Storage.",
|
|
43
|
+
keywords: [
|
|
44
|
+
"commercetools",
|
|
45
|
+
"cli",
|
|
46
|
+
"deployment"
|
|
47
|
+
],
|
|
48
|
+
license: "MIT",
|
|
49
|
+
main: "dist/commercetools-frontend-deployment-cli.cjs.js",
|
|
50
|
+
module: "dist/commercetools-frontend-deployment-cli.esm.js",
|
|
51
|
+
bin: "bin/cli.js",
|
|
52
|
+
files: [
|
|
53
|
+
"bin",
|
|
54
|
+
"cli",
|
|
55
|
+
"dist",
|
|
56
|
+
"package.json",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"README.md"
|
|
59
|
+
],
|
|
60
|
+
scripts: {
|
|
61
|
+
typecheck: "tsc --noEmit"
|
|
62
|
+
},
|
|
63
|
+
dependencies: {
|
|
64
|
+
"@babel/core": "^7.22.11",
|
|
65
|
+
"@babel/runtime-corejs3": "^7.21.0",
|
|
66
|
+
cac: "^6.7.14",
|
|
67
|
+
cosmiconfig: "9.0.0",
|
|
68
|
+
lodash: "4.17.21",
|
|
69
|
+
"p-retry": "4.6.2",
|
|
70
|
+
prompts: "2.4.2"
|
|
71
|
+
},
|
|
72
|
+
devDependencies: {
|
|
73
|
+
"@tsconfig/node20": "20.1.4",
|
|
74
|
+
"@types/lodash": "^4.14.198",
|
|
75
|
+
"@types/node": "20.17.13",
|
|
76
|
+
"@types/prompts": "2.4.9",
|
|
77
|
+
msw: "1.3.5",
|
|
78
|
+
typescript: "5.2.2"
|
|
79
|
+
},
|
|
80
|
+
engines: {
|
|
81
|
+
node: ">=21",
|
|
82
|
+
npm: ">=6"
|
|
83
|
+
},
|
|
84
|
+
publishConfig: {
|
|
85
|
+
access: "public"
|
|
86
|
+
},
|
|
87
|
+
preconstruct: {
|
|
88
|
+
entrypoints: [
|
|
89
|
+
"./cli.ts",
|
|
90
|
+
"./index.ts"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
async function loadConfig() {
|
|
96
|
+
const deploymentConfigExplorer = cosmiconfig.cosmiconfig('deployment', {
|
|
97
|
+
searchStrategy: 'project'
|
|
98
|
+
});
|
|
99
|
+
const defaultConfig = {
|
|
100
|
+
CircleCI: {
|
|
101
|
+
apiBaseUrl: 'https://circleci.com/api/v2',
|
|
102
|
+
deploymentWorkflowName: 'test_build_and_deploy',
|
|
103
|
+
pagination: {
|
|
104
|
+
maxPages: 42,
|
|
105
|
+
pagesForPipelineSelection: 5
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
MerchantCenter: {}
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const cosmiconfigResult = await deploymentConfigExplorer.search();
|
|
112
|
+
const mergedConfig = merge__default["default"](defaultConfig, cosmiconfigResult?.config);
|
|
113
|
+
return mergedConfig;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.warn(e);
|
|
116
|
+
throw new Error('Failed loading a deployment configuration. Create a cosmiconfig for `deployment` for example `deployment.config.cjs`.');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function throwIfConfigurationLacksRequiredValues(parsedConfiguration) {
|
|
120
|
+
if (!parsedConfiguration.CircleCI.projectName) {
|
|
121
|
+
throw new Error(`Missing 'projectName' in 'CircleCI' on configuration. Make sure it exists!`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function throwIfRequiredEnvironmentVariableIsUnset(requiredEnvironmentVariables) {
|
|
125
|
+
_forEachInstanceProperty__default["default"](requiredEnvironmentVariables).call(requiredEnvironmentVariables, nameOfRequiredEnvironmentVariable => {
|
|
126
|
+
const valueOfRequiredEnvironmentVariable = process.env[nameOfRequiredEnvironmentVariable];
|
|
127
|
+
if (!valueOfRequiredEnvironmentVariable) throw new Error(`Missing '${nameOfRequiredEnvironmentVariable}' environment variable`);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const promptOptions = {
|
|
131
|
+
applicationSelect: _ref => {
|
|
132
|
+
let packages = _ref.packages;
|
|
133
|
+
return {
|
|
134
|
+
type: 'select',
|
|
135
|
+
name: 'applicationName',
|
|
136
|
+
message: 'Select an application',
|
|
137
|
+
choices: _mapInstanceProperty__default["default"](packages).call(packages, _ref2 => {
|
|
138
|
+
let packageJson = _ref2.packageJson;
|
|
139
|
+
const applicationName = packageJson.name.replace('@commercetools-local/application-', '');
|
|
140
|
+
return {
|
|
141
|
+
title: applicationName,
|
|
142
|
+
value: applicationName
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
deploymentPipelineSelect: _ref3 => {
|
|
148
|
+
let deploymentPipelines = _ref3.deploymentPipelines;
|
|
149
|
+
return {
|
|
150
|
+
type: 'select',
|
|
151
|
+
name: 'deploymentPipeline',
|
|
152
|
+
message: 'Select the revision you would like to deploy',
|
|
153
|
+
choices: _mapInstanceProperty__default["default"](deploymentPipelines).call(deploymentPipelines, deploymentPipeline => ({
|
|
154
|
+
title: `${deploymentPipeline.vcs.revision.substring(0, 7)} - ${deploymentPipeline.vcs.commit.subject} <${deploymentPipeline.trigger.actor.login}>`,
|
|
155
|
+
value: deploymentPipeline
|
|
156
|
+
}))
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
deploymentConfirmation: _ref4 => {
|
|
160
|
+
let approvalJob = _ref4.approvalJob,
|
|
161
|
+
revision = _ref4.revision;
|
|
162
|
+
return {
|
|
163
|
+
type: 'toggle',
|
|
164
|
+
name: 'confirmed',
|
|
165
|
+
message: `Are you sure you want to deploy by approving ${approvalJob} at ${revision}?`,
|
|
166
|
+
initial: false,
|
|
167
|
+
active: 'Yes',
|
|
168
|
+
inactive: 'No'
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
async function paginateToDeploymentPipeline(_ref5) {
|
|
173
|
+
let circleCiApis = _ref5.circleCiApis,
|
|
174
|
+
buildRevision = _ref5.buildRevision,
|
|
175
|
+
branch = _ref5.branch,
|
|
176
|
+
debug = _ref5.debug,
|
|
177
|
+
maxPages = _ref5.maxPages;
|
|
178
|
+
let deploymentPipeline;
|
|
179
|
+
let nextPageToken;
|
|
180
|
+
// eslint-disable-next-line no-plusplus
|
|
181
|
+
for (let i = 0; i < maxPages; i++) {
|
|
182
|
+
var _context, _context2;
|
|
183
|
+
// eslint-disable-next-line no-await-in-loop
|
|
184
|
+
|
|
185
|
+
const pipelineRequest = circleCiApis.pipelines({
|
|
186
|
+
pageToken: nextPageToken,
|
|
187
|
+
branch
|
|
188
|
+
});
|
|
189
|
+
const pipelines = await pipelineRequest.execute({
|
|
190
|
+
debug
|
|
191
|
+
});
|
|
192
|
+
nextPageToken = pipelines.next_page_token;
|
|
193
|
+
const nonScheduledPipelines = _filterInstanceProperty__default["default"](_context = _filterInstanceProperty__default["default"](_context2 = pipelines.items).call(_context2, isNonScheduledPipeline)).call(_context, isNonErroredPipeline);
|
|
194
|
+
if (buildRevision) {
|
|
195
|
+
console.log(`🔄 Trying to find pipeline with revision ${buildRevision}. Attempt ${i + 1} out of ${maxPages}.`);
|
|
196
|
+
deploymentPipeline = _findInstanceProperty__default["default"](nonScheduledPipelines).call(nonScheduledPipelines, pipeline => {
|
|
197
|
+
var _context3;
|
|
198
|
+
return _startsWithInstanceProperty__default["default"](_context3 = pipeline.vcs.revision).call(_context3, buildRevision);
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
deploymentPipeline = nonScheduledPipelines[0];
|
|
202
|
+
}
|
|
203
|
+
if (deploymentPipeline) {
|
|
204
|
+
console.log(`ℹ️ Using pipeline for deployment with revision ${deploymentPipeline.vcs.revision}.`);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return deploymentPipeline;
|
|
209
|
+
}
|
|
210
|
+
async function collectDeploymentPipelines(_ref6) {
|
|
211
|
+
let circleCiApis = _ref6.circleCiApis,
|
|
212
|
+
branch = _ref6.branch,
|
|
213
|
+
pagesForPipelineSelection = _ref6.pagesForPipelineSelection,
|
|
214
|
+
debug = _ref6.debug;
|
|
215
|
+
let deploymentPipelines = [];
|
|
216
|
+
let nextPageToken;
|
|
217
|
+
// eslint-disable-next-line no-plusplus
|
|
218
|
+
for (let i = 0; i < pagesForPipelineSelection; i++) {
|
|
219
|
+
var _context4, _context5;
|
|
220
|
+
// eslint-disable-next-line no-await-in-loop
|
|
221
|
+
const pipelinesRequest = circleCiApis.pipelines({
|
|
222
|
+
pageToken: nextPageToken,
|
|
223
|
+
branch
|
|
224
|
+
});
|
|
225
|
+
const pipelines = await pipelinesRequest.execute({
|
|
226
|
+
debug
|
|
227
|
+
});
|
|
228
|
+
const nonScheduledPipelines = _filterInstanceProperty__default["default"](_context4 = _filterInstanceProperty__default["default"](_context5 = pipelines.items).call(_context5, isNonScheduledPipeline)).call(_context4, isNonErroredPipeline);
|
|
229
|
+
deploymentPipelines = _concatInstanceProperty__default["default"](deploymentPipelines).call(deploymentPipelines, nonScheduledPipelines);
|
|
230
|
+
}
|
|
231
|
+
return deploymentPipelines;
|
|
232
|
+
}
|
|
233
|
+
async function waitForDeploymentPipelinePrompt(_ref7) {
|
|
234
|
+
let circleCiApis = _ref7.circleCiApis,
|
|
235
|
+
branch = _ref7.branch,
|
|
236
|
+
pagesForPipelineSelection = _ref7.pagesForPipelineSelection,
|
|
237
|
+
debug = _ref7.debug;
|
|
238
|
+
const deploymentPipelines = await collectDeploymentPipelines({
|
|
239
|
+
branch,
|
|
240
|
+
pagesForPipelineSelection,
|
|
241
|
+
circleCiApis,
|
|
242
|
+
debug
|
|
243
|
+
});
|
|
244
|
+
const deploymentPipelinePrompt = await prompts__default["default"](
|
|
245
|
+
// @ts-expect-error prompts is not typed
|
|
246
|
+
promptOptions.deploymentPipelineSelect({
|
|
247
|
+
deploymentPipelines
|
|
248
|
+
}));
|
|
249
|
+
const revision = deploymentPipelinePrompt.deploymentPipeline.vcs.revision;
|
|
250
|
+
if (!revision) {
|
|
251
|
+
console.log('☝️ Please select a revision or specify it as a CLI argument via `--build-revision`.');
|
|
252
|
+
throw new Error('No revision specified.');
|
|
253
|
+
}
|
|
254
|
+
return deploymentPipelinePrompt.deploymentPipeline;
|
|
255
|
+
}
|
|
256
|
+
async function waitForConfirmationPrompt(_ref8) {
|
|
257
|
+
let approvalJob = _ref8.approvalJob,
|
|
258
|
+
revision = _ref8.revision;
|
|
259
|
+
return prompts__default["default"](
|
|
260
|
+
// @ts-expect-error prompts is not typed
|
|
261
|
+
promptOptions.deploymentConfirmation({
|
|
262
|
+
approvalJob,
|
|
263
|
+
revision
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
function getJobUrl(_ref9) {
|
|
267
|
+
let pipelineNumber = _ref9.pipelineNumber,
|
|
268
|
+
workflowId = _ref9.workflowId,
|
|
269
|
+
jobNumber = _ref9.jobNumber,
|
|
270
|
+
projectName = _ref9.projectName;
|
|
271
|
+
return `https://app.circleci.com/pipelines/github/commercetools/${projectName}/${pipelineNumber}/workflows/${workflowId}/jobs/${jobNumber}`;
|
|
272
|
+
}
|
|
273
|
+
function isNonScheduledPipeline(pipeline) {
|
|
274
|
+
return pipeline.trigger.type !== 'schedule' && pipeline.trigger.type !== 'scheduled_pipeline';
|
|
275
|
+
}
|
|
276
|
+
function isNonErroredPipeline(pipeline) {
|
|
277
|
+
return pipeline.state !== 'errored';
|
|
278
|
+
}
|
|
279
|
+
async function waitForDeploymentJobNumber(_ref10, _ref11) {
|
|
280
|
+
let workflowId = _ref10.workflowId,
|
|
281
|
+
deploymentJob = _ref10.deploymentJob,
|
|
282
|
+
circleCiApis = _ref10.circleCiApis;
|
|
283
|
+
let debug = _ref11.debug;
|
|
284
|
+
const fetchDeploymentJobNumber = async () => {
|
|
285
|
+
var _context6;
|
|
286
|
+
const jobsRequest = circleCiApis.jobs({
|
|
287
|
+
workflowId
|
|
288
|
+
});
|
|
289
|
+
const jobs = await jobsRequest.execute({
|
|
290
|
+
debug
|
|
291
|
+
});
|
|
292
|
+
const applicationDeploymentJob = _findInstanceProperty__default["default"](_context6 = jobs.items).call(_context6, job => job.name === deploymentJob);
|
|
293
|
+
if (!applicationDeploymentJob || applicationDeploymentJob.status === 'blocked') {
|
|
294
|
+
throw new Error('Deployment job not yet running. Retrying.');
|
|
295
|
+
}
|
|
296
|
+
return applicationDeploymentJob.job_number;
|
|
297
|
+
};
|
|
298
|
+
const deploymentJobNumber = await pRetry__default["default"](fetchDeploymentJobNumber, {
|
|
299
|
+
onFailedAttempt: error => {
|
|
300
|
+
console.log(`🔄 Trying to find deployment job. Attempt ${error.attemptNumber} with ${error.retriesLeft} retries left.`);
|
|
301
|
+
},
|
|
302
|
+
retries: 10
|
|
303
|
+
});
|
|
304
|
+
return deploymentJobNumber;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function approve(cliFlags, config, circleCiApis) {
|
|
308
|
+
var _context, _context2;
|
|
309
|
+
let approvalJob;
|
|
310
|
+
let deploymentJob;
|
|
311
|
+
if (cliFlags.deployment) {
|
|
312
|
+
const requestedDeployment = config.deployments?.[cliFlags.deployment];
|
|
313
|
+
if (!requestedDeployment) {
|
|
314
|
+
throw new Error(`⚠️ Deployment ${cliFlags.deployment} not found in configuration. Make sure it exists.`);
|
|
315
|
+
}
|
|
316
|
+
approvalJob = requestedDeployment.jobs.approval;
|
|
317
|
+
deploymentJob = requestedDeployment.jobs.deployment;
|
|
318
|
+
console.log(`ℹ️ Approving requested deployment ${cliFlags.deployment} with approval job ${approvalJob}.`);
|
|
319
|
+
} else {
|
|
320
|
+
approvalJob = cliFlags.approvalJob;
|
|
321
|
+
deploymentJob = cliFlags.deploymentJob;
|
|
322
|
+
console.log(`ℹ️ Approving with approval job ${approvalJob}.`);
|
|
323
|
+
}
|
|
324
|
+
const deploymentPipeline = cliFlags.yes ? await paginateToDeploymentPipeline({
|
|
325
|
+
circleCiApis,
|
|
326
|
+
buildRevision: cliFlags.buildRevision,
|
|
327
|
+
branch: cliFlags.branch,
|
|
328
|
+
debug: cliFlags.debug,
|
|
329
|
+
maxPages: config.CircleCI.pagination?.maxPages ?? 1
|
|
330
|
+
}) : await waitForDeploymentPipelinePrompt({
|
|
331
|
+
branch: cliFlags.branch,
|
|
332
|
+
circleCiApis,
|
|
333
|
+
pagesForPipelineSelection: config.CircleCI.pagination?.pagesForPipelineSelection ?? 1,
|
|
334
|
+
debug: cliFlags.debug
|
|
335
|
+
});
|
|
336
|
+
if (!deploymentPipeline) {
|
|
337
|
+
throw new Error(`⚠️ No workflow called ${config.CircleCI.deploymentWorkflowName} found in any pipeline for deployment.`);
|
|
338
|
+
}
|
|
339
|
+
console.log(`ℹ️ Found pipeline for deployment with revision ${deploymentPipeline.vcs.revision}.`);
|
|
340
|
+
const workflowsRequest = circleCiApis.workflows({
|
|
341
|
+
pipelineId: deploymentPipeline.id
|
|
342
|
+
});
|
|
343
|
+
const workflows = await workflowsRequest.execute({
|
|
344
|
+
debug: cliFlags.debug
|
|
345
|
+
});
|
|
346
|
+
const buildAndDeployWorkflow = _findInstanceProperty__default["default"](_context = workflows.items).call(_context, workflow => workflow.name === config.CircleCI.deploymentWorkflowName);
|
|
347
|
+
if (!buildAndDeployWorkflow) {
|
|
348
|
+
throw new Error(`⚠️ No workflow called ${config.CircleCI.deploymentWorkflowName} found for deployment.`);
|
|
349
|
+
}
|
|
350
|
+
const workflowId = buildAndDeployWorkflow.id;
|
|
351
|
+
console.log(`ℹ️ Found workflow to build and deploy with id ${workflowId}.`);
|
|
352
|
+
const jobsRequest = circleCiApis.jobs({
|
|
353
|
+
workflowId
|
|
354
|
+
});
|
|
355
|
+
const jobs = await jobsRequest.execute({
|
|
356
|
+
debug: cliFlags.debug
|
|
357
|
+
});
|
|
358
|
+
console.log(`ℹ️ Found jobs for workflow.`);
|
|
359
|
+
const applicationApprovalJob = _findInstanceProperty__default["default"](_context2 = jobs.items).call(_context2, job => job.name === approvalJob);
|
|
360
|
+
const applicationApprovalJobId = applicationApprovalJob?.id;
|
|
361
|
+
if (!applicationApprovalJobId) {
|
|
362
|
+
throw new Error(`✌️ Could not find deployment approval job named ${approvalJob} at revision ${deploymentPipeline.vcs.revision}. Maybe try again later.`);
|
|
363
|
+
}
|
|
364
|
+
console.log(`ℹ️ Found deployment approval job named ${approvalJob} with id ${applicationApprovalJobId}.`);
|
|
365
|
+
let confirmationPrompt;
|
|
366
|
+
if (!cliFlags.yes) {
|
|
367
|
+
confirmationPrompt = await waitForConfirmationPrompt({
|
|
368
|
+
approvalJob: approvalJob,
|
|
369
|
+
revision: deploymentPipeline.vcs.revision
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (cliFlags.yes || confirmationPrompt?.confirmed) {
|
|
373
|
+
if (cliFlags.dryRun) {
|
|
374
|
+
console.log(`🙊 Not approving deployment job due to dry run.`);
|
|
375
|
+
} else {
|
|
376
|
+
console.log(`ℹ️ Approving deployment job.`);
|
|
377
|
+
}
|
|
378
|
+
const approvalRequest = circleCiApis.approve({
|
|
379
|
+
workflowId,
|
|
380
|
+
approvalRequestId: applicationApprovalJobId
|
|
381
|
+
});
|
|
382
|
+
await approvalRequest.execute({
|
|
383
|
+
debug: cliFlags.debug,
|
|
384
|
+
skip: cliFlags.dryRun
|
|
385
|
+
});
|
|
386
|
+
if (cliFlags.yes) {
|
|
387
|
+
console.log(`ℹ️ Skipping determining deployment job approved by ${approvalJob} due to '--yes' flag. Please check CircleCI manually.`);
|
|
388
|
+
} else if (cliFlags.dryRun) {
|
|
389
|
+
console.log(`✌️ Dry running hence could not determine deployment job approved by ${approvalJob}.`);
|
|
390
|
+
} else if (deploymentJob) {
|
|
391
|
+
const deploymentJobNumber = await waitForDeploymentJobNumber({
|
|
392
|
+
workflowId,
|
|
393
|
+
deploymentJob,
|
|
394
|
+
circleCiApis
|
|
395
|
+
}, {
|
|
396
|
+
debug: cliFlags.debug,
|
|
397
|
+
dryRun: cliFlags.dryRun,
|
|
398
|
+
yes: cliFlags.yes
|
|
399
|
+
});
|
|
400
|
+
console.log(`🙌 The deployment via ${approvalJob} at revision ${deploymentPipeline.vcs.revision} is running at: ${getJobUrl({
|
|
401
|
+
projectName: config.CircleCI.projectName,
|
|
402
|
+
pipelineNumber: deploymentPipeline.number,
|
|
403
|
+
workflowId,
|
|
404
|
+
jobNumber: deploymentJobNumber
|
|
405
|
+
})}`);
|
|
406
|
+
} else {
|
|
407
|
+
console.log(`ℹ️ Skipping determining deployment job as no '--deployment-job' name to wait for was passed. Please check CircleCI manually.`);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
console.log(`ℹ️ Not approving deployment job. Confirm the prompt or use '--yes' option.`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function processCircleCiResponse(response) {
|
|
415
|
+
if (!response.ok) {
|
|
416
|
+
/**
|
|
417
|
+
* NOTE:
|
|
418
|
+
* Trying to handle known but undocumented responses of the CircleCI API.
|
|
419
|
+
*
|
|
420
|
+
* 1. Message: Already approved job
|
|
421
|
+
* Deployment was already triggered manually or by train the day before.
|
|
422
|
+
*/
|
|
423
|
+
|
|
424
|
+
// Response data is a stream so text OR json can only be read once, so
|
|
425
|
+
// we read it as text first and then try to parse it as json to handle
|
|
426
|
+
// known error responses.
|
|
427
|
+
const error = await response.text();
|
|
428
|
+
try {
|
|
429
|
+
// The CircleCI API always uses content-type text/plain, so we always
|
|
430
|
+
// try parsing JSON to see if the message property is present.
|
|
431
|
+
const _JSON$parse = JSON.parse(error),
|
|
432
|
+
message = _JSON$parse.message;
|
|
433
|
+
if (message.match(/job already approved/i)) {
|
|
434
|
+
console.log('ℹ️ Deployment job is already approved.');
|
|
435
|
+
// TODO: can we return instead of force exiting?
|
|
436
|
+
node_process.exit(0);
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
// Ignore JSON parsing errors
|
|
440
|
+
}
|
|
441
|
+
throw new Error(`${response.status}: Network response was not ok.\n
|
|
442
|
+
Status text is ${response.statusText} and text is ${error}.`);
|
|
443
|
+
}
|
|
444
|
+
return response.json();
|
|
445
|
+
}
|
|
446
|
+
function createCircleCiClient(_ref) {
|
|
447
|
+
let projectName = _ref.projectName,
|
|
448
|
+
apiBaseUrl = _ref.apiBaseUrl;
|
|
449
|
+
async function execute(api) {
|
|
450
|
+
let _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
|
|
451
|
+
skip = _ref2.skip,
|
|
452
|
+
debug = _ref2.debug;
|
|
453
|
+
let url = `${apiBaseUrl}${api.url}`;
|
|
454
|
+
if (api.params && api.method === 'GET') {
|
|
455
|
+
const urlSearchParams = new _URLSearchParams__default["default"]();
|
|
456
|
+
for (const _ref3 of _Object$entries__default["default"](api.params)) {
|
|
457
|
+
var _ref4 = _slicedToArray(_ref3, 2);
|
|
458
|
+
const key = _ref4[0];
|
|
459
|
+
const value = _ref4[1];
|
|
460
|
+
if (value !== null && value !== undefined) {
|
|
461
|
+
urlSearchParams.append(key, value);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
url += `?${urlSearchParams.toString()}`;
|
|
465
|
+
}
|
|
466
|
+
if (skip) {
|
|
467
|
+
if (debug) {
|
|
468
|
+
console.log(`🏭 Skipping CircleCI call API at: ${url}.`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// @ts-expect-error
|
|
472
|
+
return _Promise__default["default"].resolve();
|
|
473
|
+
}
|
|
474
|
+
if (debug) {
|
|
475
|
+
console.log(`🏭 Calling CircleCI API at: ${url}.`);
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const response = await fetch(url, {
|
|
479
|
+
headers: api.headers,
|
|
480
|
+
method: api.method,
|
|
481
|
+
body: api.method === 'POST' ? _JSON$stringify__default["default"](api.params) : undefined
|
|
482
|
+
});
|
|
483
|
+
const processedCircleCiResponse = await processCircleCiResponse(response);
|
|
484
|
+
return processedCircleCiResponse;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.log(`⚠️ Calling CircleCI API at: ${url} failed.`);
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
pipelines: function () {
|
|
492
|
+
let _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
|
493
|
+
pageToken = _ref5.pageToken,
|
|
494
|
+
_ref5$projectSlug = _ref5.projectSlug,
|
|
495
|
+
projectSlug = _ref5$projectSlug === void 0 ? `gh/commercetools/${projectName}` : _ref5$projectSlug,
|
|
496
|
+
_ref5$branch = _ref5.branch,
|
|
497
|
+
branch = _ref5$branch === void 0 ? 'main' : _ref5$branch;
|
|
498
|
+
return {
|
|
499
|
+
execute: options => execute({
|
|
500
|
+
url: `/project/${projectSlug}/pipeline`,
|
|
501
|
+
headers: {
|
|
502
|
+
'Content-Type': 'application/json',
|
|
503
|
+
// The CLI throws if this is not present on environment
|
|
504
|
+
'Circle-Token': process.env.CIRCLE_TOKEN
|
|
505
|
+
},
|
|
506
|
+
method: 'GET',
|
|
507
|
+
params: {
|
|
508
|
+
branch,
|
|
509
|
+
'page-token': pageToken
|
|
510
|
+
}
|
|
511
|
+
}, options)
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
workflows: _ref6 => {
|
|
515
|
+
let pipelineId = _ref6.pipelineId;
|
|
516
|
+
return {
|
|
517
|
+
execute: options => execute({
|
|
518
|
+
url: `/pipeline/${pipelineId}/workflow`,
|
|
519
|
+
headers: {
|
|
520
|
+
'Content-Type': 'application/json',
|
|
521
|
+
// The CLI throws if this is not present on environment
|
|
522
|
+
'Circle-Token': process.env.CIRCLE_TOKEN
|
|
523
|
+
},
|
|
524
|
+
method: 'GET'
|
|
525
|
+
}, options)
|
|
526
|
+
};
|
|
527
|
+
},
|
|
528
|
+
jobs: _ref7 => {
|
|
529
|
+
let workflowId = _ref7.workflowId;
|
|
530
|
+
return {
|
|
531
|
+
execute: options => execute({
|
|
532
|
+
url: `/workflow/${workflowId}/job`,
|
|
533
|
+
headers: {
|
|
534
|
+
'Content-Type': 'application/json',
|
|
535
|
+
// The CLI throws if this is not present on environment
|
|
536
|
+
'Circle-Token': process.env.CIRCLE_TOKEN
|
|
537
|
+
},
|
|
538
|
+
method: 'GET'
|
|
539
|
+
}, options)
|
|
540
|
+
};
|
|
541
|
+
},
|
|
542
|
+
approve: _ref8 => {
|
|
543
|
+
let workflowId = _ref8.workflowId,
|
|
544
|
+
approvalRequestId = _ref8.approvalRequestId;
|
|
545
|
+
return {
|
|
546
|
+
execute: options => execute({
|
|
547
|
+
url: `/workflow/${workflowId}/approve/${approvalRequestId} `,
|
|
548
|
+
headers: {
|
|
549
|
+
'Content-Type': 'application/json',
|
|
550
|
+
// The CLI throws if this is not present on environment
|
|
551
|
+
'Circle-Token': process.env.CIRCLE_TOKEN
|
|
552
|
+
},
|
|
553
|
+
method: 'POST'
|
|
554
|
+
}, options)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const cli = cac.cac('deployment-cli');
|
|
561
|
+
async function run() {
|
|
562
|
+
const config = await loadConfig();
|
|
563
|
+
throwIfConfigurationLacksRequiredValues(config);
|
|
564
|
+
const circleCiApis = createCircleCiClient({
|
|
565
|
+
projectName: config.CircleCI.projectName,
|
|
566
|
+
apiBaseUrl: config.CircleCI.apiBaseUrl
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// General CLI options
|
|
570
|
+
cli.option('--dry-run', '(optional) Simulate a deployment.', {
|
|
571
|
+
default: false
|
|
572
|
+
});
|
|
573
|
+
cli.option('--debug', '(optional) Print additional debug information.', {
|
|
574
|
+
default: false
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Default command
|
|
578
|
+
cli.command('').usage('\n\n Approve deployment jobs on CI for different components related to MC.').action(cli.outputHelp);
|
|
579
|
+
|
|
580
|
+
// Command: Approve
|
|
581
|
+
const usageApprove = 'Approves a job and by this triggers a deployment of a component. It requires a "CIRCLE_TOKEN" environment variable (https://circleci.com/docs/2.0/managing-api-tokens/).';
|
|
582
|
+
cli.command('approve', usageApprove).usage(`approve \n\n ${usageApprove}`).option('--approval-job <string>', 'The name of the approval job to approve a deployment with.').option('--deployment-job [string]', '(optional) The name of the deployment job triggered by the approval job. If passed the CLI will print a URL to the deployment triggered on CircleCI.').option('--deployment [string]', '(optional) The name of a deployment configured in the configuration file.').option('--build-revision [git-sha]', '(optional) The git commit SHA that needs to be deployed. If not specified, the last successful pipeline is used.').option('--branch [string]', '(optional) The git branch to deploy from. If not specified.', {
|
|
583
|
+
default: 'main'
|
|
584
|
+
}).option('--yes', '(optional) Skip all confirmation prompts. Useful in Continuous integration (CI) to automatically answer confirmation questions.', {
|
|
585
|
+
default: false
|
|
586
|
+
}).action(async options => {
|
|
587
|
+
if (options.dryRun) {
|
|
588
|
+
console.log(`🙊 Do not worry. This is a dry run!`);
|
|
589
|
+
}
|
|
590
|
+
throwIfRequiredEnvironmentVariableIsUnset(['CIRCLE_TOKEN']);
|
|
591
|
+
await approve(options, config, circleCiApis);
|
|
592
|
+
});
|
|
593
|
+
cli.help();
|
|
594
|
+
cli.version(pkgJson.version);
|
|
595
|
+
cli.parse(process.argv, {
|
|
596
|
+
run: false
|
|
597
|
+
});
|
|
598
|
+
await cli.runMatchedCommand();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
exports.run = run;
|