@meffecta/agent 1.0.2 → 1.0.7
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 +50 -2
- package/bin/meffecta-agent.js +66 -14
- package/deployment.env.example +24 -11
- package/engine.json +2 -2
- package/lib/analytics.js +262 -0
- package/lib/commands.js +244 -22
- package/lib/create-job.js +344 -0
- package/lib/doctor.js +36 -8
- package/lib/gcloud.js +42 -13
- package/lib/integrations.js +53 -2
- package/lib/resources.js +300 -0
- package/lib/verify-credentials.js +129 -0
- package/lib/version.js +90 -0
- package/package.json +3 -2
- package/scripts/create-project.sh +3 -1
- package/scripts/deploy.sh +20 -9
- package/scripts/lib/cli-names.sh +55 -0
- package/scripts/lib/deployment.sh +18 -3
- package/scripts/lib/read-json.mjs +1 -1
- package/scripts/lib/tasks-queue.sh +24 -0
- package/scripts/link-billing.sh +1 -1
- package/scripts/mint-gmail-token.mjs +7 -2
- package/scripts/mint-graph-token.mjs +7 -2
- package/scripts/set-secret.sh +2 -2
- package/scripts/setup-infrastructure.sh +187 -100
- package/scripts/{setup-scheduler.sh → sync-triggers.sh} +31 -29
- package/scripts/update-tooling.sh +3 -1
- package/scripts/verify-credentials.mjs +220 -49
package/lib/commands.js
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { configPath, decideAnalytics, readConfig, resolvePosthog, writeConfig } from "./analytics.js";
|
|
4
5
|
import { requireDeployment, UserError } from "./config.js";
|
|
6
|
+
import { checkJobs, createJob } from "./create-job.js";
|
|
5
7
|
import { runDoctor } from "./doctor.js";
|
|
6
8
|
import { parseFlags } from "./flags.js";
|
|
7
9
|
import { api, requireCommand, stream } from "./gcloud.js";
|
|
8
10
|
import { connect } from "./integrations.js";
|
|
11
|
+
import { resources } from "./resources.js";
|
|
12
|
+
import { verifyCredentials } from "./verify-credentials.js";
|
|
13
|
+
import { cliVersion, installMethod, invocation, isNewer, latestPublished } from "./version.js";
|
|
9
14
|
|
|
10
15
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
16
|
|
|
17
|
+
// Re-exported so the entrypoint keeps one import site for everything it prints.
|
|
18
|
+
export { cliVersion };
|
|
19
|
+
|
|
12
20
|
/**
|
|
13
21
|
* Where the operator scripts are. In the published package they sit at its root; in a
|
|
14
22
|
* checkout of the engine repo they are one level up, beside cli/. Same files either way —
|
|
15
23
|
* the pack step copies them in, so the CLI is a front on exactly the scripts a deployment
|
|
16
24
|
* would otherwise run by hand.
|
|
25
|
+
*
|
|
26
|
+
* **A checkout wins outright**, even when a vendored copy is present. `prepack` leaves one
|
|
27
|
+
* in cli/scripts, it is gitignored so it never shows up in `git status`, and a linked CLI
|
|
28
|
+
* reading that frozen snapshot is how you edit a script, run the command, and watch it
|
|
29
|
+
* behave exactly as though you had not. Same failure as a stale engine.json pinning a tag
|
|
30
|
+
* that was never published, and it gets the same guard: the engine's source sitting
|
|
31
|
+
* alongside means this is the repo, not an install of it.
|
|
17
32
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
33
|
+
const inCheckout = existsSync(resolve(packageRoot, "..", "src", "index.ts"));
|
|
34
|
+
export const scriptsDir = [
|
|
35
|
+
...(inCheckout ? [resolve(packageRoot, "..", "scripts")] : []),
|
|
36
|
+
resolve(packageRoot, "scripts"),
|
|
37
|
+
resolve(packageRoot, "..", "scripts"),
|
|
38
|
+
].find((candidate) => existsSync(candidate));
|
|
21
39
|
|
|
22
40
|
/**
|
|
23
41
|
* The engine build this CLI was published alongside. CI writes it at pack time, so
|
|
@@ -45,14 +63,6 @@ export function engineTag() {
|
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
export function cliVersion() {
|
|
49
|
-
try {
|
|
50
|
-
return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")).version;
|
|
51
|
-
} catch {
|
|
52
|
-
return "unknown";
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
66
|
/** A command that is one of the shipped scripts, with every argument passed straight on. */
|
|
57
67
|
function script(file, { interpreter } = {}) {
|
|
58
68
|
return async (args) => {
|
|
@@ -64,7 +74,12 @@ function script(file, { interpreter } = {}) {
|
|
|
64
74
|
throw new UserError(`Missing script: ${file}`);
|
|
65
75
|
}
|
|
66
76
|
const runner = interpreter ?? (file.endsWith(".mjs") ? "node" : "bash");
|
|
67
|
-
|
|
77
|
+
// MEFFECTA_CLI is how a script knows it was reached through the CLI, and what to call
|
|
78
|
+
// the next step: `meffecta-agent deploy`, never ./scripts/deploy.sh. A deployment run
|
|
79
|
+
// by someone outside Meffecta cannot clone this repo and has never seen those files.
|
|
80
|
+
const { code, error } = await stream(runner, [path, ...args], {
|
|
81
|
+
env: { ...process.env, MEFFECTA_CLI: invocation() },
|
|
82
|
+
});
|
|
68
83
|
if (error) {
|
|
69
84
|
throw new UserError(`Could not run ${runner}: ${error.message}`);
|
|
70
85
|
}
|
|
@@ -72,6 +87,87 @@ function script(file, { interpreter } = {}) {
|
|
|
72
87
|
};
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
/** The engine tag the deployment is actually running, from the image on its live revision. */
|
|
91
|
+
async function runningEngineTag() {
|
|
92
|
+
try {
|
|
93
|
+
const d = requireDeployment();
|
|
94
|
+
const { capture, gcloudArgs } = await import("./gcloud.js");
|
|
95
|
+
const json = capture(
|
|
96
|
+
"gcloud",
|
|
97
|
+
gcloudArgs(d, [
|
|
98
|
+
"run",
|
|
99
|
+
"services",
|
|
100
|
+
"describe",
|
|
101
|
+
d.SERVICE,
|
|
102
|
+
`--region=${d.REGION}`,
|
|
103
|
+
"--format=value(spec.template.spec.containers[0].image)",
|
|
104
|
+
]),
|
|
105
|
+
);
|
|
106
|
+
return json.trim().split(":").pop() || undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
// No deployment.env, no gcloud, no service yet — all fine, this line is just omitted.
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Am I current? Three numbers, because being "up to date" here has three parts and the
|
|
115
|
+
* dangerous state is between them: new tooling pins a NEW engine tag, but the deployment
|
|
116
|
+
* keeps running the old image until `deploy`. Tooling that provisions a shape the running
|
|
117
|
+
* engine does not implement is exactly what releasing the pair together prevents — and it
|
|
118
|
+
* reopens here, on the operator's machine, at upgrade time.
|
|
119
|
+
*/
|
|
120
|
+
async function upgrade(args) {
|
|
121
|
+
const checkOnly = args.includes("--check");
|
|
122
|
+
const installed = cliVersion();
|
|
123
|
+
const latest = await latestPublished();
|
|
124
|
+
const running = await runningEngineTag();
|
|
125
|
+
const how = installMethod();
|
|
126
|
+
|
|
127
|
+
console.log(`▶ @meffecta/agent`);
|
|
128
|
+
console.log(` installed ${installed}${how.kind === "checkout" ? " (a checkout, not an installed package)" : ""}`);
|
|
129
|
+
console.log(` published ${latest ?? "unknown — could not reach the npm registry"}`);
|
|
130
|
+
if (running) {
|
|
131
|
+
console.log(` engine ${running} (what your deployment is running now)`);
|
|
132
|
+
}
|
|
133
|
+
console.log("");
|
|
134
|
+
|
|
135
|
+
const behind = latest && isNewer(latest, installed);
|
|
136
|
+
if (!latest) {
|
|
137
|
+
console.log("Could not check for a newer version. Being offline is not a problem — this");
|
|
138
|
+
console.log("command only reads the registry; nothing else needs it.");
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (!behind) {
|
|
142
|
+
console.log(`Up to date.`);
|
|
143
|
+
} else {
|
|
144
|
+
console.log(`A newer version is out: ${installed} → ${latest}`);
|
|
145
|
+
if (how.note) {
|
|
146
|
+
console.log(` ${how.note}`);
|
|
147
|
+
}
|
|
148
|
+
console.log("");
|
|
149
|
+
console.log(` ${how.upgrade}`);
|
|
150
|
+
if (how.kind !== "checkout") {
|
|
151
|
+
console.log(` meffecta-agent deploy`);
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log("Both lines. The first gets tooling that pins a newer engine; the second is");
|
|
154
|
+
console.log("what actually moves your deployment onto it. Stopping after the first leaves");
|
|
155
|
+
console.log("tooling that provisions a shape the running engine may not implement.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A deployment can be behind the CLI without the CLI being behind npm — someone upgraded
|
|
160
|
+
// the tooling and never deployed. That is the half-upgraded state, and it is worth saying
|
|
161
|
+
// out loud rather than leaving to be noticed.
|
|
162
|
+
const { tag, pinned } = engineTag();
|
|
163
|
+
if (pinned && running && running !== tag && !checkOnly) {
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(`⚠ This CLI deploys engine ${tag}, but the deployment is running ${running}.`);
|
|
166
|
+
console.log(` meffecta-agent deploy (rolls it onto ${tag} and re-syncs the triggers)`);
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
75
171
|
/** What the deployment looks like right now: revision, shape, triggers, pending work. */
|
|
76
172
|
async function status() {
|
|
77
173
|
requireCommand("gcloud", "the CLI reads your deployment through it");
|
|
@@ -116,7 +212,7 @@ async function status() {
|
|
|
116
212
|
const crons = jobs.filter((name) => name.startsWith(`${d.SERVICE}-job-`));
|
|
117
213
|
const sweep = jobs.find((name) => name === `${d.SERVICE}-sweep`);
|
|
118
214
|
console.log(
|
|
119
|
-
` scheduler ${crons.length} cron trigger(s)${sweep ? " + sweep" : " — NO SWEEP (run
|
|
215
|
+
` scheduler ${crons.length} cron trigger(s)${sweep ? " + sweep" : " — NO SWEEP (run sync-triggers)"}`,
|
|
120
216
|
);
|
|
121
217
|
if (crons.length === 0) {
|
|
122
218
|
console.log(" ⚠ scaled to zero with no triggers: this service runs nothing.");
|
|
@@ -223,6 +319,67 @@ async function ask(args) {
|
|
|
223
319
|
return 0;
|
|
224
320
|
}
|
|
225
321
|
|
|
322
|
+
/**
|
|
323
|
+
* `analytics` — the transparency surface, and the switch.
|
|
324
|
+
*
|
|
325
|
+
* Nothing is collected that could identify a deployment, but "trust me" is not a policy.
|
|
326
|
+
* This prints the exhaustive list of what is and is not sent, where the choice is stored,
|
|
327
|
+
* the state and the reason for it, and the one command that turns it off.
|
|
328
|
+
*/
|
|
329
|
+
const ANALYTICS_REASONS = {
|
|
330
|
+
"do-not-track": "the DO_NOT_TRACK environment variable is set",
|
|
331
|
+
"env-disabled": "MEFFECTA_ANALYTICS_DISABLED is set",
|
|
332
|
+
"config-disabled": "you ran `meffecta-agent analytics off`",
|
|
333
|
+
"config-enabled": "you ran `meffecta-agent analytics on`",
|
|
334
|
+
ci: "this looks like CI, where it is off by default",
|
|
335
|
+
default: "the default (on, and one command to turn off)",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
async function analytics(args) {
|
|
339
|
+
const [action] = args;
|
|
340
|
+
if (action === "on" || action === "enable") {
|
|
341
|
+
writeConfig({ analytics: "enabled" });
|
|
342
|
+
console.log("✔ Anonymous analytics on. It steers what gets fixed and documented next.");
|
|
343
|
+
console.log(` Recorded in ${configPath()}`);
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
if (action === "off" || action === "disable") {
|
|
347
|
+
writeConfig({ analytics: "disabled" });
|
|
348
|
+
console.log("✔ Anonymous analytics off. Nothing further is sent from this machine.");
|
|
349
|
+
console.log(` Recorded in ${configPath()}`);
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
if (action && action !== "status") {
|
|
353
|
+
throw new UserError(`Unknown: analytics ${action}. Try: analytics status | on | off`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const config = readConfig();
|
|
357
|
+
const decision = decideAnalytics({ config });
|
|
358
|
+
const { host, key } = resolvePosthog();
|
|
359
|
+
console.log(`Analytics: ${decision.enabled ? "ON" : "OFF"} — ${ANALYTICS_REASONS[decision.reason]}.`);
|
|
360
|
+
console.log(` choice stored in ${configPath()}`);
|
|
361
|
+
console.log(` anonymous id ${config.anonymousId ?? "(none yet — assigned on the first run with it on)"}`);
|
|
362
|
+
if (decision.enabled) {
|
|
363
|
+
console.log(` sent to ${key ? host : "nowhere — no key is configured in this build"}`);
|
|
364
|
+
}
|
|
365
|
+
console.log("");
|
|
366
|
+
console.log("Sent, and nothing else:");
|
|
367
|
+
console.log(" • which command you ran, from the fixed list — never a word you typed");
|
|
368
|
+
console.log(" • whether it worked, how long it took, and the class of any error");
|
|
369
|
+
console.log(" • this CLI's version, the engine tag it deploys, Node, OS, architecture");
|
|
370
|
+
console.log(" • how you installed it, and whether a deployment.env was found");
|
|
371
|
+
console.log(" • a random id, made on this machine, so repeat runs count as one install");
|
|
372
|
+
console.log("");
|
|
373
|
+
console.log("Never sent: your GCP project or service name, region, repo URLs, account or");
|
|
374
|
+
console.log("email, file paths, arguments, secret names or values, job names, prompts —");
|
|
375
|
+
console.log("or any error message. Only the error's class, because messages quote paths");
|
|
376
|
+
console.log("and project ids.");
|
|
377
|
+
console.log("");
|
|
378
|
+
console.log("Turn it off: meffecta-agent analytics off (or set DO_NOT_TRACK=1)");
|
|
379
|
+
console.log("Turn it on: meffecta-agent analytics on");
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
226
383
|
async function sweep() {
|
|
227
384
|
requireCommand("gcloud", "the sweep is a Cloud Scheduler job");
|
|
228
385
|
const d = requireDeployment();
|
|
@@ -245,7 +402,7 @@ async function envList() {
|
|
|
245
402
|
);
|
|
246
403
|
const entries = svc.spec.template.spec.containers[0].env ?? [];
|
|
247
404
|
if (entries.length === 0) {
|
|
248
|
-
console.log("Nothing set. At minimum this service needs GIT_REPO_URL and
|
|
405
|
+
console.log("Nothing set. At minimum this service needs GIT_REPO_URL and AGENT_API_SECRET.");
|
|
249
406
|
return 0;
|
|
250
407
|
}
|
|
251
408
|
const width = Math.max(...entries.map((e) => e.name.length));
|
|
@@ -281,7 +438,7 @@ async function secretsList() {
|
|
|
281
438
|
);
|
|
282
439
|
if (secrets.length === 0) {
|
|
283
440
|
console.log("No secrets in this project yet. Start with:");
|
|
284
|
-
console.log(" meffecta-agent set-secret
|
|
441
|
+
console.log(" meffecta-agent set-secret AGENT_API_SECRET --random");
|
|
285
442
|
return 0;
|
|
286
443
|
}
|
|
287
444
|
const width = Math.max(...secrets.map((name) => name.length));
|
|
@@ -318,7 +475,7 @@ async function triggers() {
|
|
|
318
475
|
|
|
319
476
|
if (rows.length === 0) {
|
|
320
477
|
console.log("No triggers. If this service is scaled to zero it is running nothing:");
|
|
321
|
-
console.log(" meffecta-agent
|
|
478
|
+
console.log(" meffecta-agent sync-triggers");
|
|
322
479
|
return 0;
|
|
323
480
|
}
|
|
324
481
|
const width = Math.max(...rows.map(([id]) => id.length));
|
|
@@ -341,7 +498,7 @@ async function triggers() {
|
|
|
341
498
|
).split("\t");
|
|
342
499
|
console.log(`\nTask queue ${d.SERVICE}-runs: ${queue[0] ?? "?"}, ${queue[1] || 0} task(s) waiting`);
|
|
343
500
|
} catch {
|
|
344
|
-
console.log(`\nTask queue ${d.SERVICE}-runs: not created — run setup-
|
|
501
|
+
console.log(`\nTask queue ${d.SERVICE}-runs: not created — run setup-infra`);
|
|
345
502
|
}
|
|
346
503
|
return 0;
|
|
347
504
|
}
|
|
@@ -364,8 +521,44 @@ async function logs(args) {
|
|
|
364
521
|
}
|
|
365
522
|
|
|
366
523
|
/** Start a deployment.env in the current directory, from the template in this package. */
|
|
524
|
+
// Scaffolded by `init` into a new content repo. Deliberately short: an empty table with the
|
|
525
|
+
// columns that matter is a thing an operator fills in, where a long example is a thing they
|
|
526
|
+
// delete.
|
|
527
|
+
/**
|
|
528
|
+
* One example entry, scaffolded so the shape is obvious from a real file rather than from
|
|
529
|
+
* documentation. What the fields MEAN lives in SYSTEM.base.md, because every run needs to
|
|
530
|
+
* know it and no content repo should be carrying its own copy of an engine rule.
|
|
531
|
+
*/
|
|
532
|
+
const REGISTER_EXAMPLE = `---
|
|
533
|
+
system: Example — rename this file after the system, and delete it once you have a real one
|
|
534
|
+
skill: cloudflare
|
|
535
|
+
requires: CLOUDFLARE_API_KEY, CLOUDFLARE_ACCOUNT_ID
|
|
536
|
+
access: read; writes only when a job says so explicitly
|
|
537
|
+
probe: list zones
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
One file per system, in this directory. The agent reads them **at run time**: its skills
|
|
541
|
+
are shared across every deployment and name no deployment's variables, so a skill says what
|
|
542
|
+
kind of credential it needs and comes here for the name.
|
|
543
|
+
|
|
544
|
+
**A system with no file here is one the agent will report as unavailable.** That is the
|
|
545
|
+
point of the register, and it is why an absence means as much as an entry.
|
|
546
|
+
|
|
547
|
+
- \`requires\` is exact variable names, all of which must be set. Use \`none\` for a system
|
|
548
|
+
that authenticates as the runtime service account instead.
|
|
549
|
+
- \`selectors\` is for optional per-project pointers — one per world.
|
|
550
|
+
- \`access\` is what a JOB may do, written honestly: "read-only", "may send mail", "may
|
|
551
|
+
change DNS". It is what a person reads when deciding whether to let a job run unattended.
|
|
552
|
+
- \`probe\` is the cheapest read that proves the credential works, with no side effect —
|
|
553
|
+
list domains rather than send, read quota rather than generate.
|
|
554
|
+
|
|
555
|
+
Set the values with \`meffecta-agent set-secret <NAME>\` (credentials) or
|
|
556
|
+
\`set-env <NAME>=<value>\` (ids, addresses, URLs); \`meffecta-agent connect\` lists the
|
|
557
|
+
systems the engine already has skills for and walks through each.
|
|
558
|
+
`;
|
|
559
|
+
|
|
367
560
|
async function init(args) {
|
|
368
|
-
const { copyFileSync, existsSync: exists } = await import("node:fs");
|
|
561
|
+
const { copyFileSync, existsSync: exists, writeFileSync } = await import("node:fs");
|
|
369
562
|
const target = resolve(process.cwd(), "deployment.env");
|
|
370
563
|
if (exists(target) && !args.includes("--force")) {
|
|
371
564
|
console.log(`deployment.env already exists here. Leaving it alone (--force to replace).`);
|
|
@@ -383,6 +576,22 @@ async function init(args) {
|
|
|
383
576
|
console.log("Fill in PROJECT and SERVICE — those two are never guessed — then commit it.");
|
|
384
577
|
console.log("It belongs in your content repo, beside jobs/, because which GCP project a");
|
|
385
578
|
console.log("set of jobs runs as is a fact about the deployment and none of it is secret.\n");
|
|
579
|
+
|
|
580
|
+
// The register is read by the agent itself at run time, not just by people: the skills
|
|
581
|
+
// are shared across deployments and name no deployment's variables, so without it a run
|
|
582
|
+
// knows how a system works and not which variable holds its credential.
|
|
583
|
+
const { mkdirSync } = await import("node:fs");
|
|
584
|
+
const registerDir = resolve(process.cwd(), "systems");
|
|
585
|
+
if (exists(registerDir)) {
|
|
586
|
+
console.log("systems/ already exists here. Leaving it alone.\n");
|
|
587
|
+
} else {
|
|
588
|
+
mkdirSync(registerDir, { recursive: true });
|
|
589
|
+
writeFileSync(resolve(registerDir, "example.md"), REGISTER_EXAMPLE);
|
|
590
|
+
console.log("✅ Wrote systems/example.md — the register of what this agent can reach.\n");
|
|
591
|
+
console.log("One file per system, added as you connect each one. The agent reads them at");
|
|
592
|
+
console.log("run time to find out which variable holds which credential, so a system with");
|
|
593
|
+
console.log("no file here is one it will report as unavailable.\n");
|
|
594
|
+
}
|
|
386
595
|
console.log("Next: meffecta-agent steps");
|
|
387
596
|
return 0;
|
|
388
597
|
}
|
|
@@ -407,13 +616,17 @@ export const GROUPS = [
|
|
|
407
616
|
["init", "Start a deployment.env here, from the template", init],
|
|
408
617
|
[
|
|
409
618
|
"setup-infra",
|
|
410
|
-
"Provision GCP: registry, buckets, service account, service shell",
|
|
619
|
+
"Provision GCP: registry, buckets, service account, run queue, service shell",
|
|
411
620
|
script("setup-infrastructure.sh"),
|
|
412
621
|
],
|
|
413
|
-
["set-secret", "
|
|
622
|
+
["set-secret", "AGENT_API_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN", script("set-secret.sh")],
|
|
414
623
|
["set-env", "GIT_REPO_URL — the one setting the service will not boot without", script("set-env.sh")],
|
|
415
624
|
["deploy", "Roll out an engine image and assert the runtime shape", script("deploy.sh")],
|
|
416
|
-
[
|
|
625
|
+
[
|
|
626
|
+
"sync-triggers",
|
|
627
|
+
"Re-sync the Cloud Scheduler triggers without a rollout (deploy does it)",
|
|
628
|
+
script("sync-triggers.sh"),
|
|
629
|
+
],
|
|
417
630
|
],
|
|
418
631
|
},
|
|
419
632
|
{
|
|
@@ -447,15 +660,24 @@ export const GROUPS = [
|
|
|
447
660
|
["secrets", "What is in Secret Manager, and whether the service reads it", secretsList],
|
|
448
661
|
["triggers", "The Cloud Scheduler jobs and task queue that drive it", triggers],
|
|
449
662
|
["logs", "Recent service logs (--limit N)", logs],
|
|
663
|
+
["check-jobs", "Validate the job files here — after you have edited one by hand", checkJobs],
|
|
664
|
+
["analytics", "What anonymous usage data is sent, and how to turn it off", analytics],
|
|
665
|
+
["resources", "Everything the set-up built in Google Cloud, and what each part is for", resources],
|
|
450
666
|
],
|
|
451
667
|
},
|
|
452
668
|
{
|
|
453
669
|
title: "Operate it",
|
|
454
670
|
commands: [
|
|
671
|
+
["create-job", 'Write a new job from a description: create-job "a report every Wednesday 2pm"', createJob],
|
|
455
672
|
["run", "Trigger one job now, or --in <seconds>", runJob],
|
|
456
673
|
["ask", "Ask it something as a one-off run", ask],
|
|
457
674
|
["sweep", "Run the housekeeping sweep now", sweep],
|
|
458
|
-
[
|
|
675
|
+
[
|
|
676
|
+
"verify-credentials",
|
|
677
|
+
"Test every credential the deployment holds, from inside it (--quick skips the slow half)",
|
|
678
|
+
verifyCredentials(ask),
|
|
679
|
+
],
|
|
680
|
+
["upgrade", "Check for a newer CLI, and what your deployment is running", upgrade],
|
|
459
681
|
["update-tooling", "Refresh the shipped scripts from an engine image (docker path)", script("update-tooling.sh")],
|
|
460
682
|
],
|
|
461
683
|
},
|