@rizom/ops 0.2.0-alpha.254 → 0.2.0-alpha.255
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 +12 -0
- package/dist/brains-ops.js +215 -202
- package/dist/directory-sync-stress-system.d.ts +63 -0
- package/dist/directory-sync-stress.d.ts +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +209 -196
- package/dist/run-command.d.ts +3 -0
- package/package.json +1 -1
- package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
- package/templates/rover-pilot/.github/workflows/deploy.yml +2 -52
- package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +100 -0
- package/templates/rover-pilot/docs/operator-playbook.md +15 -0
package/dist/run-command.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ 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
7
|
import type { UserRunner } from "./user-runner";
|
|
8
|
+
import type { cleanupDirectorySyncStress, runDeployedDirectorySyncStress } from "./directory-sync-stress-system";
|
|
8
9
|
export interface CommandResult {
|
|
9
10
|
success: boolean;
|
|
10
11
|
message?: string;
|
|
@@ -17,6 +18,8 @@ export interface CommandDependencies extends LoadPilotRegistryOptions {
|
|
|
17
18
|
lookupHost?: LookupHost | undefined;
|
|
18
19
|
bootstrapRunCommand?: OpsRunCommand | undefined;
|
|
19
20
|
sshKeygen?: SshKeygen | undefined;
|
|
21
|
+
directorySyncStressRunner?: typeof runDeployedDirectorySyncStress | undefined;
|
|
22
|
+
directorySyncStressCleanupRunner?: typeof cleanupDirectorySyncStress | undefined;
|
|
20
23
|
}
|
|
21
24
|
export declare const globalFlags: FlagDefinitions;
|
|
22
25
|
export declare const commands: readonly CommandDefinition<CommandDependencies, CommandResult>[];
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -87,60 +87,10 @@ jobs:
|
|
|
87
87
|
- name: Install operator tooling
|
|
88
88
|
run: bun install
|
|
89
89
|
|
|
90
|
-
- name: Load shared env via
|
|
90
|
+
- name: Load shared env via Varlock
|
|
91
|
+
uses: ./.github/actions/varlock-env
|
|
91
92
|
env:
|
|
92
93
|
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
93
|
-
run: |
|
|
94
|
-
for attempt in 1 2 3; do
|
|
95
|
-
if bunx varlock@1.1.0 load --path .env.schema --format json --compact > /tmp/varlock-env.json; then
|
|
96
|
-
break
|
|
97
|
-
fi
|
|
98
|
-
if [ "$attempt" = "3" ]; then
|
|
99
|
-
exit 1
|
|
100
|
-
fi
|
|
101
|
-
echo "Varlock load failed (attempt $attempt/3); retrying in 5s..."
|
|
102
|
-
sleep 5
|
|
103
|
-
done
|
|
104
|
-
node <<'NODE'
|
|
105
|
-
import { appendFileSync, readFileSync } from "node:fs";
|
|
106
|
-
const env = JSON.parse(readFileSync('/tmp/varlock-env.json', 'utf8'));
|
|
107
|
-
const githubEnvPath = process.env.GITHUB_ENV;
|
|
108
|
-
if (!githubEnvPath) {
|
|
109
|
-
throw new Error('Missing GITHUB_ENV');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const newline = String.fromCharCode(10);
|
|
113
|
-
const carriageReturn = String.fromCharCode(13);
|
|
114
|
-
const chunks = [];
|
|
115
|
-
for (const [key, value] of Object.entries(env)) {
|
|
116
|
-
if (key === 'BWS_ACCESS_TOKEN' || value === null || value === undefined) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const text = String(value)
|
|
121
|
-
.split(carriageReturn + newline)
|
|
122
|
-
.join(newline);
|
|
123
|
-
const escapedMask = text
|
|
124
|
-
.replace(/%/g, '%25')
|
|
125
|
-
.replace(/\r/g, '%0D')
|
|
126
|
-
.replace(/\n/g, '%0A');
|
|
127
|
-
if (escapedMask.length > 0) {
|
|
128
|
-
process.stdout.write('::add-mask::' + escapedMask + newline);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (text.includes(newline)) {
|
|
132
|
-
let delimiter = `VARLOCK_${key}_EOF`;
|
|
133
|
-
while (text.includes(delimiter)) {
|
|
134
|
-
delimiter += '_X';
|
|
135
|
-
}
|
|
136
|
-
chunks.push(`${key}<<${delimiter}${newline}${text}${newline}${delimiter}`);
|
|
137
|
-
} else {
|
|
138
|
-
chunks.push(`${key}=${text}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
appendFileSync(githubEnvPath, chunks.join(newline) + newline);
|
|
143
|
-
NODE
|
|
144
94
|
|
|
145
95
|
- name: Decrypt user secrets
|
|
146
96
|
id: user_secrets
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
name: Directory Sync Stress
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
handle:
|
|
7
|
+
description: Smoke handle only
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
default: smoke
|
|
11
|
+
profile:
|
|
12
|
+
description: Workload profile
|
|
13
|
+
required: true
|
|
14
|
+
type: choice
|
|
15
|
+
default: regression
|
|
16
|
+
options:
|
|
17
|
+
- regression
|
|
18
|
+
- load
|
|
19
|
+
- stress
|
|
20
|
+
confirm:
|
|
21
|
+
description: Type stress:<handle> to confirm reversible content writes
|
|
22
|
+
required: true
|
|
23
|
+
type: string
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
|
|
28
|
+
concurrency:
|
|
29
|
+
group: directory-sync-stress-${{ inputs.handle }}
|
|
30
|
+
cancel-in-progress: false
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
stress:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
timeout-minutes: 75
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v5
|
|
38
|
+
|
|
39
|
+
- uses: oven-sh/setup-bun@v2
|
|
40
|
+
|
|
41
|
+
- name: Install operator tooling
|
|
42
|
+
run: bun install
|
|
43
|
+
|
|
44
|
+
- name: Load Bitwarden environment through Varlock
|
|
45
|
+
uses: ./.github/actions/varlock-env
|
|
46
|
+
env:
|
|
47
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
48
|
+
|
|
49
|
+
- name: Run smoke directory-sync stress profile
|
|
50
|
+
env:
|
|
51
|
+
HANDLE_INPUT: ${{ inputs.handle }}
|
|
52
|
+
CONFIRM_INPUT: ${{ inputs.confirm }}
|
|
53
|
+
run: |
|
|
54
|
+
bunx brains-ops stress:directory-sync "$GITHUB_WORKSPACE" "$HANDLE_INPUT" \
|
|
55
|
+
--profile "${{ inputs.profile }}" \
|
|
56
|
+
--confirm "$CONFIRM_INPUT" \
|
|
57
|
+
--artifacts-dir "$RUNNER_TEMP/directory-sync-stress"
|
|
58
|
+
|
|
59
|
+
- name: Upload stress evidence
|
|
60
|
+
if: always()
|
|
61
|
+
uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: directory-sync-stress-${{ inputs.handle }}-${{ inputs.profile }}-${{ github.run_id }}
|
|
64
|
+
path: ${{ runner.temp }}/directory-sync-stress
|
|
65
|
+
if-no-files-found: warn
|
|
66
|
+
|
|
67
|
+
cleanup:
|
|
68
|
+
needs: stress
|
|
69
|
+
if: always()
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
timeout-minutes: 20
|
|
72
|
+
steps:
|
|
73
|
+
- uses: actions/checkout@v5
|
|
74
|
+
|
|
75
|
+
- uses: oven-sh/setup-bun@v2
|
|
76
|
+
|
|
77
|
+
- name: Install operator tooling
|
|
78
|
+
run: bun install
|
|
79
|
+
|
|
80
|
+
- name: Load Bitwarden environment through Varlock
|
|
81
|
+
uses: ./.github/actions/varlock-env
|
|
82
|
+
env:
|
|
83
|
+
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
|
|
84
|
+
|
|
85
|
+
- name: Remove residual stress probes
|
|
86
|
+
env:
|
|
87
|
+
HANDLE_INPUT: ${{ inputs.handle }}
|
|
88
|
+
CONFIRM_INPUT: ${{ inputs.confirm }}
|
|
89
|
+
run: |
|
|
90
|
+
bunx brains-ops stress:directory-sync:cleanup "$GITHUB_WORKSPACE" "$HANDLE_INPUT" \
|
|
91
|
+
--confirm "$CONFIRM_INPUT" \
|
|
92
|
+
--artifacts-dir "$RUNNER_TEMP/directory-sync-stress-cleanup"
|
|
93
|
+
|
|
94
|
+
- name: Upload cleanup evidence
|
|
95
|
+
if: always()
|
|
96
|
+
uses: actions/upload-artifact@v4
|
|
97
|
+
with:
|
|
98
|
+
name: directory-sync-stress-cleanup-${{ inputs.handle }}-${{ github.run_id }}
|
|
99
|
+
path: ${{ runner.temp }}/directory-sync-stress-cleanup
|
|
100
|
+
if-no-files-found: warn
|
|
@@ -9,6 +9,7 @@ Treat these as checked-in deploy artifacts in the pilot repo:
|
|
|
9
9
|
- `deploy/scripts/`
|
|
10
10
|
- `.github/workflows/build.yml`
|
|
11
11
|
- `.github/workflows/deploy.yml`
|
|
12
|
+
- `.github/workflows/directory-sync-stress.yml`
|
|
12
13
|
- `.github/workflows/reconcile.yml`
|
|
13
14
|
|
|
14
15
|
`.env.schema` is the single source of truth for required and sensitive deploy vars.
|
|
@@ -66,6 +67,20 @@ During the approved window:
|
|
|
66
67
|
|
|
67
68
|
If any gate fails, disable all three workflows again. Restore the prior pilot desired-state and dependency revision, reconcile with the prior ops version, and redeploy the prior image tag/digest as one rollback pair. Verify the prior `/health` version and identity/content/site checks before re-enabling automation. Never restore only config or only an image.
|
|
68
69
|
|
|
70
|
+
## Directory-sync stress gate
|
|
71
|
+
|
|
72
|
+
Use the manual `Directory Sync Stress` workflow only against a disposable smoke user. It refuses a target unless the handle, domain, and content repository all identify smoke and the confirmation input exactly matches `stress:<handle>`.
|
|
73
|
+
|
|
74
|
+
Profiles are deterministic and reversible:
|
|
75
|
+
|
|
76
|
+
- `regression`: 20 probes;
|
|
77
|
+
- `load`: ramps to 350 probes, updates all, renames 100, updates again, then deletes all;
|
|
78
|
+
- `stress`: ramps to 700 probes and renames 200 before cleanup.
|
|
79
|
+
|
|
80
|
+
The workflow loads operator credentials through Bitwarden/Varlock, but it is separate from Deploy and cannot deploy an image. It creates a rollback branch before the first content write, gates on health timeouts during the monitored workload window, preserves warmup and cleanup samples as evidence, uploads JSON/Markdown/runtime artifacts, and runs an independent idempotent cleanup job with `if: always()`. Once cleanup confirms that no probes remain, it also prunes retained `ops/directory-sync-stress-backup-*` branches; if probes remain, the branches stay available for recovery.
|
|
81
|
+
|
|
82
|
+
Treat any gated health failure, restart, OOM, residual probe, or entity-baseline drift as a failed gate. Do not restart the target during measurement. Recovery is a separate operator action after evidence collection.
|
|
83
|
+
|
|
69
84
|
## Stale deploy lock recovery
|
|
70
85
|
|
|
71
86
|
Kamal intentionally leaves its remote deploy lock in place when a deployment is cancelled or interrupted. Confirm that no deployment for the user is still active before releasing the lock, then use the deploy workflow's explicit recovery input:
|