@prodgator/report 1.8.0 → 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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +103 -4
  3. 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.8.0");
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.8.0"} (${ci.provider})`
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.8.0",
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",