@meffecta/agent 1.0.10 → 1.0.11
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 +0 -1
- package/bin/meffecta-agent.js +3 -48
- package/deployment.env.example +4 -15
- package/engine.json +2 -2
- package/lib/commands.js +111 -32
- package/lib/doctor.js +1 -1
- package/package.json +1 -1
- package/scripts/deploy.sh +9 -65
- package/scripts/lib/deployment.sh +4 -6
- package/scripts/set-artifact-cleanup.sh +1 -1
- package/scripts/setup-infrastructure.sh +20 -27
- package/scripts/update-tooling.sh +4 -5
package/README.md
CHANGED
|
@@ -92,7 +92,6 @@ not consent. Secret values are never printed, by any command.
|
|
|
92
92
|
## Operating it
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
npx @meffecta/agent steps # the set-up, in order
|
|
96
95
|
npx @meffecta/agent help # every command — each also takes --help
|
|
97
96
|
npx @meffecta/agent doctor # the one to run when something feels wrong
|
|
98
97
|
```
|
package/bin/meffecta-agent.js
CHANGED
|
@@ -18,40 +18,14 @@ import { installMethod } from "../lib/version.js";
|
|
|
18
18
|
|
|
19
19
|
// Tools first: every command below needs gcloud, and the very first one already does. The
|
|
20
20
|
// claude CLI is the odd one out — it is used once, to mint the token, and never again.
|
|
21
|
-
const STEPS = [
|
|
22
|
-
[
|
|
23
|
-
"",
|
|
24
|
-
"Install gcloud, docker, node 24+ and git. Also the claude CLI — it is\n used once, for the token two steps down, and not after that.",
|
|
25
|
-
],
|
|
26
|
-
[
|
|
27
|
-
"create-project",
|
|
28
|
-
"A new GCP project. This and the next step are per GCP account, not\n per deployment — skip both if you are adding a second agent to a project you\n already have.",
|
|
29
|
-
],
|
|
30
|
-
["link-billing", "Attach a billing account, or nothing can run."],
|
|
31
|
-
[
|
|
32
|
-
"mint-gmail",
|
|
33
|
-
"Credentials: `claude setup-token`, a GitHub token that can clone your repos,\n and — only if this agent reads or sends mail — one token per mailbox.",
|
|
34
|
-
],
|
|
35
|
-
[
|
|
36
|
-
"init",
|
|
37
|
-
"Create your content repo — jobs/, SYSTEM.md, systems/, and a deployment.env in it.\n Run everything from there; it is what says which deployment you mean.",
|
|
38
|
-
],
|
|
39
|
-
["setup-infra", "Provision GCP, the run queue, and the Cloud Run service shell.\n --dry-run shows it first."],
|
|
40
|
-
["set-secret", "AGENT_API_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN."],
|
|
41
|
-
["set-env", "GIT_REPO_URL=… — the service will not boot without it."],
|
|
42
|
-
[
|
|
43
|
-
"deploy",
|
|
44
|
-
"Roll out the engine image, then create its triggers — scaled to zero it fires\n nothing without them, and only the running engine knows which jobs exist.",
|
|
45
|
-
],
|
|
46
|
-
["doctor", 'Check it. Then: jobs, run <job>, ask "…".'],
|
|
47
|
-
];
|
|
48
21
|
|
|
49
22
|
function help() {
|
|
50
23
|
const { tag, pinned } = engineTag();
|
|
51
24
|
console.log("meffecta-agent — set up and operate a Meffecta Agent deployment\n");
|
|
52
25
|
console.log("Usage: meffecta-agent <command> [options]");
|
|
53
26
|
console.log(" npx @meffecta/agent <command> [options]\n");
|
|
54
|
-
console.log("Run it from your content repo — the directory holding deployment.env and jobs
|
|
27
|
+
console.log("Run it from your content repo — the directory holding deployment.env and jobs/.");
|
|
28
|
+
console.log("Setting one up for the first time: https://agent.meffecta.com\n");
|
|
55
29
|
for (const group of GROUPS) {
|
|
56
30
|
console.log(`${group.title}:`);
|
|
57
31
|
const width = Math.max(...group.commands.map(([name]) => name.length));
|
|
@@ -63,7 +37,6 @@ function help() {
|
|
|
63
37
|
}
|
|
64
38
|
console.log("");
|
|
65
39
|
}
|
|
66
|
-
console.log(" steps The whole set-up, in order, with the command for each");
|
|
67
40
|
console.log(" version This CLI and the engine build it deploys");
|
|
68
41
|
console.log(" upgrade Whether a newer one is out, and what to run\n");
|
|
69
42
|
console.log("Every command takes --help, and every option it documents — so");
|
|
@@ -71,20 +44,6 @@ function help() {
|
|
|
71
44
|
console.log(`Engine: ghcr.io/meffecta/agent:${tag}${pinned ? " (pinned to this release)" : ""}`);
|
|
72
45
|
}
|
|
73
46
|
|
|
74
|
-
function steps() {
|
|
75
|
-
console.log("Setting up a deployment, in order.\n");
|
|
76
|
-
STEPS.forEach(([command, note], index) => {
|
|
77
|
-
const label = `${String(index + 1).padStart(2)}.`;
|
|
78
|
-
console.log(`${label} ${note}`);
|
|
79
|
-
if (command) {
|
|
80
|
-
console.log(` meffecta-agent ${command}`);
|
|
81
|
-
}
|
|
82
|
-
console.log("");
|
|
83
|
-
});
|
|
84
|
-
console.log("Each step is checkable: `meffecta-agent doctor` after the last one should come");
|
|
85
|
-
console.log("back clean, and `meffecta-agent jobs` should list your jobs.");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
47
|
function version() {
|
|
89
48
|
const { tag, pinned } = engineTag();
|
|
90
49
|
const deployment = loadDeployment();
|
|
@@ -104,7 +63,7 @@ const { analytics, firstRun } = openAnalytics({ version: cliVersion() });
|
|
|
104
63
|
// The name is matched against the known commands before it is ever recorded. Whatever an
|
|
105
64
|
// operator actually typed is theirs — a mistyped command could be anything, including
|
|
106
65
|
// something they would not want leaving the machine.
|
|
107
|
-
const BUILT_IN = new Set(["help", "
|
|
66
|
+
const BUILT_IN = new Set(["help", "version"]);
|
|
108
67
|
function recorded(candidate) {
|
|
109
68
|
if (!candidate) {
|
|
110
69
|
return "help";
|
|
@@ -146,10 +105,6 @@ if (!name || name === "help" || name === "--help" || name === "-h") {
|
|
|
146
105
|
help();
|
|
147
106
|
await finish(0, "ok");
|
|
148
107
|
}
|
|
149
|
-
if (name === "steps") {
|
|
150
|
-
steps();
|
|
151
|
-
await finish(0, "ok");
|
|
152
|
-
}
|
|
153
108
|
if (name === "version" || name === "--version" || name === "-v") {
|
|
154
109
|
version();
|
|
155
110
|
await finish(0, "ok");
|
package/deployment.env.example
CHANGED
|
@@ -20,20 +20,12 @@ PROJECT=acme-agent-123456
|
|
|
20
20
|
# The Cloud Run service.
|
|
21
21
|
SERVICE=acme-agent
|
|
22
22
|
|
|
23
|
-
#
|
|
23
|
+
# A repository in that project standing in front of ghcr.io. Cloud Run can only pull from
|
|
24
|
+
# Artifact Registry, so this is how it reaches the engine image: AR fetches the public
|
|
25
|
+
# image itself, on demand, and caches it. Nothing is copied by hand and no docker is
|
|
26
|
+
# needed on your machine at any point.
|
|
24
27
|
ARTIFACT_REPO=acme-agent-images
|
|
25
28
|
|
|
26
|
-
# How the engine image reaches Cloud Run, which can only pull from Artifact Registry.
|
|
27
|
-
# proxy (default) ARTIFACT_REPO is a remote repository in front of ghcr.io. AR fetches
|
|
28
|
-
# the public engine image on demand — nothing is copied, and no docker is needed
|
|
29
|
-
# on your machine at any point.
|
|
30
|
-
# mirror the older path: pull from ghcr.io here, retag, push. Needs docker running and
|
|
31
|
-
# `gcloud auth configure-docker <region>-docker.pkg.dev`, and pushes several GB
|
|
32
|
-
# per engine version. Use it only if your policy forbids AR reaching the internet.
|
|
33
|
-
# A repository's mode is fixed when it is created, so changing this means a new
|
|
34
|
-
# ARTIFACT_REPO name and another setup-infrastructure run.
|
|
35
|
-
REGISTRY_MODE=proxy
|
|
36
|
-
|
|
37
29
|
REGION=europe-west1
|
|
38
30
|
|
|
39
31
|
# Optional. Unset = whichever account gcloud is logged in as. Set it when you work across
|
|
@@ -43,9 +35,6 @@ REGION=europe-west1
|
|
|
43
35
|
# Optional. The service's TIMEZONE env var, which is what cron schedules are read in.
|
|
44
36
|
# TIMEZONE=Europe/Stockholm
|
|
45
37
|
|
|
46
|
-
# Optional. Where published engine images come from.
|
|
47
|
-
# GHCR_IMAGE=ghcr.io/meffecta/agent
|
|
48
|
-
|
|
49
38
|
# Optional. The runtime shape asserted on every rollout, so the deployed service
|
|
50
39
|
# matches this file rather than whatever was last changed by hand. The defaults below are
|
|
51
40
|
# what `meffecta-agent setup-infra` provisions, so leaving them out is the same as setting
|
package/engine.json
CHANGED
package/lib/commands.js
CHANGED
|
@@ -52,8 +52,8 @@ export function engineTag() {
|
|
|
52
52
|
const { engineTag: tag, builtFrom } = JSON.parse(readFileSync(file, "utf8"));
|
|
53
53
|
// Only a release is pinned. `npm pack` run by hand writes this file into the working
|
|
54
54
|
// tree with builtFrom "local", where it would otherwise survive and pin `deploy` to a
|
|
55
|
-
// version that was never published
|
|
56
|
-
//
|
|
55
|
+
// version that was never published, where the pull fails with nothing to say why that
|
|
56
|
+
// tag was chosen.
|
|
57
57
|
if (!tag || builtFrom === "local" || existsSync(resolve(packageRoot, "..", "src", "index.ts"))) {
|
|
58
58
|
return { tag: "latest", pinned: false };
|
|
59
59
|
}
|
|
@@ -584,13 +584,65 @@ Set the values with \`meffecta-agent set-secret <NAME>\` (credentials) or
|
|
|
584
584
|
systems the engine already has skills for and walks through each.
|
|
585
585
|
`;
|
|
586
586
|
|
|
587
|
+
/**
|
|
588
|
+
* The deployment prompt, put in front of every job's own prompt. A starter rather than an
|
|
589
|
+
* empty file, because the engine's baseline already states how a run works, what a run may
|
|
590
|
+
* act on and what it may never say — so what is left is the half only this organisation can
|
|
591
|
+
* write, and a blank file gives no hint which half that is.
|
|
592
|
+
*/
|
|
593
|
+
const SYSTEM_EXAMPLE = `# Who you work for
|
|
594
|
+
|
|
595
|
+
Replace all of this. It goes in front of every job's prompt, so it is for what holds across
|
|
596
|
+
all of them and only that — rules, not facts. What the agent should KNOW about a business
|
|
597
|
+
belongs in \`worlds/\`.
|
|
598
|
+
|
|
599
|
+
You work for **Acme AB**. You report to **Alex Doe, alex@acme.com**, and your output is for
|
|
600
|
+
Alex unless the job you are running names someone else.
|
|
601
|
+
|
|
602
|
+
## How to write
|
|
603
|
+
|
|
604
|
+
- Lead with what you found. No preamble, and do not restate the task.
|
|
605
|
+
- Short. This gets read between meetings.
|
|
606
|
+
- One thing you are unsure of, said plainly, is worth more than three you are not.
|
|
607
|
+
|
|
608
|
+
## People
|
|
609
|
+
|
|
610
|
+
- Alex Doe, alex@acme.com — runs the company. Everything comes here by default.
|
|
611
|
+
`;
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* A first job that proves the whole path — clone, system prompt, register, memory — and
|
|
615
|
+
* reaches nothing real while doing it. Named to match \`systems/example.md\`: both are the
|
|
616
|
+
* shape of the thing, shown once, and deleted when a real one exists.
|
|
617
|
+
*
|
|
618
|
+
* No \`cron:\`, deliberately. A scaffolded job that started firing on a schedule the moment
|
|
619
|
+
* the service booted would be the wrong first impression of what these files do.
|
|
620
|
+
*/
|
|
621
|
+
const JOB_EXAMPLE = `---
|
|
622
|
+
model: sonnet
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
A first run, to prove the plumbing. It reaches no real system and changes nothing.
|
|
626
|
+
|
|
627
|
+
Report, in under ten lines:
|
|
628
|
+
|
|
629
|
+
1. Who you work for — one line, from \`SYSTEM.md\`.
|
|
630
|
+
2. What you can reach — every file in \`systems/\`, and for each one whether the variables it
|
|
631
|
+
requires are set. **Names and set/unset only, never a value.** A system whose variables
|
|
632
|
+
are missing is one you would report as unavailable.
|
|
633
|
+
3. Where your memory directory is, and whether \`MEMORY.md\` is in it yet.
|
|
634
|
+
|
|
635
|
+
Then add one dated line to your \`MEMORY.md\` saying this ran.
|
|
636
|
+
|
|
637
|
+
When you have a real job, write it in a file of its own and delete this one. The frontmatter
|
|
638
|
+
above the \`---\` is the settings; everything below it is what you would have said to a
|
|
639
|
+
colleague.
|
|
640
|
+
`;
|
|
641
|
+
|
|
587
642
|
async function init(args) {
|
|
588
|
-
const { copyFileSync, existsSync: exists, writeFileSync } = await import("node:fs");
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
console.log(`deployment.env already exists here. Leaving it alone (--force to replace).`);
|
|
592
|
-
return 0;
|
|
593
|
-
}
|
|
643
|
+
const { copyFileSync, existsSync: exists, mkdirSync, writeFileSync } = await import("node:fs");
|
|
644
|
+
const force = args.includes("--force");
|
|
645
|
+
|
|
594
646
|
const template = [
|
|
595
647
|
resolve(packageRoot, "deployment.env.example"),
|
|
596
648
|
resolve(packageRoot, "..", "deployment.env.example"),
|
|
@@ -598,28 +650,55 @@ async function init(args) {
|
|
|
598
650
|
if (!template) {
|
|
599
651
|
throw new UserError("The deployment.env template is missing from this install — reinstall @meffecta/agent.");
|
|
600
652
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
653
|
+
|
|
654
|
+
// Four artifacts, each decided on its own. `init` is run in a repo that already holds
|
|
655
|
+
// some of them — after an upgrade, or because a first attempt stopped halfway — and one
|
|
656
|
+
// file being present must not decide the fate of the other three.
|
|
657
|
+
const SCAFFOLD = [
|
|
658
|
+
["deployment.env", "which GCP project and service this deployment is", (to) => copyFileSync(template, to)],
|
|
659
|
+
[
|
|
660
|
+
"SYSTEM.md",
|
|
661
|
+
"who the agent works for — read at the start of every run",
|
|
662
|
+
(to) => writeFileSync(to, SYSTEM_EXAMPLE),
|
|
663
|
+
],
|
|
664
|
+
[
|
|
665
|
+
"systems/example.md",
|
|
666
|
+
"one file per system it may reach, and the variables for each",
|
|
667
|
+
(to) => writeFileSync(to, REGISTER_EXAMPLE),
|
|
668
|
+
],
|
|
669
|
+
["jobs/example.md", "one file per job: a few settings, then the prompt", (to) => writeFileSync(to, JOB_EXAMPLE)],
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
const wrote = [];
|
|
673
|
+
const kept = [];
|
|
674
|
+
for (const [name, purpose, produce] of SCAFFOLD) {
|
|
675
|
+
const to = resolve(process.cwd(), name);
|
|
676
|
+
if (exists(to) && !force) {
|
|
677
|
+
kept.push([name, purpose]);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
681
|
+
produce(to);
|
|
682
|
+
wrote.push([name, purpose]);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const pad = Math.max(...SCAFFOLD.map(([name]) => name.length)) + 2;
|
|
686
|
+
for (const [name, purpose] of wrote) {
|
|
687
|
+
console.log(`✅ ${name.padEnd(pad)}${purpose}`);
|
|
688
|
+
}
|
|
689
|
+
for (const [name, purpose] of kept) {
|
|
690
|
+
console.log(`• ${name.padEnd(pad)}${purpose} — already here, left alone`);
|
|
691
|
+
}
|
|
692
|
+
if (kept.length && !force) {
|
|
693
|
+
console.log("\n--force replaces the ones already here.");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
console.log("\nFill in PROJECT and SERVICE in deployment.env — those two are never guessed —");
|
|
697
|
+
console.log("and say who the agent works for in SYSTEM.md. Both example files are the shape of");
|
|
698
|
+
console.log("the thing: rename them after something real, and delete what is left.");
|
|
699
|
+
console.log("\nThen commit all of it. None of it is secret, and a clone of this repo is");
|
|
700
|
+
console.log("everything an operator needs.");
|
|
701
|
+
console.log("\nThe set-up in order, with the command for each part: https://agent.meffecta.com");
|
|
623
702
|
return 0;
|
|
624
703
|
}
|
|
625
704
|
|
|
@@ -640,7 +719,7 @@ export const GROUPS = [
|
|
|
640
719
|
// next group so a first deployment is not a shopping list.
|
|
641
720
|
title: "Stand one up (once, in order)",
|
|
642
721
|
commands: [
|
|
643
|
-
["init", "
|
|
722
|
+
["init", "Scaffold a content repo here: deployment.env, SYSTEM.md, systems/, jobs/", init],
|
|
644
723
|
[
|
|
645
724
|
"setup-infra",
|
|
646
725
|
"Provision GCP: registry, buckets, service account, run queue, service shell",
|
|
@@ -668,7 +747,7 @@ export const GROUPS = [
|
|
|
668
747
|
script("mint-gmail-token.mjs"),
|
|
669
748
|
],
|
|
670
749
|
["mint-graph", "A Microsoft Graph refresh token, per mailbox", script("mint-graph-token.mjs")],
|
|
671
|
-
["artifact-cleanup", "Expire
|
|
750
|
+
["artifact-cleanup", "Expire cached images in the deployment's registry", script("set-artifact-cleanup.sh")],
|
|
672
751
|
],
|
|
673
752
|
note: [
|
|
674
753
|
"A new credential is `set-secret` again, plus `set-env` for whatever names the",
|
package/lib/doctor.js
CHANGED
|
@@ -439,7 +439,7 @@ export async function doctor() {
|
|
|
439
439
|
} else {
|
|
440
440
|
r.warn(
|
|
441
441
|
"No registry cleanup policy",
|
|
442
|
-
"
|
|
442
|
+
"Cached engine images accumulate; they are a copy of GHCR and can be expired freely.",
|
|
443
443
|
"meffecta-agent artifact-cleanup",
|
|
444
444
|
);
|
|
445
445
|
}
|
package/package.json
CHANGED
package/scripts/deploy.sh
CHANGED
|
@@ -7,18 +7,10 @@ set -euo pipefail
|
|
|
7
7
|
# new engine version is the deployment's call, not the engine repo's. This script is
|
|
8
8
|
# that call, and it is the same script for the vendor's own instance and for a client's.
|
|
9
9
|
#
|
|
10
|
-
# Cloud Run can only pull from Artifact Registry, never from GHCR directly
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# plus `gcloud auth configure-docker <region>-docker.pkg.dev` once, and — for a
|
|
15
|
-
# private GHCR package — `docker login ghcr.io` with a PAT that has read:packages.
|
|
16
|
-
# 2. AR remote repository: a one-time pull-through proxy of GHCR in the deployment's
|
|
17
|
-
# project, after which no local docker is involved. Then point --image straight at
|
|
18
|
-
# the proxied path and skip the mirror:
|
|
19
|
-
# gcloud artifacts repositories create ghcr-remote --repository-format=docker \
|
|
20
|
-
# --mode=remote-repository --location=<region> \
|
|
21
|
-
# --remote-docker-repo=https://ghcr.io # + upstream creds for a private package
|
|
10
|
+
# Cloud Run can only pull from Artifact Registry, never from GHCR directly. ARTIFACT_REPO
|
|
11
|
+
# is a remote repository standing in front of ghcr.io, so a tag resolves to a path inside
|
|
12
|
+
# it and Artifact Registry fetches the public engine image itself, on demand. Nothing is
|
|
13
|
+
# copied by hand and no docker is involved — not here, not in CI.
|
|
22
14
|
#
|
|
23
15
|
# What it writes, and what it deliberately does not.
|
|
24
16
|
#
|
|
@@ -71,62 +63,14 @@ require_gcloud
|
|
|
71
63
|
resolve_deployment
|
|
72
64
|
announce_target "Deploying ${SERVICE}"
|
|
73
65
|
|
|
74
|
-
# --image is taken as-is. Otherwise --tag resolves
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
if [ -z "${IMAGE}" ]
|
|
79
|
-
# A remote repository preserves the upstream path, so ghcr.io/meffecta/agent is served at
|
|
80
|
-
# <repo>/meffecta/agent. Nothing is copied and nothing is pushed: the first pull populates
|
|
81
|
-
# AR's cache, and Cloud Run's own pull is what triggers it.
|
|
66
|
+
# --image is taken as-is. Otherwise --tag resolves to a path inside the deployment's own
|
|
67
|
+
# remote repository, which preserves the upstream one: ghcr.io/meffecta/agent is served at
|
|
68
|
+
# <repo>/meffecta/agent. Nothing is copied and nothing is pushed — Cloud Run's own pull is
|
|
69
|
+
# what populates the cache.
|
|
70
|
+
if [ -z "${IMAGE}" ]; then
|
|
82
71
|
IMAGE="${REGION}-docker.pkg.dev/${PROJECT}/${ARTIFACT_REPO}/${GHCR_PATH}:${TAG}"
|
|
83
72
|
echo "🔗 Through the registry proxy: ${IMAGE}"
|
|
84
73
|
echo " (Artifact Registry fetches ${GHCR_IMAGE}:${TAG} on demand — no local docker)"
|
|
85
|
-
elif [ -z "${IMAGE}" ]; then
|
|
86
|
-
SOURCE="${GHCR_IMAGE}:${TAG}"
|
|
87
|
-
IMAGE="${REGION}-docker.pkg.dev/${PROJECT}/${ARTIFACT_REPO}/${SERVICE}:${TAG}"
|
|
88
|
-
command -v docker >/dev/null || {
|
|
89
|
-
echo "docker is required to mirror ${SOURCE} into Artifact Registry." >&2
|
|
90
|
-
echo "REGISTRY_MODE=proxy in deployment.env avoids it entirely — Artifact Registry" >&2
|
|
91
|
-
echo "fetches from ghcr.io itself. After changing it, re-run:" >&2
|
|
92
|
-
echo " $(cmd setup-infra)" >&2
|
|
93
|
-
exit 1
|
|
94
|
-
}
|
|
95
|
-
echo "📥 Mirroring ${SOURCE} → ${IMAGE}"
|
|
96
|
-
if [ "${DRY_RUN}" = true ]; then
|
|
97
|
-
echo " (dry run: would pull, retag and push)"
|
|
98
|
-
else
|
|
99
|
-
if ! docker pull "${SOURCE}"; then
|
|
100
|
-
cat >&2 <<EOF
|
|
101
|
-
|
|
102
|
-
Could not pull ${SOURCE}.
|
|
103
|
-
|
|
104
|
-
The published engine image is public, so this needs no login — check the tag exists
|
|
105
|
-
and that the host can reach ghcr.io:
|
|
106
|
-
|
|
107
|
-
curl -sS "https://ghcr.io/token?scope=repository:meffecta/agent:pull&service=ghcr.io"
|
|
108
|
-
|
|
109
|
-
If you are pointing GHCR_IMAGE at a private package of your own instead, that one does
|
|
110
|
-
need a token with the read:packages scope, which the gh CLI does not request by default:
|
|
111
|
-
|
|
112
|
-
gh auth refresh -h github.com -s read:packages
|
|
113
|
-
gh auth token | docker login ghcr.io -u <your-github-username> --password-stdin
|
|
114
|
-
EOF
|
|
115
|
-
exit 1
|
|
116
|
-
fi
|
|
117
|
-
docker tag "${SOURCE}" "${IMAGE}"
|
|
118
|
-
if ! docker push "${IMAGE}"; then
|
|
119
|
-
cat >&2 <<EOF
|
|
120
|
-
|
|
121
|
-
Could not push to ${IMAGE}.
|
|
122
|
-
|
|
123
|
-
Artifact Registry needs docker configured for this region once:
|
|
124
|
-
|
|
125
|
-
gcloud auth configure-docker ${REGION}-docker.pkg.dev
|
|
126
|
-
EOF
|
|
127
|
-
exit 1
|
|
128
|
-
fi
|
|
129
|
-
fi
|
|
130
74
|
fi
|
|
131
75
|
|
|
132
76
|
# scale-to-zero is the default and what sync-triggers.sh's triggers assume: no CPU
|
|
@@ -64,13 +64,11 @@ resolve_deployment() {
|
|
|
64
64
|
load_deployment_config
|
|
65
65
|
REGION="${REGION:-europe-west1}"
|
|
66
66
|
ARTIFACT_REPO="${ARTIFACT_REPO:-${SERVICE:-agent}-images}"
|
|
67
|
+
# Where the published engine images are. An environment override exists for building
|
|
68
|
+
# against a fork of the engine, and is deliberately not a deployment.env setting: a
|
|
69
|
+
# deployment points at the engine, and choosing a different one is not a thing to
|
|
70
|
+
# configure by editing a committed file.
|
|
67
71
|
GHCR_IMAGE="${GHCR_IMAGE:-ghcr.io/meffecta/agent}"
|
|
68
|
-
# How the engine image reaches Cloud Run, which can only ever pull from Artifact
|
|
69
|
-
# Registry. `proxy` makes ARTIFACT_REPO a remote repository in front of ghcr.io, so AR
|
|
70
|
-
# fetches on demand and no local docker is involved at all. `mirror` is the older path:
|
|
71
|
-
# pull from ghcr.io on the operator's machine, retag, push — which needs docker, and
|
|
72
|
-
# pushes several gigabytes for every engine version.
|
|
73
|
-
REGISTRY_MODE="${REGISTRY_MODE:-proxy}"
|
|
74
72
|
GHCR_PATH="${GHCR_IMAGE#*/}" # ghcr.io/meffecta/agent → meffecta/agent
|
|
75
73
|
TIMEZONE="${TIMEZONE:-Europe/Stockholm}"
|
|
76
74
|
|
|
@@ -4,7 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
# Stop a deployment's Artifact Registry filling up with engine images it will never use
|
|
5
5
|
# again.
|
|
6
6
|
#
|
|
7
|
-
# The images there are a **cache**, not an archive:
|
|
7
|
+
# The images there are a **cache**, not an archive: Artifact Registry fetches them from
|
|
8
8
|
# ghcr.io/meffecta/agent because Cloud Run cannot pull from GHCR directly. GHCR keeps the
|
|
9
9
|
# real copies, so anything deleted here comes back on the next `deploy.sh --tag <that one>`.
|
|
10
10
|
# That is what makes an aggressive policy correct for this repo and wrong for a repo whose
|
|
@@ -119,33 +119,26 @@ run ${GC} services enable \
|
|
|
119
119
|
analyticsdata.googleapis.com \
|
|
120
120
|
iam.googleapis.com
|
|
121
121
|
|
|
122
|
-
echo "🐳 Artifact Registry repo
|
|
122
|
+
echo "🐳 Artifact Registry repo..."
|
|
123
123
|
if ! ${GC} artifacts repositories describe "${ARTIFACT_REPO}" --location="${REGION}" &>/dev/null; then
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
--description="Proxy of ${GHCR_IMAGE%%/*} — the engine image is fetched on demand"
|
|
133
|
-
else
|
|
134
|
-
run ${GC} artifacts repositories create "${ARTIFACT_REPO}" --location="${REGION}" --repository-format=docker
|
|
135
|
-
fi
|
|
124
|
+
# A remote repository standing in front of ghcr.io. Cloud Run can only pull from
|
|
125
|
+
# Artifact Registry, and this is how that requirement is met without any docker: AR
|
|
126
|
+
# fetches the public engine image itself, on demand, and caches it. No upstream
|
|
127
|
+
# credentials, because the engine package is public.
|
|
128
|
+
run ${GC} artifacts repositories create "${ARTIFACT_REPO}" --location="${REGION}" \
|
|
129
|
+
--repository-format=docker --mode=remote-repository \
|
|
130
|
+
--remote-docker-repo="https://${GHCR_IMAGE%%/*}" \
|
|
131
|
+
--description="Proxy of ${GHCR_IMAGE%%/*} — the engine image is fetched on demand"
|
|
136
132
|
else
|
|
137
133
|
ACTUAL_MODE=$(${GC} artifacts repositories describe "${ARTIFACT_REPO}" --location="${REGION}" --format='value(mode)')
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
echo " name a different ARTIFACT_REPO in deployment.env and re-run this." >&2
|
|
147
|
-
;;
|
|
148
|
-
esac
|
|
134
|
+
if [ "${ACTUAL_MODE}" != "REMOTE_REPOSITORY" ]; then
|
|
135
|
+
# A repository's mode is fixed at creation, so this is not something to "fix" here —
|
|
136
|
+
# deploy.sh would resolve an image path the repository cannot serve, and the failure
|
|
137
|
+
# would arrive as a Cloud Run pull error with nothing pointing back to this.
|
|
138
|
+
echo "⚠ ${ARTIFACT_REPO} is a ${ACTUAL_MODE}, which cannot serve the engine image." >&2
|
|
139
|
+
echo " A repository's mode cannot be changed. Name a different ARTIFACT_REPO in" >&2
|
|
140
|
+
echo " deployment.env and re-run this." >&2
|
|
141
|
+
fi
|
|
149
142
|
fi
|
|
150
143
|
# The images here are a cache of ghcr.io/meffecta/agent, not an archive, so they are worth
|
|
151
144
|
# expiring — see set-artifact-cleanup.sh for why that is safe and what the policy keeps.
|
|
@@ -198,8 +191,8 @@ if ! ${GC} iam service-accounts describe "${DEPLOY_SA}" &>/dev/null; then
|
|
|
198
191
|
sleep 5
|
|
199
192
|
done
|
|
200
193
|
fi
|
|
201
|
-
# What a DEPLOY needs, and nothing a BUILD would: roll out a revision, pull
|
|
202
|
-
#
|
|
194
|
+
# What a DEPLOY needs, and nothing a BUILD would: roll out a revision, pull the engine
|
|
195
|
+
# image, and read this project's enabled services.
|
|
203
196
|
#
|
|
204
197
|
# Deliberately NOT granted: secretmanager.secretAccessor. deploy.sh re-syncs the Cloud
|
|
205
198
|
# Scheduler triggers, and that step reads the API secret — so a CI deploy runs
|
|
@@ -334,7 +327,7 @@ if has_cli; then
|
|
|
334
327
|
echo ""
|
|
335
328
|
echo " ${MEFFECTA_CLI} connect # lists them; connect <name> walks through one"
|
|
336
329
|
echo ""
|
|
337
|
-
echo "
|
|
330
|
+
echo " The whole set-up, in order: https://agent.meffecta.com"
|
|
338
331
|
echo " ${MEFFECTA_CLI} doctor what is still missing, once something is running"
|
|
339
332
|
fi
|
|
340
333
|
if [ "${SETUP_CI_DEPLOY}" = "true" ]; then
|
|
@@ -6,8 +6,8 @@ set -euo pipefail
|
|
|
6
6
|
# The engine's source repo is private; the image is not. The operator tooling — these
|
|
7
7
|
# scripts and the config template — ships inside ghcr.io/meffecta/agent, so a deployment run
|
|
8
8
|
# by someone outside Meffecta gets its tooling from the same public artifact it gets its
|
|
9
|
-
# engine from, and never needs the source.
|
|
10
|
-
#
|
|
9
|
+
# engine from, and never needs the source. A deployment is documented by
|
|
10
|
+
# https://agent.meffecta.com and by the CLI's own `meffecta-agent help`.
|
|
11
11
|
#
|
|
12
12
|
# The tooling belongs in your content repo, committed next to jobs/ and deployment.env.
|
|
13
13
|
# That is what makes a fresh clone of that repo enough to operate the deployment, and it
|
|
@@ -26,9 +26,8 @@ set -euo pipefail
|
|
|
26
26
|
# scripts/update-tooling.sh --tag sha-558a144 # match a specific one, e.g. what you run
|
|
27
27
|
# scripts/update-tooling.sh --dry-run # show what would change, touch nothing
|
|
28
28
|
#
|
|
29
|
-
# Needs docker,
|
|
30
|
-
#
|
|
31
|
-
# here — once per engine version, rather than once per deploy.
|
|
29
|
+
# Needs docker, and is the only thing here that does: the files are lifted out of the
|
|
30
|
+
# published image with `docker cp`. Once per engine version, and never during a deploy.
|
|
32
31
|
|
|
33
32
|
. "$(dirname "${BASH_SOURCE[0]}")/lib/cli-names.sh"
|
|
34
33
|
|