@prodgator/report 1.12.0 → 3.5.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 +88 -15
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ Shared report client for the Prodgator GitHub action, GitLab component and Bitbu
|
|
|
8
8
|
|
|
9
9
|
Options can be set as a CLI flag, a `PRODGATOR_`-prefixed environment variable, or (only with `--pipe`) an unprefixed pipe variable. A flag wins over a pipe variable, which wins over the `PRODGATOR_` variable, which wins over the default.
|
|
10
10
|
|
|
11
|
+
On Azure Pipelines, a script step reads `PRODGATOR_URL` and `PRODGATOR_ALLOW_CUSTOM_URL` from pipeline variables, so do not make them settable at queue time. The Marketplace task ignores them and takes inputs only.
|
|
12
|
+
|
|
11
13
|
| Option | CLI flag | Env (GitLab, any shell) | Pipe variable |
|
|
12
14
|
|---|---|---|---|
|
|
13
15
|
| prodgatorUrl | `--prodgator-url` | `PRODGATOR_URL` | `PRODGATOR_URL` |
|
package/dist/cli.js
CHANGED
|
@@ -7054,6 +7054,8 @@ async function postJson(url, body, audience, deps) {
|
|
|
7054
7054
|
method: "POST",
|
|
7055
7055
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": deps.userAgent ?? "prodgator-report/1" },
|
|
7056
7056
|
body: JSON.stringify(body),
|
|
7057
|
+
// A redirect would carry the token to another address: never follow one.
|
|
7058
|
+
redirect: "manual",
|
|
7057
7059
|
signal: AbortSignal.timeout(3e4)
|
|
7058
7060
|
});
|
|
7059
7061
|
} catch (e) {
|
|
@@ -7061,6 +7063,9 @@ async function postJson(url, body, audience, deps) {
|
|
|
7061
7063
|
last = new ReportError(0, "NETWORK", e.message, true);
|
|
7062
7064
|
continue;
|
|
7063
7065
|
}
|
|
7066
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
7067
|
+
throw new ReportError(res.status, "REDIRECT_REFUSED", "Prodgator answered with a redirect; the token is never sent to a redirected address. Check prodgator-url.", false);
|
|
7068
|
+
}
|
|
7064
7069
|
if (res.ok) return await res.clone().json();
|
|
7065
7070
|
const payload2 = await res.clone().json().catch(() => ({}));
|
|
7066
7071
|
const retryable = !(deps.fixedToken && payload2.code === "TOKEN_EXPIRED") && (payload2.retryable === true || RETRY_STATUS.has(res.status));
|
|
@@ -7076,8 +7081,8 @@ async function postJson(url, body, audience, deps) {
|
|
|
7076
7081
|
}
|
|
7077
7082
|
throw last;
|
|
7078
7083
|
}
|
|
7079
|
-
var sendReport = (baseUrl, provider, req, audience, deps) => postJson(`${baseUrl}/api/ingest/${provider}/reports`, req, audience, deps);
|
|
7080
|
-
var completeReport = (baseUrl, provider, reportId, artifactIds, audience, deps) => postJson(`${baseUrl}/api/ingest/${provider}/reports/${encodeURIComponent(reportId)}/complete`, { artifactIds }, audience, deps);
|
|
7084
|
+
var sendReport = (baseUrl, provider, req, audience, deps, azure) => postJson(`${baseUrl}/api/ingest/${provider}/reports`, azure ? { ...req, azure } : req, audience, deps);
|
|
7085
|
+
var completeReport = (baseUrl, provider, reportId, artifactIds, audience, deps, azure) => postJson(`${baseUrl}/api/ingest/${provider}/reports/${encodeURIComponent(reportId)}/complete`, azure ? { artifactIds, azure } : { artifactIds }, audience, deps);
|
|
7081
7086
|
|
|
7082
7087
|
// src/core/upload.ts
|
|
7083
7088
|
var import_fs4 = require("fs");
|
|
@@ -7087,7 +7092,8 @@ function putOnce(slot, file) {
|
|
|
7087
7092
|
return new Promise((resolve3, reject) => {
|
|
7088
7093
|
const url = new URL(slot.url);
|
|
7089
7094
|
const mod = url.protocol === "http:" ? http : https;
|
|
7090
|
-
const
|
|
7095
|
+
const headers = Object.fromEntries(Object.entries(slot.headers).filter(([k]) => !/^(proxy-)?authorization$|^cookie$/i.test(k)));
|
|
7096
|
+
const req = mod.request(url, { method: "PUT", headers, timeout: 10 * 6e4 }, (res) => {
|
|
7091
7097
|
let body = "";
|
|
7092
7098
|
res.setEncoding("utf8");
|
|
7093
7099
|
res.on("data", (c) => {
|
|
@@ -7131,11 +7137,15 @@ async function readGate(baseUrl, gate, deps) {
|
|
|
7131
7137
|
res = await (deps.fetch ?? fetch)(`${baseUrl}/api/ingest/bitbucket/gates/${encodeURIComponent(gate.gateId)}`, {
|
|
7132
7138
|
method: "GET",
|
|
7133
7139
|
headers: { authorization: `Bearer ${gate.pollToken}`, "user-agent": deps.userAgent ?? "prodgator-report/1" },
|
|
7140
|
+
redirect: "manual",
|
|
7134
7141
|
signal: AbortSignal.timeout(3e4)
|
|
7135
7142
|
});
|
|
7136
7143
|
} catch (e) {
|
|
7137
7144
|
throw new ReportError(0, "NETWORK", e.message, true);
|
|
7138
7145
|
}
|
|
7146
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
7147
|
+
throw new ReportError(res.status, "REDIRECT_REFUSED", "Prodgator answered with a redirect; the token is never sent to a redirected address. Check prodgator-url.", false);
|
|
7148
|
+
}
|
|
7139
7149
|
if (res.ok) return await res.json();
|
|
7140
7150
|
const payload2 = await res.json().catch(() => ({}));
|
|
7141
7151
|
const retryable = res.status === 429 || res.status >= 500;
|
|
@@ -7149,7 +7159,42 @@ var CliError = class extends Error {
|
|
|
7149
7159
|
this.name = "CliError";
|
|
7150
7160
|
}
|
|
7151
7161
|
};
|
|
7152
|
-
|
|
7162
|
+
var AZURE_TOKEN_HINT = 'Map the job token: add "env: SYSTEM_ACCESSTOKEN: $(System.AccessToken)" to this step, or use the Prodgator task.';
|
|
7163
|
+
function azureContext(env) {
|
|
7164
|
+
const need = (name) => {
|
|
7165
|
+
const v = env[name]?.trim();
|
|
7166
|
+
if (!v) throw new CliError(`${name} is not set; run prodgator-report inside an Azure Pipelines job.`);
|
|
7167
|
+
return v;
|
|
7168
|
+
};
|
|
7169
|
+
const sha = need("BUILD_SOURCEVERSION");
|
|
7170
|
+
if (!/^[0-9a-f]{40}$/i.test(sha)) throw new CliError("BUILD_SOURCEVERSION is not a 40-character commit hash; Prodgator reports need a pipeline that builds a Git commit.");
|
|
7171
|
+
const buildId = Number(need("BUILD_BUILDID"));
|
|
7172
|
+
if (!Number.isInteger(buildId) || buildId < 1) throw new CliError("BUILD_BUILDID is not a build number.");
|
|
7173
|
+
const jobId = need("SYSTEM_JOBID");
|
|
7174
|
+
if (jobId.length > 64) throw new CliError("SYSTEM_JOBID is longer than 64 characters.");
|
|
7175
|
+
const attempt = Number(env.SYSTEM_JOBATTEMPT);
|
|
7176
|
+
return {
|
|
7177
|
+
collectionUri: need("SYSTEM_COLLECTIONURI"),
|
|
7178
|
+
projectId: need("SYSTEM_TEAMPROJECTID"),
|
|
7179
|
+
buildId,
|
|
7180
|
+
jobId,
|
|
7181
|
+
attempt: Number.isInteger(attempt) && attempt >= 1 && attempt <= 999 ? attempt : 1,
|
|
7182
|
+
sha: sha.toLowerCase()
|
|
7183
|
+
};
|
|
7184
|
+
}
|
|
7185
|
+
function detectCi(env, cwd, tokenVariable) {
|
|
7186
|
+
if (env.TF_BUILD === "True") {
|
|
7187
|
+
const variable = tokenVariable ?? "SYSTEM_ACCESSTOKEN";
|
|
7188
|
+
if (!env[variable]?.trim()) throw new CliError(variable === "SYSTEM_ACCESSTOKEN" ? AZURE_TOKEN_HINT : `No job token in ${variable}. ${AZURE_TOKEN_HINT}`);
|
|
7189
|
+
return {
|
|
7190
|
+
provider: "azure",
|
|
7191
|
+
workspace: env.BUILD_SOURCESDIRECTORY || cwd,
|
|
7192
|
+
jobName: env.SYSTEM_JOBDISPLAYNAME || "job",
|
|
7193
|
+
runner: env.AGENT_NAME || null,
|
|
7194
|
+
defaultTokenVariable: "SYSTEM_ACCESSTOKEN",
|
|
7195
|
+
azure: azureContext(env)
|
|
7196
|
+
};
|
|
7197
|
+
}
|
|
7153
7198
|
if (env.GITLAB_CI === "true") {
|
|
7154
7199
|
return {
|
|
7155
7200
|
provider: "gitlab",
|
|
@@ -7170,13 +7215,14 @@ function detectCi(env, cwd) {
|
|
|
7170
7215
|
};
|
|
7171
7216
|
}
|
|
7172
7217
|
if (env.GITHUB_ACTIONS === "true") throw new CliError("On GitHub Actions use the prodgator/prodgator-action action instead of prodgator-report.");
|
|
7173
|
-
throw new CliError("prodgator-report runs in GitLab CI or
|
|
7218
|
+
throw new CliError("prodgator-report runs in GitLab CI, Bitbucket Pipelines or Azure Pipelines (GITLAB_CI, BITBUCKET_BUILD_NUMBER or TF_BUILD is not set).");
|
|
7174
7219
|
}
|
|
7175
7220
|
|
|
7176
7221
|
// src/cli/options.ts
|
|
7177
7222
|
var DEFAULT_PRODGATOR_URL = "https://api.prodgator.io";
|
|
7178
7223
|
var OPTION_NAMES = {
|
|
7179
7224
|
prodgatorUrl: { flag: "--prodgator-url", env: "PRODGATOR_URL", pipe: "PRODGATOR_URL" },
|
|
7225
|
+
allowCustomUrl: { flag: "--allow-custom-url", env: "PRODGATOR_ALLOW_CUSTOM_URL", pipe: null },
|
|
7180
7226
|
audience: { flag: "--audience", env: "PRODGATOR_AUDIENCE", pipe: "AUDIENCE" },
|
|
7181
7227
|
summary: { flag: "--summary", env: "PRODGATOR_SUMMARY", pipe: "SUMMARY" },
|
|
7182
7228
|
summaryFile: { flag: "--summary-file", env: "PRODGATOR_SUMMARY_FILE", pipe: "SUMMARY_FILE" },
|
|
@@ -7222,8 +7268,8 @@ function parseOptions(argv, env) {
|
|
|
7222
7268
|
pipe = true;
|
|
7223
7269
|
continue;
|
|
7224
7270
|
}
|
|
7225
|
-
if (a === "--gate" && (argv[i + 1] === void 0 || argv[i + 1].startsWith("--"))) {
|
|
7226
|
-
flags.set(
|
|
7271
|
+
if ((a === "--gate" || a === "--allow-custom-url") && (argv[i + 1] === void 0 || argv[i + 1].startsWith("--"))) {
|
|
7272
|
+
flags.set(a, "true");
|
|
7227
7273
|
continue;
|
|
7228
7274
|
}
|
|
7229
7275
|
const eq = a.indexOf("=");
|
|
@@ -7266,6 +7312,7 @@ function parseOptions(argv, env) {
|
|
|
7266
7312
|
attestations: get("attestations"),
|
|
7267
7313
|
redactVariables: list(get("redactVariables")),
|
|
7268
7314
|
failOnError: fail === "true",
|
|
7315
|
+
allowCustomUrl: (get("allowCustomUrl") ?? "false").trim().toLowerCase() === "true",
|
|
7269
7316
|
tokenVariable: get("tokenVariable")?.trim() ?? null,
|
|
7270
7317
|
pipe,
|
|
7271
7318
|
gate: (get("gate") ?? "false").trim().toLowerCase() === "true",
|
|
@@ -7276,11 +7323,12 @@ function parseOptions(argv, env) {
|
|
|
7276
7323
|
// src/cli/token.ts
|
|
7277
7324
|
var HOW = {
|
|
7278
7325
|
gitlab: (v) => `Add "id_tokens: ${v}: aud: <Prodgator API origin>" to the job (the Prodgator component does this).`,
|
|
7326
|
+
azure: () => AZURE_TOKEN_HINT,
|
|
7279
7327
|
bitbucket: () => 'Add "oidc: audiences: [<Prodgator API origin>]" to the step that runs the pipe.'
|
|
7280
7328
|
};
|
|
7281
7329
|
function readToken(env, variable, provider) {
|
|
7282
7330
|
const t = env[variable]?.trim();
|
|
7283
|
-
if (!t) throw new CliError(`No OIDC token in ${variable}. ${HOW[provider](variable)}`);
|
|
7331
|
+
if (!t) throw new CliError(`No ${provider === "azure" ? "job" : "OIDC"} token in ${variable}. ${HOW[provider](variable)}`);
|
|
7284
7332
|
return t;
|
|
7285
7333
|
}
|
|
7286
7334
|
function payload(token) {
|
|
@@ -7293,6 +7341,7 @@ function payload(token) {
|
|
|
7293
7341
|
}
|
|
7294
7342
|
}
|
|
7295
7343
|
function tokenProblem(token, audience, provider, variable, nowMs) {
|
|
7344
|
+
if (provider === "azure") return token ? null : "The job token is empty.";
|
|
7296
7345
|
const p = payload(token);
|
|
7297
7346
|
if (!p) return null;
|
|
7298
7347
|
const auds = [].concat(p.aud ?? []).filter((a) => typeof a === "string");
|
|
@@ -7305,10 +7354,30 @@ function tokenProblem(token, audience, provider, variable, nowMs) {
|
|
|
7305
7354
|
return null;
|
|
7306
7355
|
}
|
|
7307
7356
|
|
|
7357
|
+
// src/cli/url-policy.ts
|
|
7358
|
+
var PRODGATOR_DOMAINS = ["prodgator.io", "prodgator.dev"];
|
|
7359
|
+
var LOCAL_HOSTS = ["localhost", "127.0.0.1"];
|
|
7360
|
+
var isProdgatorHost = (host) => PRODGATOR_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
7361
|
+
function checkTokenDestination(prodgatorUrl, provider, allowCustomUrl) {
|
|
7362
|
+
if (provider !== "azure") return null;
|
|
7363
|
+
const u = new URL(prodgatorUrl);
|
|
7364
|
+
const optIn = "To send the job token to another server on purpose, set allow-custom-url (--allow-custom-url, PRODGATOR_ALLOW_CUSTOM_URL=true, or the task input allowCustomUrl).";
|
|
7365
|
+
if (u.username || u.password) throw new CliError(`prodgator-url must not carry a user name or password (host ${u.hostname}).`);
|
|
7366
|
+
if (u.protocol === "https:" && !u.port && isProdgatorHost(u.hostname)) return null;
|
|
7367
|
+
if (!allowCustomUrl) {
|
|
7368
|
+
throw new CliError(`Refusing to send the Azure Pipelines job token to ${u.host}: it is not prodgator.io or prodgator.dev over https. ${optIn}`);
|
|
7369
|
+
}
|
|
7370
|
+
if (u.protocol === "http:" && !LOCAL_HOSTS.includes(u.hostname)) {
|
|
7371
|
+
throw new CliError(`Refusing to send the Azure Pipelines job token to ${u.host} over plain http; only localhost and 127.0.0.1 may use http.`);
|
|
7372
|
+
}
|
|
7373
|
+
return `Sending the Azure Pipelines job token to ${u.host}, which is not a Prodgator address. allow-custom-url is on.`;
|
|
7374
|
+
}
|
|
7375
|
+
|
|
7308
7376
|
// src/cli/redact.ts
|
|
7309
7377
|
var ALWAYS_REDACT = Object.freeze([
|
|
7310
7378
|
"PRODGATOR_ID_TOKEN",
|
|
7311
7379
|
"BITBUCKET_STEP_OIDC_TOKEN",
|
|
7380
|
+
"SYSTEM_ACCESSTOKEN",
|
|
7312
7381
|
"CI_JOB_TOKEN",
|
|
7313
7382
|
"CI_JOB_JWT",
|
|
7314
7383
|
"CI_JOB_JWT_V1",
|
|
@@ -7377,7 +7446,7 @@ function decided(view) {
|
|
|
7377
7446
|
return `${by}${note}`;
|
|
7378
7447
|
}
|
|
7379
7448
|
async function runGate(ci, opts, client, deps) {
|
|
7380
|
-
if (ci.provider !== "bitbucket") throw new CliError("Gate mode runs in Bitbucket Pipelines only. On GitLab, use protected environment approvals.");
|
|
7449
|
+
if (ci.provider !== "bitbucket") throw new CliError("Gate mode runs in Bitbucket Pipelines only. On GitLab, use protected environment approvals. On Azure Pipelines, use environment approvals and checks.");
|
|
7381
7450
|
const { log } = deps;
|
|
7382
7451
|
const deadline = deps.nowMs() + opts.timeoutMinutes * 6e4;
|
|
7383
7452
|
const gate = await openGate(opts.prodgatorUrl, opts.audience, { ...client, maxAttempts: 10 });
|
|
@@ -7451,7 +7520,7 @@ async function run(deps = {}) {
|
|
|
7451
7520
|
const argv = deps.argv ?? process.argv.slice(2);
|
|
7452
7521
|
const log = deps.log ?? { info: (m) => console.log(m), warn: (m) => console.log(`Warning: ${m}`), error: (m) => console.error(m) };
|
|
7453
7522
|
if (argv.includes("--version")) {
|
|
7454
|
-
log.info("
|
|
7523
|
+
log.info("3.5.0");
|
|
7455
7524
|
return 0;
|
|
7456
7525
|
}
|
|
7457
7526
|
let failOnError = false;
|
|
@@ -7461,13 +7530,15 @@ async function run(deps = {}) {
|
|
|
7461
7530
|
const opts = parseOptions(argv, env);
|
|
7462
7531
|
failOnError = opts.failOnError;
|
|
7463
7532
|
gate = opts.gate;
|
|
7464
|
-
const ci = detectCi(env, deps.cwd ?? process.cwd());
|
|
7533
|
+
const ci = detectCi(env, deps.cwd ?? process.cwd(), opts.tokenVariable ?? void 0);
|
|
7465
7534
|
const variable = opts.tokenVariable ?? ci.defaultTokenVariable;
|
|
7535
|
+
const custom = checkTokenDestination(opts.prodgatorUrl, ci.provider, opts.allowCustomUrl);
|
|
7536
|
+
if (custom) log.warn(custom);
|
|
7466
7537
|
const token = readToken(env, variable, ci.provider);
|
|
7467
7538
|
const problem = tokenProblem(token, opts.audience, ci.provider, variable, (deps.nowMs ?? Date.now)());
|
|
7468
7539
|
if (problem) throw new CliError(problem);
|
|
7469
7540
|
if (opts.gate) {
|
|
7470
|
-
const client2 = { getToken: async () => token, fetch: deps.fetch, sleep: deps.sleep, fixedToken: true, userAgent: `prodgator-report/${"
|
|
7541
|
+
const client2 = { getToken: async () => token, fetch: deps.fetch, sleep: deps.sleep, fixedToken: true, userAgent: `prodgator-report/${"3.5.0"} (${ci.provider}; gate)` };
|
|
7471
7542
|
return await runGate(ci, { prodgatorUrl: opts.prodgatorUrl, audience: opts.audience, timeoutMinutes: opts.gateTimeoutMinutes }, client2, {
|
|
7472
7543
|
sleep: deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))),
|
|
7473
7544
|
nowMs: deps.nowMs ?? Date.now,
|
|
@@ -7490,10 +7561,10 @@ async function run(deps = {}) {
|
|
|
7490
7561
|
fetch: deps.fetch,
|
|
7491
7562
|
sleep: deps.sleep,
|
|
7492
7563
|
fixedToken: true,
|
|
7493
|
-
userAgent: `prodgator-report/${"
|
|
7564
|
+
userAgent: `prodgator-report/${"3.5.0"} (${ci.provider})`
|
|
7494
7565
|
};
|
|
7495
7566
|
const request = buildIngestRequest({ job: { name: opts.jobName ?? ci.jobName, matrix: opts.matrix, runner: ci.runner }, name: opts.name, summary, attestations, files });
|
|
7496
|
-
const res = await sendReport(opts.prodgatorUrl, ci.provider, request, opts.audience, client);
|
|
7567
|
+
const res = await sendReport(opts.prodgatorUrl, ci.provider, request, opts.audience, client, ci.azure);
|
|
7497
7568
|
for (const w of res.warnings) log.warn(WARNINGS[w] ?? w);
|
|
7498
7569
|
for (const s of res.skipped) if (s.reason !== "already_uploaded") log.warn(`${s.path} was not uploaded: ${SKIPPED[s.reason] ?? s.reason}`);
|
|
7499
7570
|
if (!res.trusted) log.info("Prodgator stored this report as coming from an untrusted source (fork merge request or external pull request); release policies ignore it by default.");
|
|
@@ -7509,10 +7580,11 @@ async function run(deps = {}) {
|
|
|
7509
7580
|
}
|
|
7510
7581
|
}
|
|
7511
7582
|
if (uploaded.length) {
|
|
7512
|
-
const done = await completeReport(opts.prodgatorUrl, ci.provider, res.reportId, uploaded, opts.audience, client);
|
|
7583
|
+
const done = await completeReport(opts.prodgatorUrl, ci.provider, res.reportId, uploaded, opts.audience, client, ci.azure);
|
|
7513
7584
|
for (const r of done.results.filter((r2) => r2.status !== "uploaded")) log.warn(`artifact ${r.artifactId} was ${r.status}${r.reason ? `: ${r.reason}` : ""}`);
|
|
7514
7585
|
}
|
|
7515
7586
|
log.info(`Prodgator report: ${res.url}`);
|
|
7587
|
+
deps.onOutcome?.("sent");
|
|
7516
7588
|
return 0;
|
|
7517
7589
|
} catch (e) {
|
|
7518
7590
|
const msg2 = e instanceof ReportError ? `${e.code}: ${e.message}` : e.message;
|
|
@@ -7525,6 +7597,7 @@ async function run(deps = {}) {
|
|
|
7525
7597
|
return 1;
|
|
7526
7598
|
}
|
|
7527
7599
|
log.warn(`Prodgator report was not sent: ${msg2}`);
|
|
7600
|
+
deps.onOutcome?.("not_sent");
|
|
7528
7601
|
return 0;
|
|
7529
7602
|
}
|
|
7530
7603
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prodgator/report",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.5.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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"build": "node scripts/build.mjs",
|
|
21
21
|
"typecheck": "tsc --noEmit",
|
|
22
22
|
"test": "vitest run",
|
|
23
|
+
"build:azure-task": "node scripts/build-azure-task.mjs",
|
|
23
24
|
"release:prepare": "node scripts/prepare-release.mjs"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/node": "^22.20.5",
|
|
32
|
+
"azure-pipelines-task-lib": "^5.281.0",
|
|
31
33
|
"esbuild": "^0.25.0",
|
|
32
34
|
"typescript": "^5.9.3",
|
|
33
35
|
"vitest": "^4.1.11",
|