@rizom/ops 0.2.0-alpha.397 → 0.2.0-alpha.398

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.
@@ -4,6 +4,7 @@ import type { LoadPilotRegistryOptions } from "./load-registry";
4
4
  import { type LookupHost } from "./observed-status";
5
5
  import { type RunCommand as OpsRunCommand } from "./run-subprocess";
6
6
  import { type SshKeygen } from "./ssh-key-bootstrap";
7
+ import type { PilotOffboardDriver } from "./user-offboard";
7
8
  import type { UserRunner } from "./user-runner";
8
9
  import type { cleanupDirectorySyncStress, runDeployedDirectorySyncStress, verifyDirectorySyncStressAccess } from "./directory-sync-stress-system";
9
10
  import type { cleanupHealthWatchdogSmoke, runHealthWatchdogSmoke } from "./health-watchdog-smoke";
@@ -29,6 +30,7 @@ export interface CommandDependencies extends LoadPilotRegistryOptions {
29
30
  healthWatchdogSmokeRunner?: typeof runHealthWatchdogSmoke | undefined;
30
31
  healthWatchdogSmokeCleanupRunner?: typeof cleanupHealthWatchdogSmoke | undefined;
31
32
  legacyProjectionJobRecoveryRunner?: typeof retireLegacyProjectionJob | undefined;
33
+ pilotOffboardDriver?: PilotOffboardDriver | undefined;
32
34
  }
33
35
  export declare const globalFlags: FlagDefinitions;
34
36
  export declare const commands: readonly CommandDefinition<CommandDependencies, CommandResult>[];
@@ -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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rizom/ops",
3
- "version": "0.2.0-alpha.397",
3
+ "version": "0.2.0-alpha.398",
4
4
  "description": "Operator CLI for managing private brain fleet registry repos",
5
5
  "keywords": [
6
6
  "brains",
@@ -0,0 +1,107 @@
1
+ name: Offboard
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ handles:
7
+ description: Comma-separated pilot handles
8
+ required: true
9
+ type: string
10
+ confirm:
11
+ description: Exact confirmation (sunset:<sorted-comma-separated-handles>)
12
+ required: true
13
+ type: string
14
+ apply:
15
+ description: Archive content repos and destroy DNS, servers, and desired state
16
+ required: true
17
+ type: boolean
18
+ default: false
19
+
20
+ permissions:
21
+ contents: write
22
+
23
+ concurrency:
24
+ group: offboard-pilot-users
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ offboard:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v5
32
+ with:
33
+ ref: main
34
+ fetch-depth: 0
35
+
36
+ - uses: oven-sh/setup-bun@v2
37
+
38
+ - name: Install operator tooling
39
+ run: bun install
40
+
41
+ - name: Load provider credentials
42
+ uses: ./.github/actions/varlock-env
43
+ env:
44
+ BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
45
+
46
+ - name: Resolve handles
47
+ id: handles
48
+ env:
49
+ OFFBOARD_HANDLES: ${{ inputs.handles }}
50
+ run: |
51
+ node <<'NODE'
52
+ const handles = [...new Set(process.env.OFFBOARD_HANDLES
53
+ .split(',')
54
+ .map((handle) => handle.trim())
55
+ .filter(Boolean))]
56
+ .sort((left, right) => left.localeCompare(right));
57
+ if (handles.length === 0 || handles.some((handle) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(handle))) {
58
+ throw new Error('Invalid offboarding handle list');
59
+ }
60
+ const output = process.env.GITHUB_OUTPUT;
61
+ require('node:fs').appendFileSync(output, `json=${JSON.stringify(handles)}\n`);
62
+ NODE
63
+
64
+ - name: Preview offboarding plan
65
+ env:
66
+ OFFBOARD_HANDLES_JSON: ${{ steps.handles.outputs.json }}
67
+ run: |
68
+ mapfile -t handles < <(node -e 'for (const handle of JSON.parse(process.env.OFFBOARD_HANDLES_JSON)) console.log(handle)')
69
+ bunx brains-ops user:offboard "$GITHUB_WORKSPACE" "${handles[@]}"
70
+
71
+ - name: Apply offboarding plan
72
+ if: inputs.apply
73
+ env:
74
+ OFFBOARD_CONFIRM: ${{ inputs.confirm }}
75
+ OFFBOARD_HANDLES_JSON: ${{ steps.handles.outputs.json }}
76
+ run: |
77
+ mapfile -t handles < <(node -e 'for (const handle of JSON.parse(process.env.OFFBOARD_HANDLES_JSON)) console.log(handle)')
78
+ bunx brains-ops user:offboard "$GITHUB_WORKSPACE" "${handles[@]}" \
79
+ --apply --confirm "$OFFBOARD_CONFIRM"
80
+
81
+ - name: Validate changed paths
82
+ if: inputs.apply
83
+ run: |
84
+ unexpected="$(git status --short | awk '{print $2}' | grep -Ev '^(cohorts/|users/|views/users\.md$)' || true)"
85
+ if [ -n "$unexpected" ]; then
86
+ echo "Offboarding changed unexpected paths:" >&2
87
+ echo "$unexpected" >&2
88
+ exit 1
89
+ fi
90
+
91
+ - name: Commit desired-state removal
92
+ if: inputs.apply
93
+ env:
94
+ OFFBOARD_HANDLES: ${{ inputs.handles }}
95
+ run: |
96
+ git add --all -- cohorts users views/users.md
97
+ if git diff --cached --quiet; then
98
+ echo "No desired-state changes remain to commit."
99
+ exit 0
100
+ fi
101
+
102
+ git config user.name "github-actions[bot]"
103
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
104
+ git commit -m "chore(ops): sunset pilot brains" \
105
+ -m "Retired handles: $OFFBOARD_HANDLES"
106
+ git pull --rebase origin main
107
+ git push origin HEAD:main
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
2
3
  import { requireEnv, writeGitHubOutput } from "./helpers";
3
4
 
4
5
  const eventName = requireEnv("GITHUB_EVENT_NAME");
@@ -47,6 +48,9 @@ const handles = [
47
48
  return match?.[1] ?? null;
48
49
  })
49
50
  .filter((handle): handle is string => handle !== null)
51
+ // Deleted generated files belong to retired users and must not be sent
52
+ // through the onboarding/deploy path after an offboarding commit.
53
+ .filter((handle) => existsSync(`users/${handle}.yaml`))
50
54
  .sort((left, right) => left.localeCompare(right)),
51
55
  ),
52
56
  ];
@@ -11,6 +11,7 @@ Treat these as checked-in deploy artifacts in the pilot repo:
11
11
  - `.github/workflows/deploy.yml`
12
12
  - `.github/workflows/directory-sync-stress.yml`
13
13
  - `.github/workflows/health-watchdog-smoke.yml`
14
+ - `.github/workflows/offboard.yml`
14
15
  - `.github/workflows/reconcile.yml`
15
16
 
16
17
  `.env.schema` is the single source of truth for required and sensitive deploy vars.
@@ -180,6 +181,38 @@ The shared cert bootstrap writes local cert artifacts under `.brains-ops/certs/s
180
181
 
181
182
  Preview hosts use the shape `<handle>-preview.rizom.ai`, so one wildcard origin cert for `*.rizom.ai` covers both the primary and preview hosts for every pilot user.
182
183
 
184
+ ## Pilot user offboarding
185
+
186
+ Use the manual **Offboard** workflow for explicit pilot retirement. It is the only
187
+ supported path that removes both checked-in desired state and provider resources.
188
+ Normal reconcile/deploy changes never imply destruction.
189
+
190
+ 1. Enter a comma-separated handle list. The command sorts and deduplicates it.
191
+ 2. Run with `apply: false` and review the exact servers, DNS records, and content
192
+ repositories in the plan.
193
+ 3. Enter the canonical confirmation printed by the dry run, for example
194
+ `sunset:alice,bob`.
195
+ 4. Rerun with `apply: true`.
196
+ 5. Verify the workflow's bot commit removes user YAML, encrypted secrets, generated
197
+ user directories, cohort membership, and rows from `views/users.md`.
198
+
199
+ The apply path archives each private content repository, deletes the user's main and
200
+ preview DNS records, destroys the dedicated Hetzner server, and removes desired state.
201
+ Custom-domain users also lose their managed `www` record. The operation is idempotent,
202
+ but it deliberately creates **no runtime backup**; obtain separate owner approval and
203
+ backup state before apply when retention is required.
204
+
205
+ The equivalent operator-local commands are:
206
+
207
+ ```sh
208
+ bunx brains-ops user:offboard . alice bob
209
+ bunx brains-ops user:offboard . alice bob \
210
+ --apply --confirm sunset:alice,bob
211
+ ```
212
+
213
+ The deploy handle resolver ignores deleted users, so the generated-file deletions in
214
+ the offboarding commit cannot route those handles back through onboarding/deploy.
215
+
183
216
  ## Upgrading operator behavior
184
217
 
185
218
  The pilot repository pins `@rizom/ops` in `package.json`. The scheduled and manually dispatched Upgrade workflow owns routine upgrades to that pin. It refreshes the scaffold on a branch and opens a reviewable PR; it does not change runtime desired state or authorize a deployment.