@rizom/ops 0.2.0-alpha.7 → 0.2.0-alpha.700

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 (85) hide show
  1. package/README.md +89 -4
  2. package/dist/age-key-bootstrap.d.ts +17 -0
  3. package/dist/brains-ops.js +763 -177
  4. package/dist/capability-bundle-migration.d.ts +26 -0
  5. package/dist/cert-bootstrap.d.ts +6 -4
  6. package/dist/content-repo-ref.d.ts +10 -0
  7. package/dist/content-repo.d.ts +13 -0
  8. package/dist/default-user-runner.d.ts +1 -1
  9. package/dist/deploy.js +110 -166
  10. package/dist/directory-sync-stress-system.d.ts +75 -0
  11. package/dist/directory-sync-stress.d.ts +117 -0
  12. package/dist/entries/deploy.d.ts +4 -2
  13. package/dist/health-watchdog-smoke.d.ts +54 -0
  14. package/dist/image-inventory.d.ts +4 -0
  15. package/dist/image-types.d.ts +7 -0
  16. package/dist/images.d.ts +89 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.js +775 -177
  19. package/dist/legacy-pilot-migration.d.ts +11 -0
  20. package/dist/legacy-projection-job-recovery.d.ts +29 -0
  21. package/dist/load-registry.d.ts +92 -8
  22. package/dist/observed-status.d.ts +1 -1
  23. package/dist/onboard-user.d.ts +2 -2
  24. package/dist/parse-args.d.ts +2 -10
  25. package/dist/preview-domain.d.ts +9 -0
  26. package/dist/provider-key-migration.d.ts +20 -0
  27. package/dist/reconcile-all.d.ts +2 -2
  28. package/dist/reconcile-cohort.d.ts +2 -2
  29. package/dist/reconcile-dry-run.d.ts +9 -0
  30. package/dist/reconcile-lib.d.ts +4 -2
  31. package/dist/run-command.d.ts +22 -6
  32. package/dist/runtime-image-lock.d.ts +12 -0
  33. package/dist/schema.d.ts +145 -71
  34. package/dist/secrets-encrypt.d.ts +27 -0
  35. package/dist/secrets-push.d.ts +2 -2
  36. package/dist/ssh-key-bootstrap.d.ts +1 -26
  37. package/dist/stage-legacy-crossover.d.ts +25 -0
  38. package/dist/stress-command.d.ts +14 -0
  39. package/dist/stress-git-checkout.d.ts +21 -0
  40. package/dist/stress-health-monitor.d.ts +56 -0
  41. package/dist/upgrade.d.ts +10 -0
  42. package/dist/user-add.d.ts +16 -0
  43. package/dist/user-offboard.d.ts +61 -0
  44. package/dist/user-runner.d.ts +5 -0
  45. package/dist/verify-user.d.ts +22 -0
  46. package/package.json +51 -40
  47. package/templates/rover-pilot/.env.schema +38 -4
  48. package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
  49. package/templates/rover-pilot/.github/workflows/build.yml +71 -17
  50. package/templates/rover-pilot/.github/workflows/deploy.yml +127 -49
  51. package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
  52. package/templates/rover-pilot/.github/workflows/health-watchdog-smoke.yml +95 -0
  53. package/templates/rover-pilot/.github/workflows/offboard.yml +107 -0
  54. package/templates/rover-pilot/.github/workflows/reconcile.yml +23 -3
  55. package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
  56. package/templates/rover-pilot/README.md +19 -8
  57. package/templates/rover-pilot/deploy/scripts/create-predeploy-backup.ts +962 -0
  58. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +139 -0
  59. package/templates/rover-pilot/deploy/scripts/helpers.ts +3 -0
  60. package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
  61. package/templates/rover-pilot/deploy/scripts/provision-server.ts +52 -22
  62. package/templates/rover-pilot/deploy/scripts/pull-local-models.ts +69 -0
  63. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +19 -4
  64. package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
  65. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +71 -14
  66. package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +183 -0
  67. package/templates/rover-pilot/deploy/scripts/update-dns.ts +72 -16
  68. package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
  69. package/templates/rover-pilot/deploy/scripts/verify-runtime-image.ts +18 -0
  70. package/templates/rover-pilot/docs/canonical-crossover-record.md +107 -0
  71. package/templates/rover-pilot/docs/onboarding-checklist.md +39 -14
  72. package/templates/rover-pilot/docs/operator-playbook.md +421 -22
  73. package/templates/rover-pilot/docs/user-onboarding.md +49 -271
  74. package/templates/rover-pilot/package.json +4 -0
  75. package/templates/rover-pilot/pilot.yaml +10 -4
  76. package/templates/rover-pilot/users/alice.yaml +5 -1
  77. package/dist/origin-ca.d.ts +0 -1
  78. package/dist/push-secrets.d.ts +0 -9
  79. package/dist/push-target.d.ts +0 -2
  80. package/dist/run-subprocess.d.ts +0 -5
  81. package/dist/user-secret-names.d.ts +0 -6
  82. package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
  83. package/templates/rover-pilot/deploy/Caddyfile +0 -66
  84. package/templates/rover-pilot/deploy/Dockerfile +0 -38
  85. package/templates/rover-pilot/deploy/kamal/deploy.yml +0 -39
@@ -0,0 +1,14 @@
1
+ export interface StressCommandOptions {
2
+ cwd?: string;
3
+ env?: NodeJS.ProcessEnv;
4
+ stdin?: string;
5
+ }
6
+ export interface StressCommandResult {
7
+ exitCode: number;
8
+ stdout: string;
9
+ stderr: string;
10
+ }
11
+ export type StressCommandRunner = (command: string, args: readonly string[], options?: StressCommandOptions) => Promise<StressCommandResult>;
12
+ export declare const runStressCommand: StressCommandRunner;
13
+ export declare function requireCommand(runner: StressCommandRunner, command: string, args: readonly string[], options?: StressCommandOptions): Promise<StressCommandResult>;
14
+ export declare function commandError(command: string, args: readonly string[], result: StressCommandResult): Error;
@@ -0,0 +1,21 @@
1
+ import { type StressCommandResult, type StressCommandRunner } from "./stress-command";
2
+ /**
3
+ * Git operations against one content-repo checkout, executed through a
4
+ * stress command runner with token-bearing credentials.
5
+ */
6
+ export declare class GitCheckout {
7
+ #private;
8
+ readonly dir: string;
9
+ constructor(runner: StressCommandRunner, dir: string, token: string);
10
+ /** Clone `repository`'s main branch into `dir` and pin it to origin/main. */
11
+ static clone(runner: StressCommandRunner, repository: string, dir: string, token: string): Promise<GitCheckout>;
12
+ run(args: readonly string[], required?: boolean): Promise<StressCommandResult>;
13
+ output(args: readonly string[]): Promise<string>;
14
+ sync(hardReset?: boolean): Promise<void>;
15
+ /** Stage everything and commit as the ops identity. */
16
+ commitAll(message: string): Promise<void>;
17
+ pushMainWithRebase(): Promise<void>;
18
+ /** Delete every remote stress backup branch; returns the deleted names. */
19
+ pruneStressBackupBranches(): Promise<string[]>;
20
+ private environment;
21
+ }
@@ -0,0 +1,56 @@
1
+ import type { FetchLike } from "@brains/utils/fetch-like";
2
+ import { z } from "@brains/utils/zod";
3
+ import { type StressHealthSample, type StressRuntimeSample } from "./directory-sync-stress";
4
+ /** Entity counts by type, as the runtime's database reports them. */
5
+ export declare const stressEntityCountsSchema: z.ZodArray<z.ZodObject<{
6
+ entityType: z.ZodString;
7
+ count: z.ZodNumber;
8
+ }>>;
9
+ export type StressEntityCounts = z.output<typeof stressEntityCountsSchema>;
10
+ export interface HealthPayload {
11
+ version: string;
12
+ entities: number;
13
+ entityCounts: StressEntityCounts;
14
+ }
15
+ export declare function noteCount(payload: HealthPayload): number;
16
+ export interface StressHealthMonitorOptions {
17
+ domain: string;
18
+ fetchImpl: FetchLike;
19
+ now: () => Date;
20
+ sleep: (milliseconds: number) => Promise<void>;
21
+ /** Take one runtime sample (e.g. docker stats over SSH), if available. */
22
+ sampleRuntime: () => Promise<StressRuntimeSample | undefined>;
23
+ /**
24
+ * Entity counts by type from an authenticated source (the runtime's
25
+ * database over SSH), or undefined when this poll has no reading.
26
+ */
27
+ readEntityCounts: () => Promise<StressEntityCounts | undefined>;
28
+ }
29
+ /**
30
+ * Samples deployment health and runtime resource usage during a stress run,
31
+ * and answers entity-baseline waits from the operational health endpoint and
32
+ * the runtime's own entity counts.
33
+ */
34
+ export declare class StressHealthMonitor {
35
+ #private;
36
+ constructor(options: StressHealthMonitorOptions);
37
+ /** ISO timestamp of the most recent start() call. */
38
+ get startedAt(): string;
39
+ /** The first sampling failure since start(), if any. */
40
+ get error(): unknown;
41
+ get healthSamples(): readonly StressHealthSample[];
42
+ get runtimeSamples(): readonly StressRuntimeSample[];
43
+ /** Health samples between start() and markGateEnd() (or now). */
44
+ gateHealthSamples(): StressHealthSample[];
45
+ /** Exclude samples taken after this point from the gate window. */
46
+ markGateEnd(): void;
47
+ /** Probe optional health endpoints and add responsive ones to the loop. */
48
+ discoverEndpoints(): Promise<void>;
49
+ start(): void;
50
+ /** Stop sampling and wait for the loops; a loop error stays on `error`. */
51
+ stop(): Promise<void>;
52
+ waitForHealthSnapshot(timeoutMs: number): Promise<HealthPayload | undefined>;
53
+ waitForEntityBaseline(expectedNotes: number | undefined, expectedEntities: number | undefined, timeoutMs: number): Promise<HealthPayload | undefined>;
54
+ waitForStableEntityBaseline(expectedNotes: number | undefined, expectedEntities: number | undefined, timeoutMs: number): Promise<HealthPayload | undefined>;
55
+ fetchHealthPayload(): Promise<HealthPayload | undefined>;
56
+ }
@@ -0,0 +1,10 @@
1
+ export type UpgradeExec = (command: string[], cwd: string) => Promise<void>;
2
+ export interface UpgradeOptions {
3
+ exec?: UpgradeExec | undefined;
4
+ version?: string | undefined;
5
+ }
6
+ export interface UpgradeResult {
7
+ from: string;
8
+ to: string;
9
+ }
10
+ export declare function upgradePilotRepo(rootDir: string, options?: UpgradeOptions): Promise<UpgradeResult>;
@@ -0,0 +1,16 @@
1
+ export interface AddPilotUserOptions {
2
+ cohort: string;
3
+ discord?: boolean | undefined;
4
+ anchorId?: string | undefined;
5
+ }
6
+ export interface AddPilotUserResult {
7
+ handle: string;
8
+ cohort: string;
9
+ userPath: string;
10
+ secretsTemplatePath: string;
11
+ cohortPath: string;
12
+ createdUser: boolean;
13
+ createdSecretsTemplate: boolean;
14
+ addedToCohort: boolean;
15
+ }
16
+ export declare function addPilotUser(rootDir: string, handle: string, options: AddPilotUserOptions): Promise<AddPilotUserResult>;
@@ -0,0 +1,61 @@
1
+ import type { FetchLike } from "@brains/utils/fetch-like";
2
+ export interface OffboardPilotUsersOptions {
3
+ handles: string[];
4
+ confirmation?: string | undefined;
5
+ dryRun?: boolean | undefined;
6
+ env?: NodeJS.ProcessEnv | undefined;
7
+ fetchImpl?: FetchLike | undefined;
8
+ logger?: ((message: string) => void) | undefined;
9
+ sleep?: ((delayMs: number) => Promise<void>) | undefined;
10
+ driver?: PilotOffboardDriver | undefined;
11
+ }
12
+ export interface PilotOffboardTarget {
13
+ handle: string;
14
+ instanceName: string;
15
+ domain: string;
16
+ previewDomain: string;
17
+ wwwDomain?: string | undefined;
18
+ contentRepo: string;
19
+ cloudflareZoneId: string;
20
+ }
21
+ export interface PilotOffboardServer {
22
+ id: number;
23
+ name: string;
24
+ ip?: string | undefined;
25
+ }
26
+ export interface PilotOffboardDnsRecord {
27
+ id: string;
28
+ name: string;
29
+ type: "A" | "CNAME";
30
+ content: string;
31
+ }
32
+ export interface PilotOffboardInspection {
33
+ server?: PilotOffboardServer | undefined;
34
+ dnsRecords: PilotOffboardDnsRecord[];
35
+ contentRepoArchived: boolean;
36
+ }
37
+ export interface PilotOffboardDriver {
38
+ inspect(target: PilotOffboardTarget): Promise<PilotOffboardInspection>;
39
+ archiveContentRepo(target: PilotOffboardTarget): Promise<void>;
40
+ deleteDnsRecord(target: PilotOffboardTarget, record: PilotOffboardDnsRecord): Promise<void>;
41
+ deleteServer(target: PilotOffboardTarget, server: PilotOffboardServer): Promise<void>;
42
+ }
43
+ export interface OffboardPilotUserPlan {
44
+ target: PilotOffboardTarget;
45
+ inspection: PilotOffboardInspection;
46
+ }
47
+ export interface OffboardPilotUsersResult {
48
+ dryRun: boolean;
49
+ handles: string[];
50
+ confirmation: string;
51
+ plans: OffboardPilotUserPlan[];
52
+ }
53
+ export declare function offboardPilotUsers(rootDir: string, options: OffboardPilotUsersOptions): Promise<OffboardPilotUsersResult>;
54
+ interface DefaultDriverOptions {
55
+ env: NodeJS.ProcessEnv;
56
+ fetchImpl?: FetchLike | undefined;
57
+ contentRepoTokenName: string;
58
+ }
59
+ export declare function createPilotOffboardDriver(options: DefaultDriverOptions): PilotOffboardDriver;
60
+ export declare function assertPilotUsersRemoved(rootDir: string, handles: string[]): Promise<void>;
61
+ export {};
@@ -1,6 +1,11 @@
1
1
  import type { ResolvedUser } from "./load-registry";
2
+ export interface ContentRepoFile {
3
+ path: string;
4
+ content: string;
5
+ }
2
6
  export interface UserRunResult {
3
7
  brainYaml?: string;
4
8
  envFile?: string;
9
+ contentRepoFiles?: ContentRepoFile[];
5
10
  }
6
11
  export type UserRunner = (user: ResolvedUser) => Promise<UserRunResult | void>;
@@ -0,0 +1,22 @@
1
+ import type { FetchLike } from "@brains/utils/fetch-like";
2
+ import { type ResolvedUser } from "./load-registry";
3
+ export interface VerifyPilotUserOptions {
4
+ fetchImpl?: FetchLike;
5
+ logger?: (message: string) => void;
6
+ operationalRetryAttempts?: number;
7
+ operationalRetryDelayMs?: number;
8
+ sleep?: (delayMs: number) => Promise<void>;
9
+ }
10
+ export interface FailedCheck {
11
+ name: string;
12
+ message: string;
13
+ }
14
+ export interface VerifyPilotUserResult {
15
+ handle: string;
16
+ bundles: ResolvedUser["bundles"];
17
+ domain: string;
18
+ contentRepo: string;
19
+ checks: string[];
20
+ failedChecks: FailedCheck[];
21
+ }
22
+ export declare function verifyPilotUser(rootDir: string, handle: string, options?: VerifyPilotUserOptions): Promise<VerifyPilotUserResult>;
package/package.json CHANGED
@@ -1,65 +1,76 @@
1
1
  {
2
2
  "name": "@rizom/ops",
3
+ "version": "0.2.0-alpha.700",
3
4
  "description": "Operator CLI for managing private brain fleet registry repos",
4
- "publishConfig": {
5
- "access": "public"
5
+ "keywords": [
6
+ "brains",
7
+ "ops",
8
+ "cli",
9
+ "deploy",
10
+ "fleet",
11
+ "operator"
12
+ ],
13
+ "license": "AGPL-3.0-only",
14
+ "author": "Yeehaa <yeehaa@rizom.ai> (https://rizom.ai)",
15
+ "bin": {
16
+ "brains-ops": "./dist/brains-ops.js"
17
+ },
18
+ "bugs": "https://github.com/rizom-ai/brains/issues",
19
+ "dependencies": {
20
+ "@libsql/client": "^0.17.4",
21
+ "age-encryption": "^0.3.0",
22
+ "yaml": "^2.9.0"
23
+ },
24
+ "devDependencies": {
25
+ "@brains/contracts": "workspace:*",
26
+ "@brains/deploy-support": "workspace:*",
27
+ "@brains/eslint-config": "workspace:*",
28
+ "@brains/job-queue": "workspace:*",
29
+ "@brains/profile": "workspace:*",
30
+ "@brains/site-composition": "workspace:*",
31
+ "@brains/test-utils": "workspace:*",
32
+ "@brains/typescript-config": "workspace:*",
33
+ "@brains/utils": "workspace:*",
34
+ "@types/bun": "^1.4.0",
35
+ "typescript": "^7.0.2"
36
+ },
37
+ "engines": {
38
+ "bun": ">=1.3.3"
6
39
  },
7
- "version": "0.2.0-alpha.7",
8
- "type": "module",
9
40
  "exports": {
10
41
  ".": {
11
42
  "types": "./dist/index.d.ts",
12
43
  "import": "./dist/index.js"
13
44
  },
14
45
  "./deploy": {
15
- "bun": "./dist/deploy.js",
16
46
  "types": "./dist/deploy.d.ts",
17
- "import": "./dist/deploy.js"
47
+ "import": "./dist/deploy.js",
48
+ "bun": "./dist/deploy.js"
18
49
  }
19
50
  },
20
- "main": "./dist/index.js",
21
- "types": "./dist/index.d.ts",
22
- "bin": {
23
- "brains-ops": "./dist/brains-ops.js"
24
- },
25
51
  "files": [
26
52
  "dist",
27
53
  "templates"
28
54
  ],
29
- "scripts": {
30
- "build": "bun scripts/build.ts",
31
- "prepublishOnly": "bun scripts/build.ts",
32
- "typecheck": "tsc --noEmit",
33
- "lint": "eslint . --ext .ts",
34
- "lint:fix": "eslint . --ext .ts --fix",
35
- "test": "bun test"
36
- },
37
- "dependencies": {},
38
- "devDependencies": {
39
- "@brains/eslint-config": "workspace:*",
40
- "@brains/typescript-config": "workspace:*",
41
- "@brains/utils": "workspace:*",
42
- "@types/bun": "latest",
43
- "typescript": "^5.3.3"
55
+ "homepage": "https://github.com/rizom-ai/brains/tree/main/packages/brains-ops#readme",
56
+ "main": "./dist/index.js",
57
+ "publishConfig": {
58
+ "access": "public"
44
59
  },
45
60
  "repository": {
46
61
  "type": "git",
47
62
  "url": "https://github.com/rizom-ai/brains.git",
48
63
  "directory": "packages/brains-ops"
49
64
  },
50
- "license": "Apache-2.0",
51
- "author": "Yeehaa <yeehaa@rizom.ai> (https://rizom.ai)",
52
- "homepage": "https://github.com/rizom-ai/brains/tree/main/packages/brains-ops#readme",
53
- "bugs": "https://github.com/rizom-ai/brains/issues",
54
- "engines": {
55
- "bun": ">=1.3.3"
65
+ "scripts": {
66
+ "build": "bun scripts/build.ts",
67
+ "lint": "eslint . --max-warnings 0",
68
+ "lint:fix": "eslint . --max-warnings 0 --fix",
69
+ "prepublishOnly": "bun scripts/build.ts",
70
+ "test": "bun test --timeout 20000",
71
+ "test:smoke": "bun run build && RUN_SMOKE_TESTS=1 bun test --timeout 60000",
72
+ "typecheck": "tsc --noEmit"
56
73
  },
57
- "keywords": [
58
- "brains",
59
- "ops",
60
- "cli",
61
- "deploy",
62
- "fleet",
63
- "operator"
64
- ]
74
+ "type": "module",
75
+ "types": "./dist/index.d.ts"
65
76
  }
@@ -3,22 +3,56 @@
3
3
  # @defaultRequired=false @defaultSensitive=false
4
4
  # ----------
5
5
 
6
- # AI provider
6
+ # OpenAI, the provider of the canonical chat and embedding models
7
+ # Shared secret by default; a per-user override may come from the decrypted
8
+ # users/<handle>.secrets.yaml.age file at deploy time.
7
9
  # @required @sensitive
8
- AI_API_KEY=
10
+ OPENAI_API_KEY=
11
+
12
+ # Anthropic and Google: unused by the canonical models; the shared deploy
13
+ # config passes every provider's key to the brain.
14
+ # @sensitive
15
+ ANTHROPIC_API_KEY=
16
+
17
+ # @sensitive
18
+ GOOGLE_GENERATIVE_AI_API_KEY=
9
19
 
10
20
  # Git sync
21
+ # Comes from the decrypted users/<handle>.secrets.yaml.age file.
11
22
  # @required @sensitive
12
23
  GIT_SYNC_TOKEN=
13
24
 
14
- # MCP interface
25
+ # Content repo administration
26
+ # Local/operator secret only. Used by brains-ops to create missing GitHub repos;
27
+ # do not deploy it into Rover runtime config.
15
28
  # @required @sensitive
16
- MCP_AUTH_TOKEN=
29
+ CONTENT_REPO_ADMIN_TOKEN=
17
30
 
18
31
  # Discord (optional, per-user)
32
+ # Comes from the decrypted users/<handle>.secrets.yaml.age file when enabled.
19
33
  # @sensitive
20
34
  DISCORD_BOT_TOKEN=
21
35
 
36
+ # Stored with the per-user Discord credentials and injected at deploy time.
37
+ # @sensitive
38
+ DISCORD_PUBLIC_KEY=
39
+
40
+ # @sensitive
41
+ DISCORD_APPLICATION_ID=
42
+
43
+ # AT Protocol publishing/discovery (optional, per-user)
44
+ # Comes from the decrypted users/<handle>.secrets.yaml.age file when configured.
45
+ # @sensitive
46
+ ATPROTO_APP_PASSWORD=
47
+
48
+ # Setup email delivery (optional, shared)
49
+ # Used when a user file declares setup.delivery: email.
50
+ # @sensitive
51
+ SETUP_EMAIL_API_KEY=
52
+
53
+ # @sensitive
54
+ SETUP_EMAIL_FROM=
55
+
22
56
  # ---- deploy/provision vars ----
23
57
 
24
58
  # @required @sensitive
@@ -0,0 +1,47 @@
1
+ name: Load Bitwarden environment through Varlock
2
+ description: Load, mask, and export the pilot environment from Varlock
3
+
4
+ runs:
5
+ using: composite
6
+ steps:
7
+ - name: Load shared environment
8
+ shell: bash
9
+ run: |
10
+ for attempt in 1 2 3; do
11
+ if bunx varlock@1.1.0 load --path .env.schema --format json --compact > /tmp/varlock-env.json; then
12
+ break
13
+ fi
14
+ if [ "$attempt" = "3" ]; then
15
+ exit 1
16
+ fi
17
+ echo "Varlock load failed (attempt $attempt/3); retrying in 5s..."
18
+ sleep 5
19
+ done
20
+ node <<'NODE'
21
+ import { appendFileSync, readFileSync, rmSync } from "node:fs";
22
+ const env = JSON.parse(readFileSync("/tmp/varlock-env.json", "utf8"));
23
+ const output = process.env.GITHUB_ENV;
24
+ if (!output) throw new Error("Missing GITHUB_ENV");
25
+ const newline = "\n";
26
+ const chunks = [];
27
+ for (const [key, value] of Object.entries(env)) {
28
+ if (key === "BWS_ACCESS_TOKEN" || value === null || value === undefined) continue;
29
+ const text = String(value).replace(/\r\n/g, newline);
30
+ const escapedMask = text
31
+ .replace(/%/g, "%25")
32
+ .replace(/\r/g, "%0D")
33
+ .replace(/\n/g, "%0A");
34
+ if (escapedMask.length > 0) {
35
+ process.stdout.write(`::add-mask::${escapedMask}${newline}`);
36
+ }
37
+ if (text.includes(newline)) {
38
+ let delimiter = `VARLOCK_${key}_EOF`;
39
+ while (text.includes(delimiter)) delimiter += "_X";
40
+ chunks.push(`${key}<<${delimiter}${newline}${text}${newline}${delimiter}`);
41
+ } else {
42
+ chunks.push(`${key}=${text}`);
43
+ }
44
+ }
45
+ appendFileSync(output, `${chunks.join(newline)}${newline}`);
46
+ rmSync("/tmp/varlock-env.json", { force: true });
47
+ NODE
@@ -2,10 +2,29 @@ name: Build
2
2
 
3
3
  on:
4
4
  workflow_dispatch:
5
+ inputs:
6
+ brain_version:
7
+ description: Build one Brain version from its checked-in runtime lock
8
+ required: false
9
+ type: string
10
+ site_packages:
11
+ description: "Exact site/theme pins for an explicit build; a matching checked-in runtime lock is required."
12
+ required: false
13
+ type: string
14
+ overwrite:
15
+ description: Rebuild an existing tag in place. Published tags are immutable; confirm only when replacing a tag whose containers were never deployed.
16
+ required: false
17
+ type: boolean
18
+ default: false
5
19
  push:
6
20
  branches: ["main"]
7
21
  paths:
22
+ # The image set is a pure function of the declared fleet state: a push
23
+ # that changes it builds exactly what it declares, so the deploy (fired
24
+ # off the same push via Reconcile) finds its tag ready.
8
25
  - pilot.yaml
26
+ - cohorts/**
27
+ - users/*.yaml
9
28
  - deploy/**
10
29
  - .github/workflows/build.yml
11
30
 
@@ -14,50 +33,85 @@ permissions:
14
33
  packages: write
15
34
 
16
35
  jobs:
17
- build:
36
+ resolve:
18
37
  runs-on: ubuntu-latest
38
+ outputs:
39
+ images_json: ${{ steps.resolve.outputs.images_json }}
19
40
  steps:
20
41
  - uses: actions/checkout@v5
21
42
  with:
22
43
  ref: ${{ github.sha }}
23
44
 
24
- - name: Extract image metadata inputs
25
- run: |
26
- BRAIN_VERSION="$(grep '^brainVersion:' pilot.yaml | sed 's/^brainVersion:[[:space:]]*//' | tr -d '"' | tr -d "'")"
27
- if [ -z "$BRAIN_VERSION" ]; then
28
- echo "Missing brainVersion in pilot.yaml" >&2
29
- exit 1
30
- fi
31
- echo "BRAIN_VERSION=$BRAIN_VERSION" >> "$GITHUB_ENV"
32
- echo "IMAGE_REPOSITORY=ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}" >> "$GITHUB_ENV"
45
+ - uses: oven-sh/setup-bun@v2
46
+ with:
47
+ bun-version: 1.4.0
48
+
49
+ - name: Install operator tooling
50
+ run: bun install --frozen-lockfile --ignore-scripts
51
+
52
+ - name: Log in to GHCR
53
+ uses: docker/login-action@v4
54
+ with:
55
+ registry: ghcr.io
56
+ username: ${{ github.actor }}
57
+ password: ${{ secrets.GITHUB_TOKEN }}
58
+
59
+ - name: Resolve images to build
60
+ id: resolve
61
+ env:
62
+ BRAIN_VERSION_INPUT: ${{ inputs.brain_version || '' }}
63
+ SITE_PACKAGES_INPUT: ${{ inputs.site_packages || '' }}
64
+ ALLOW_TAG_OVERWRITE: ${{ inputs.overwrite && 'true' || 'false' }}
65
+ run: bun deploy/scripts/resolve-missing-images.ts
66
+
67
+ no_builds:
68
+ needs: resolve
69
+ if: needs.resolve.outputs.images_json == '[]'
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - name: Skip when every declared image exists
73
+ run: echo "All declared images already exist; nothing to build."
74
+
75
+ build:
76
+ needs: resolve
77
+ if: needs.resolve.outputs.images_json != '[]'
78
+ runs-on: ubuntu-latest
79
+ strategy:
80
+ fail-fast: false
81
+ matrix:
82
+ image: ${{ fromJson(needs.resolve.outputs.images_json) }}
83
+ steps:
84
+ - uses: actions/checkout@v5
85
+ with:
86
+ ref: ${{ github.sha }}
33
87
 
34
88
  - name: Set up Docker Buildx
35
- uses: docker/setup-buildx-action@v3
89
+ uses: docker/setup-buildx-action@v4
36
90
 
37
91
  - name: Extract image metadata
38
92
  id: meta
39
- uses: docker/metadata-action@v5
93
+ uses: docker/metadata-action@v6
40
94
  with:
41
- images: ${{ env.IMAGE_REPOSITORY }}
95
+ images: ghcr.io/${{ github.repository }}
42
96
  tags: |
43
- type=raw,value=brain-${{ env.BRAIN_VERSION }}
97
+ type=raw,value=${{ matrix.image.tag }}
44
98
 
45
99
  - name: Log in to GHCR
46
- uses: docker/login-action@v3
100
+ uses: docker/login-action@v4
47
101
  with:
48
102
  registry: ghcr.io
49
103
  username: ${{ github.actor }}
50
104
  password: ${{ secrets.GITHUB_TOKEN }}
51
105
 
52
106
  - name: Build and push image
53
- uses: docker/build-push-action@v6
107
+ uses: docker/build-push-action@v7
54
108
  with:
55
109
  context: .
56
110
  file: deploy/Dockerfile
57
111
  target: fleet
58
112
  push: true
59
113
  build-args: |
60
- BRAIN_VERSION=${{ env.BRAIN_VERSION }}
114
+ RUNTIME_IMAGE_TAG=${{ matrix.image.tag }}
61
115
  tags: ${{ steps.meta.outputs.tags }}
62
116
  labels: |
63
117
  ${{ steps.meta.outputs.labels }}