@prodgator/report 1.9.1 → 1.10.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/README.md +2 -0
- package/dist/cli.js +103 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ Options can be set as a CLI flag, a `PRODGATOR_`-prefixed environment variable,
|
|
|
22
22
|
| attestations | `--attestations` | `PRODGATOR_ATTESTATIONS` | `ATTESTATIONS` |
|
|
23
23
|
| redactVariables | `--redact-variables` | `PRODGATOR_REDACT_VARIABLES` | `REDACT_VARIABLES` |
|
|
24
24
|
| failOnError | `--fail-on-error` | `PRODGATOR_FAIL_ON_ERROR` | `FAIL_ON_ERROR` |
|
|
25
|
+
| gate | `--gate` | `PRODGATOR_GATE` | `GATE` |
|
|
26
|
+
| gateTimeoutMinutes | `--gate-timeout` | `PRODGATOR_GATE_TIMEOUT` | `GATE_TIMEOUT` |
|
|
25
27
|
| tokenVariable | `--token-variable` | `PRODGATOR_TOKEN_VARIABLE` | (none) |
|
|
26
28
|
|
|
27
29
|
GitLab CI example, reporting from its own short job:
|
package/dist/cli.js
CHANGED
|
@@ -5092,6 +5092,25 @@ async function uploadFile(slot, file, attempts = 2) {
|
|
|
5092
5092
|
}
|
|
5093
5093
|
}
|
|
5094
5094
|
|
|
5095
|
+
// src/core/gate.ts
|
|
5096
|
+
var openGate = (baseUrl, audience, deps) => postJson(`${baseUrl}/api/ingest/bitbucket/gates`, {}, audience, deps);
|
|
5097
|
+
async function readGate(baseUrl, gate, deps) {
|
|
5098
|
+
let res;
|
|
5099
|
+
try {
|
|
5100
|
+
res = await (deps.fetch ?? fetch)(`${baseUrl}/api/ingest/bitbucket/gates/${encodeURIComponent(gate.gateId)}`, {
|
|
5101
|
+
method: "GET",
|
|
5102
|
+
headers: { authorization: `Bearer ${gate.pollToken}`, "user-agent": deps.userAgent ?? "prodgator-report/1" },
|
|
5103
|
+
signal: AbortSignal.timeout(3e4)
|
|
5104
|
+
});
|
|
5105
|
+
} catch (e) {
|
|
5106
|
+
throw new ReportError(0, "NETWORK", e.message, true);
|
|
5107
|
+
}
|
|
5108
|
+
if (res.ok) return await res.json();
|
|
5109
|
+
const payload2 = await res.json().catch(() => ({}));
|
|
5110
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
5111
|
+
throw new ReportError(res.status, payload2.code ?? `HTTP_${res.status}`, payload2.error ?? res.statusText, retryable);
|
|
5112
|
+
}
|
|
5113
|
+
|
|
5095
5114
|
// src/cli/env.ts
|
|
5096
5115
|
var CliError = class extends Error {
|
|
5097
5116
|
constructor(message) {
|
|
@@ -5138,8 +5157,18 @@ var OPTION_NAMES = {
|
|
|
5138
5157
|
attestations: { flag: "--attestations", env: "PRODGATOR_ATTESTATIONS", pipe: "ATTESTATIONS" },
|
|
5139
5158
|
redactVariables: { flag: "--redact-variables", env: "PRODGATOR_REDACT_VARIABLES", pipe: "REDACT_VARIABLES" },
|
|
5140
5159
|
failOnError: { flag: "--fail-on-error", env: "PRODGATOR_FAIL_ON_ERROR", pipe: "FAIL_ON_ERROR" },
|
|
5141
|
-
tokenVariable: { flag: "--token-variable", env: "PRODGATOR_TOKEN_VARIABLE", pipe: null }
|
|
5160
|
+
tokenVariable: { flag: "--token-variable", env: "PRODGATOR_TOKEN_VARIABLE", pipe: null },
|
|
5161
|
+
gate: { flag: "--gate", env: "PRODGATOR_GATE", pipe: "GATE" },
|
|
5162
|
+
gateTimeoutMinutes: { flag: "--gate-timeout", env: "PRODGATOR_GATE_TIMEOUT", pipe: "GATE_TIMEOUT" }
|
|
5142
5163
|
};
|
|
5164
|
+
var DEFAULT_GATE_TIMEOUT_MINUTES = 60;
|
|
5165
|
+
var MAX_GATE_TIMEOUT_MINUTES = 720;
|
|
5166
|
+
function gateTimeout(raw) {
|
|
5167
|
+
if (raw === null) return DEFAULT_GATE_TIMEOUT_MINUTES;
|
|
5168
|
+
const n = Number(raw.trim());
|
|
5169
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_GATE_TIMEOUT_MINUTES) throw new CliError(`gate-timeout must be a whole number of minutes from 1 to ${MAX_GATE_TIMEOUT_MINUTES}`);
|
|
5170
|
+
return n;
|
|
5171
|
+
}
|
|
5143
5172
|
var list = (v) => v ? v.split(/[\n,]/).map((s) => s.trim()).filter(Boolean) : [];
|
|
5144
5173
|
function matrix(raw) {
|
|
5145
5174
|
if (!raw) return null;
|
|
@@ -5162,6 +5191,10 @@ function parseOptions(argv, env) {
|
|
|
5162
5191
|
pipe = true;
|
|
5163
5192
|
continue;
|
|
5164
5193
|
}
|
|
5194
|
+
if (a === "--gate" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("--"))) {
|
|
5195
|
+
flags.set("--gate", "true");
|
|
5196
|
+
continue;
|
|
5197
|
+
}
|
|
5165
5198
|
const eq = a.indexOf("=");
|
|
5166
5199
|
if (a.startsWith("--") && eq > 0) {
|
|
5167
5200
|
flags.set(a.slice(0, eq), a.slice(eq + 1));
|
|
@@ -5203,7 +5236,9 @@ function parseOptions(argv, env) {
|
|
|
5203
5236
|
redactVariables: list(get("redactVariables")),
|
|
5204
5237
|
failOnError: fail === "true",
|
|
5205
5238
|
tokenVariable: get("tokenVariable")?.trim() ?? null,
|
|
5206
|
-
pipe
|
|
5239
|
+
pipe,
|
|
5240
|
+
gate: (get("gate") ?? "false").trim().toLowerCase() === "true",
|
|
5241
|
+
gateTimeoutMinutes: gateTimeout(get("gateTimeoutMinutes"))
|
|
5207
5242
|
};
|
|
5208
5243
|
}
|
|
5209
5244
|
|
|
@@ -5303,6 +5338,55 @@ function workspaceGlob(workspace, warn) {
|
|
|
5303
5338
|
};
|
|
5304
5339
|
}
|
|
5305
5340
|
|
|
5341
|
+
// src/cli/gate.ts
|
|
5342
|
+
var MAX_POLL_FAILURES = 30;
|
|
5343
|
+
function decided(view) {
|
|
5344
|
+
const by = view.decidedBy ? ` by ${view.decidedBy}` : "";
|
|
5345
|
+
const note = view.note ? `: ${view.note}` : "";
|
|
5346
|
+
return `${by}${note}`;
|
|
5347
|
+
}
|
|
5348
|
+
async function runGate(ci, opts, client, deps) {
|
|
5349
|
+
if (ci.provider !== "bitbucket") throw new CliError("Gate mode runs in Bitbucket Pipelines only. On GitLab, use protected environment approvals.");
|
|
5350
|
+
const { log } = deps;
|
|
5351
|
+
const deadline = deps.nowMs() + opts.timeoutMinutes * 6e4;
|
|
5352
|
+
const gate = await openGate(opts.prodgatorUrl, opts.audience, { ...client, maxAttempts: 10 });
|
|
5353
|
+
log.info(`Prodgator gate for ${gate.environment}: ${gate.url}`);
|
|
5354
|
+
let view = gate;
|
|
5355
|
+
let failures = 0;
|
|
5356
|
+
let announced = false;
|
|
5357
|
+
for (; ; ) {
|
|
5358
|
+
if (view.decision === "approved") {
|
|
5359
|
+
log.info(`Approved in Prodgator${decided(view)}. Deploying to ${view.environment}.`);
|
|
5360
|
+
return 0;
|
|
5361
|
+
}
|
|
5362
|
+
if (view.decision === "rejected") {
|
|
5363
|
+
log.error(`Rejected in Prodgator${decided(view)}. The deployment to ${view.environment} is stopped.`);
|
|
5364
|
+
return 1;
|
|
5365
|
+
}
|
|
5366
|
+
if (view.decision === "closed") {
|
|
5367
|
+
log.error(`The Prodgator gate for ${view.environment} is closed (deployment ${view.status}). Run the step again to open a new gate.`);
|
|
5368
|
+
return 1;
|
|
5369
|
+
}
|
|
5370
|
+
if (!announced) {
|
|
5371
|
+
log.info(`Waiting for approval in Prodgator (up to ${opts.timeoutMinutes} minutes).`);
|
|
5372
|
+
announced = true;
|
|
5373
|
+
}
|
|
5374
|
+
const waitMs = Math.max(5, view.pollAfterSeconds || 10) * 1e3;
|
|
5375
|
+
if (deps.nowMs() + waitMs > deadline) {
|
|
5376
|
+
log.error(`No decision in Prodgator after ${opts.timeoutMinutes} minutes. The deployment to ${view.environment} is stopped; run the step again to wait again.`);
|
|
5377
|
+
return 1;
|
|
5378
|
+
}
|
|
5379
|
+
await deps.sleep(waitMs);
|
|
5380
|
+
try {
|
|
5381
|
+
view = await readGate(opts.prodgatorUrl, gate, client);
|
|
5382
|
+
failures = 0;
|
|
5383
|
+
} catch (e) {
|
|
5384
|
+
if (!(e instanceof ReportError) || !e.retryable || ++failures > MAX_POLL_FAILURES) throw e;
|
|
5385
|
+
log.warn(`Could not read the gate (${e.code}); trying again.`);
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5306
5390
|
// src/cli/main.ts
|
|
5307
5391
|
var WARNINGS = {
|
|
5308
5392
|
REPORT_REPLACED_BY_OTHER_RUNNER: "Another job with the same job and report name replaced this report. Give each job its own name input."
|
|
@@ -5336,18 +5420,29 @@ async function run(deps = {}) {
|
|
|
5336
5420
|
const argv = deps.argv ?? process.argv.slice(2);
|
|
5337
5421
|
const log = deps.log ?? { info: (m) => console.log(m), warn: (m) => console.log(`Warning: ${m}`), error: (m) => console.error(m) };
|
|
5338
5422
|
if (argv.includes("--version")) {
|
|
5339
|
-
log.info("1.
|
|
5423
|
+
log.info("1.10.0");
|
|
5340
5424
|
return 0;
|
|
5341
5425
|
}
|
|
5342
5426
|
let failOnError = false;
|
|
5427
|
+
const truthy = (v) => v?.trim().toLowerCase() === "true";
|
|
5428
|
+
let gate = argv.some((a) => a === "--gate" || a === "--gate=true") || truthy(env.PRODGATOR_GATE) || argv.includes("--pipe") && truthy(env.GATE);
|
|
5343
5429
|
try {
|
|
5344
5430
|
const opts = parseOptions(argv, env);
|
|
5345
5431
|
failOnError = opts.failOnError;
|
|
5432
|
+
gate = opts.gate;
|
|
5346
5433
|
const ci = detectCi(env, deps.cwd ?? process.cwd());
|
|
5347
5434
|
const variable = opts.tokenVariable ?? ci.defaultTokenVariable;
|
|
5348
5435
|
const token = readToken(env, variable, ci.provider);
|
|
5349
5436
|
const problem = tokenProblem(token, opts.audience, ci.provider, variable, (deps.nowMs ?? Date.now)());
|
|
5350
5437
|
if (problem) throw new CliError(problem);
|
|
5438
|
+
if (opts.gate) {
|
|
5439
|
+
const client2 = { getToken: async () => token, fetch: deps.fetch, sleep: deps.sleep, fixedToken: true, userAgent: `prodgator-report/${"1.10.0"} (${ci.provider}; gate)` };
|
|
5440
|
+
return await runGate(ci, { prodgatorUrl: opts.prodgatorUrl, audience: opts.audience, timeoutMinutes: opts.gateTimeoutMinutes }, client2, {
|
|
5441
|
+
sleep: deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))),
|
|
5442
|
+
nowMs: deps.nowMs ?? Date.now,
|
|
5443
|
+
log
|
|
5444
|
+
});
|
|
5445
|
+
}
|
|
5351
5446
|
const secrets = secretValues(env, [variable, ...opts.redactVariables]);
|
|
5352
5447
|
const glob2 = workspaceGlob(ci.workspace, (w) => log.warn(w));
|
|
5353
5448
|
const rawSummary = await readSummary(ci.workspace, opts.summary, opts.summaryFile, (m) => log.info(m));
|
|
@@ -5364,7 +5459,7 @@ async function run(deps = {}) {
|
|
|
5364
5459
|
fetch: deps.fetch,
|
|
5365
5460
|
sleep: deps.sleep,
|
|
5366
5461
|
fixedToken: true,
|
|
5367
|
-
userAgent: `prodgator-report/${"1.
|
|
5462
|
+
userAgent: `prodgator-report/${"1.10.0"} (${ci.provider})`
|
|
5368
5463
|
};
|
|
5369
5464
|
const request = buildIngestRequest({ job: { name: opts.jobName ?? ci.jobName, matrix: opts.matrix, runner: ci.runner }, name: opts.name, summary, attestations, files });
|
|
5370
5465
|
const res = await sendReport(opts.prodgatorUrl, ci.provider, request, opts.audience, client);
|
|
@@ -5390,6 +5485,10 @@ async function run(deps = {}) {
|
|
|
5390
5485
|
return 0;
|
|
5391
5486
|
} catch (e) {
|
|
5392
5487
|
const msg2 = e instanceof ReportError ? `${e.code}: ${e.message}` : e.message;
|
|
5488
|
+
if (gate) {
|
|
5489
|
+
log.error(`Prodgator gate failed: ${msg2}`);
|
|
5490
|
+
return 1;
|
|
5491
|
+
}
|
|
5393
5492
|
if (failOnError) {
|
|
5394
5493
|
log.error(`Prodgator report failed: ${msg2}`);
|
|
5395
5494
|
return 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prodgator/report",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Send CI job summaries, test results, attestations and artifacts to Prodgator, authenticated with the CI system's OIDC token",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.prodgator.io/pipelines/run-reports",
|