@getantidote/cli 0.1.1-rc.0 → 1.0.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 +11 -5
- package/dist/antidote.mjs +142 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,9 +64,9 @@ jobs:
|
|
|
64
64
|
(`ANTIDOTE_OIDC_TOKEN` or `ANTIDOTE_OIDC_TOKEN_FILE`), without `ANTIDOTE_SERVICE_IDENTITY`.
|
|
65
65
|
The platform finds the identity from the token itself: the one that belongs to
|
|
66
66
|
your organisation's connection to that CI platform (Settings → Integrations →
|
|
67
|
-
Source control & CI/CD), so there is nothing to create or name.
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
Source control & CI/CD), so there is nothing to create or name. Connecting
|
|
68
|
+
GitLab CI/CD creates it, for GitLab jobs' ID tokens; the platform refuses
|
|
69
|
+
other CI platforms' tokens this way. The token's audience must be the platform's URL (the origin of
|
|
70
70
|
`ANTIDOTE_API_URL`). When the platform refuses a token, the CLI prints its reason as
|
|
71
71
|
it gave it.
|
|
72
72
|
|
|
@@ -142,6 +142,8 @@ empty one.
|
|
|
142
142
|
antidote whoami # the signed-in user (or service identity), organisation and auth mode (a tool command, below)
|
|
143
143
|
antidote tools # the tools available to the CLI
|
|
144
144
|
antidote tools --refresh # re-fetch the tool list
|
|
145
|
+
antidote wait-for-deployment --product-id <id> --commit-sha <sha>
|
|
146
|
+
# poll the environment's URL until it serves the commit (--help: its flags)
|
|
145
147
|
antidote logout # revoke the session and delete this machine's credentials
|
|
146
148
|
antidote --version
|
|
147
149
|
```
|
|
@@ -214,6 +216,10 @@ the tool's input schema. `antidote <tool> --help` lists its flags.
|
|
|
214
216
|
|
|
215
217
|
### In CI
|
|
216
218
|
|
|
219
|
+
On GitLab, the `preview` and `release` components (`actions-repo/gitlab`) make
|
|
220
|
+
these calls for you and run the CLI's major version, `@getantidote/cli@1`. The
|
|
221
|
+
jobs below show the calls themselves.
|
|
222
|
+
|
|
217
223
|
A job that needs a tool's result in a later job writes it as a dotenv file. In
|
|
218
224
|
GitLab, with `ANTIDOTE_API_KEY` set as a masked CI/CD variable and the product's
|
|
219
225
|
id given to the tool as `ANTIDOTE_INPUT_PRODUCT_ID`:
|
|
@@ -226,7 +232,7 @@ antidote-preview:
|
|
|
226
232
|
rules:
|
|
227
233
|
- if: $CI_MERGE_REQUEST_IID
|
|
228
234
|
script:
|
|
229
|
-
- npx -y @getantidote/cli@
|
|
235
|
+
- npx -y @getantidote/cli@1 record-preview --provider gitlab --host "$CI_SERVER_FQDN" --repository "$CI_PROJECT_PATH" --repository-id "$CI_PROJECT_ID" --number "$CI_MERGE_REQUEST_IID" --head-sha "$CI_COMMIT_SHA" --branch "$CI_COMMIT_REF_NAME" --pipeline-id "$CI_PIPELINE_ID" --dotenv antidote.env
|
|
230
236
|
artifacts:
|
|
231
237
|
when: always
|
|
232
238
|
reports:
|
|
@@ -254,7 +260,7 @@ antidote-release:
|
|
|
254
260
|
variables:
|
|
255
261
|
ANTIDOTE_INPUT_PRODUCT_ID: prod_123
|
|
256
262
|
script:
|
|
257
|
-
- npx -y @getantidote/cli@
|
|
263
|
+
- npx -y @getantidote/cli@1 record-release --environment staging --build-sha "$CI_COMMIT_SHA" --provider gitlab --host "$CI_SERVER_FQDN" --repository "$CI_PROJECT_PATH" --repository-id "$CI_PROJECT_ID" --dotenv antidote.env
|
|
258
264
|
artifacts:
|
|
259
265
|
when: always
|
|
260
266
|
reports:
|
package/dist/antidote.mjs
CHANGED
|
@@ -16941,7 +16941,7 @@ async function openMcpSession(apiUrl, credentials, deps) {
|
|
|
16941
16941
|
|
|
16942
16942
|
// src/tool-commands.ts
|
|
16943
16943
|
import { parseArgs } from "node:util";
|
|
16944
|
-
var BUILTIN_COMMANDS = ["login", "logout", "tools"];
|
|
16944
|
+
var BUILTIN_COMMANDS = ["login", "logout", "tools", "wait-for-deployment"];
|
|
16945
16945
|
var RESERVED_FLAGS = /* @__PURE__ */ new Set(["json", "help", "version", "device", "refresh", "dotenv"]);
|
|
16946
16946
|
var UsageError = class extends Error {
|
|
16947
16947
|
/** The --dotenv the strict parse accepted, when the mistake came after it. */
|
|
@@ -17135,6 +17135,74 @@ async function loadTools(source, opts = {}) {
|
|
|
17135
17135
|
return list;
|
|
17136
17136
|
}
|
|
17137
17137
|
|
|
17138
|
+
// src/wait-for-deployment.ts
|
|
17139
|
+
var DEFAULT_WAIT_COMMIT_HEADER = "x-commit-sha";
|
|
17140
|
+
var DEFAULT_WAIT_COMMIT_PATH = "/";
|
|
17141
|
+
var DEFAULT_WAIT_TIMEOUT_SECONDS = 300;
|
|
17142
|
+
var DEFAULT_WAIT_INTERVAL_SECONDS = 10;
|
|
17143
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
17144
|
+
function buildPollUrl(baseUrl, path) {
|
|
17145
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
17146
|
+
const trimmed = (path || "").trim() || "/";
|
|
17147
|
+
const suffix = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
17148
|
+
return `${base}${suffix}`;
|
|
17149
|
+
}
|
|
17150
|
+
function commitHeaderMatches(headerValue, expectedSha) {
|
|
17151
|
+
const normalized = (headerValue || "").trim().toLowerCase();
|
|
17152
|
+
const expected = (expectedSha || "").trim().toLowerCase();
|
|
17153
|
+
if (!normalized || !expected) return false;
|
|
17154
|
+
if (normalized === expected) return true;
|
|
17155
|
+
return normalized === expected.slice(0, 7);
|
|
17156
|
+
}
|
|
17157
|
+
function fetchProbe(http, headerName) {
|
|
17158
|
+
return async (url2) => {
|
|
17159
|
+
const response = await http(url2, { headers: { accept: "*/*" }, signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
17160
|
+
await response.body?.cancel().catch(() => void 0);
|
|
17161
|
+
return response.headers.get(headerName);
|
|
17162
|
+
};
|
|
17163
|
+
}
|
|
17164
|
+
function environmentUrl(environments, name) {
|
|
17165
|
+
const target = environments.find((e) => name ? e.name?.text === name : e.is_default);
|
|
17166
|
+
if (!target) throw new Error(name ? `The product has no environment named ${name}.` : "The product has no default environment.");
|
|
17167
|
+
if (!target.url) throw new Error(`Environment ${name || "(the default)"} has no URL in Duku.`);
|
|
17168
|
+
return target.url;
|
|
17169
|
+
}
|
|
17170
|
+
function defaultSleep(ms) {
|
|
17171
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17172
|
+
}
|
|
17173
|
+
async function waitForDeployment(opts) {
|
|
17174
|
+
const { pollUrl, expectedSha, headerName, timeoutSeconds, intervalSeconds, probe, sleep = defaultSleep, now = Date.now, log } = opts;
|
|
17175
|
+
const expected = expectedSha.trim();
|
|
17176
|
+
const short = expected.slice(0, 7);
|
|
17177
|
+
log?.(`Waiting up to ${timeoutSeconds}s for ${pollUrl} to serve commit ${short} (header \`${headerName}\`)...`);
|
|
17178
|
+
const deadline = now() + timeoutSeconds * 1e3;
|
|
17179
|
+
let attempt = 0;
|
|
17180
|
+
for (; ; ) {
|
|
17181
|
+
attempt += 1;
|
|
17182
|
+
let value = null;
|
|
17183
|
+
try {
|
|
17184
|
+
value = await probe(pollUrl);
|
|
17185
|
+
} catch (err) {
|
|
17186
|
+
log?.(`Attempt ${attempt}: request to ${pollUrl} failed (${err instanceof Error ? err.message : String(err)}) \u2014 retrying...`);
|
|
17187
|
+
}
|
|
17188
|
+
if (value !== null && commitHeaderMatches(value, expected)) {
|
|
17189
|
+
log?.(`Attempt ${attempt}: ${pollUrl} serves commit ${short} \u2014 proceeding.`);
|
|
17190
|
+
return;
|
|
17191
|
+
}
|
|
17192
|
+
if (value !== null) {
|
|
17193
|
+
log?.(`Attempt ${attempt}: serves '${value.trim() || "(empty)"}' \u2014 waiting for '${short}'...`);
|
|
17194
|
+
} else if (attempt === 1) {
|
|
17195
|
+
log?.(`Attempt 1: no \`${headerName}\` header yet \u2014 waiting for '${short}'...`);
|
|
17196
|
+
}
|
|
17197
|
+
if (now() >= deadline) break;
|
|
17198
|
+
const remaining = deadline - now();
|
|
17199
|
+
await sleep(Math.min(intervalSeconds * 1e3, Math.max(remaining, 0)));
|
|
17200
|
+
}
|
|
17201
|
+
throw new Error(
|
|
17202
|
+
`Timed out after ${timeoutSeconds}s waiting for ${pollUrl} to serve commit ${expected}. Expose the commit as response header \`${headerName}\` (e.g. \`res.setHeader('${headerName}', process.env.COMMIT_SHA)\`) so the job can tell the new deploy apart from the previous build.`
|
|
17203
|
+
);
|
|
17204
|
+
}
|
|
17205
|
+
|
|
17138
17206
|
// src/cli.ts
|
|
17139
17207
|
var USAGE = `Usage: antidote <command> [--json]
|
|
17140
17208
|
|
|
@@ -17143,6 +17211,8 @@ Commands:
|
|
|
17143
17211
|
the default over SSH or without a display)
|
|
17144
17212
|
logout Sign out and forget this machine's credentials
|
|
17145
17213
|
tools List the tools available to the CLI (--refresh: re-fetch the list)
|
|
17214
|
+
wait-for-deployment
|
|
17215
|
+
Wait until an environment serves a commit (--help: its flags)
|
|
17146
17216
|
|
|
17147
17217
|
The platform's tools are commands too, such as \`antidote whoami\`.
|
|
17148
17218
|
\`antidote <tool> --help\` shows a tool's flags.
|
|
@@ -17221,6 +17291,9 @@ async function run(argv, deps) {
|
|
|
17221
17291
|
return ctx;
|
|
17222
17292
|
};
|
|
17223
17293
|
try {
|
|
17294
|
+
if (command === "wait-for-deployment" && !leading.some((arg) => VERSION_FLAGS.has(arg))) {
|
|
17295
|
+
return await runWaitForDeployment(argv.slice(commandAt + 1), deps, context);
|
|
17296
|
+
}
|
|
17224
17297
|
return isBuiltin ? await runBuiltin(argv, deps, context) : await runTool(command, [...argv.slice(0, commandAt), ...argv.slice(commandAt + 1)], deps, context);
|
|
17225
17298
|
} catch (e) {
|
|
17226
17299
|
if (e instanceof UsageError) {
|
|
@@ -17329,6 +17402,73 @@ async function runBuiltin(argv, deps, context) {
|
|
|
17329
17402
|
return 2;
|
|
17330
17403
|
}
|
|
17331
17404
|
}
|
|
17405
|
+
var WAIT_USAGE = `Usage: antidote wait-for-deployment --product-id <id> --commit-sha <sha> [flags]
|
|
17406
|
+
|
|
17407
|
+
Poll the environment's URL, as Duku has it, until the response header names
|
|
17408
|
+
the commit (its full or 7-character SHA).
|
|
17409
|
+
|
|
17410
|
+
--product-id <id> The Duku product
|
|
17411
|
+
--environment <name> The environment (default: the product's default)
|
|
17412
|
+
--commit-sha <sha> The commit to wait for
|
|
17413
|
+
--commit-header <name> Response header the app exposes its deployed
|
|
17414
|
+
commit on (default ${DEFAULT_WAIT_COMMIT_HEADER})
|
|
17415
|
+
--commit-path <path> Path polled on the environment's URL (default ${DEFAULT_WAIT_COMMIT_PATH})
|
|
17416
|
+
--timeout-seconds <n> How long to wait before failing (default ${DEFAULT_WAIT_TIMEOUT_SECONDS})
|
|
17417
|
+
--interval-seconds <n> Delay between polls (default ${DEFAULT_WAIT_INTERVAL_SECONDS})`;
|
|
17418
|
+
async function runWaitForDeployment(args, deps, context) {
|
|
17419
|
+
let values;
|
|
17420
|
+
try {
|
|
17421
|
+
({ values } = parseArgs2({
|
|
17422
|
+
args,
|
|
17423
|
+
options: {
|
|
17424
|
+
"product-id": { type: "string" },
|
|
17425
|
+
environment: { type: "string", default: "" },
|
|
17426
|
+
"commit-sha": { type: "string" },
|
|
17427
|
+
"commit-header": { type: "string", default: DEFAULT_WAIT_COMMIT_HEADER },
|
|
17428
|
+
"commit-path": { type: "string", default: DEFAULT_WAIT_COMMIT_PATH },
|
|
17429
|
+
"timeout-seconds": { type: "string", default: String(DEFAULT_WAIT_TIMEOUT_SECONDS) },
|
|
17430
|
+
"interval-seconds": { type: "string", default: String(DEFAULT_WAIT_INTERVAL_SECONDS) },
|
|
17431
|
+
help: { type: "boolean", short: "h", default: false }
|
|
17432
|
+
}
|
|
17433
|
+
}));
|
|
17434
|
+
} catch (e) {
|
|
17435
|
+
deps.err(e.message);
|
|
17436
|
+
deps.err(WAIT_USAGE);
|
|
17437
|
+
return 2;
|
|
17438
|
+
}
|
|
17439
|
+
if (values.help) {
|
|
17440
|
+
deps.out(WAIT_USAGE);
|
|
17441
|
+
return 0;
|
|
17442
|
+
}
|
|
17443
|
+
const productId = values["product-id"]?.trim();
|
|
17444
|
+
const commitSha = values["commit-sha"]?.trim();
|
|
17445
|
+
const headerName = values["commit-header"].trim();
|
|
17446
|
+
const timeoutSeconds = Number(values["timeout-seconds"]);
|
|
17447
|
+
const intervalSeconds = Number(values["interval-seconds"]);
|
|
17448
|
+
if (!productId || !commitSha || !headerName || !(timeoutSeconds > 0) || !(intervalSeconds > 0)) {
|
|
17449
|
+
deps.err("--product-id, --commit-sha and a --commit-header are required, and the seconds must be numbers above 0.");
|
|
17450
|
+
deps.err(WAIT_USAGE);
|
|
17451
|
+
return 2;
|
|
17452
|
+
}
|
|
17453
|
+
const result = await (await context().session()).callTool("get_product", { product_id: productId });
|
|
17454
|
+
if (result.isError) {
|
|
17455
|
+
deps.err(`Error: ${printable(contentText(result.content)) || "get_product failed"}`);
|
|
17456
|
+
return 1;
|
|
17457
|
+
}
|
|
17458
|
+
const environments = result.structuredContent?.environments ?? [];
|
|
17459
|
+
const pollUrl = buildPollUrl(environmentUrl(environments, values.environment.trim()), values["commit-path"]);
|
|
17460
|
+
await waitForDeployment({
|
|
17461
|
+
pollUrl,
|
|
17462
|
+
expectedSha: commitSha,
|
|
17463
|
+
headerName,
|
|
17464
|
+
timeoutSeconds,
|
|
17465
|
+
intervalSeconds,
|
|
17466
|
+
probe: fetchProbe(deps.http, headerName),
|
|
17467
|
+
sleep: deps.sleep,
|
|
17468
|
+
log: (line) => deps.err(printable(line))
|
|
17469
|
+
});
|
|
17470
|
+
return 0;
|
|
17471
|
+
}
|
|
17332
17472
|
async function runTool(name, args, deps, context) {
|
|
17333
17473
|
const files = {
|
|
17334
17474
|
writeFile: deps.writeFile ?? ((path, data) => writeFile2(path, data, { flag: "wx" })),
|
|
@@ -17428,7 +17568,7 @@ var code = await run(process.argv.slice(2), {
|
|
|
17428
17568
|
err: (line) => process.stderr.write(`${line}
|
|
17429
17569
|
`),
|
|
17430
17570
|
openBrowser,
|
|
17431
|
-
version: true ? "
|
|
17571
|
+
version: true ? "1.0.0" : "0.0.0-dev"
|
|
17432
17572
|
});
|
|
17433
17573
|
process.exitCode = code;
|
|
17434
17574
|
/*! Bundled license information:
|