@rizom/ops 0.2.0-alpha.397 → 0.2.0-alpha.399
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/dist/brains-ops.js +253 -253
- package/dist/index.d.ts +1 -0
- package/dist/index.js +193 -193
- package/dist/run-command.d.ts +2 -0
- package/dist/user-offboard.d.ts +61 -0
- package/package.json +1 -1
- package/templates/rover-pilot/.github/workflows/offboard.yml +107 -0
- package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +4 -0
- package/templates/rover-pilot/docs/operator-playbook.md +33 -0
package/dist/run-command.d.ts
CHANGED
|
@@ -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
|
@@ -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.
|