@ankhorage/infra 1.0.2 → 1.0.4
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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/adapters/minikube/base/index.d.ts.map +1 -1
- package/dist/adapters/minikube/base/index.js +19 -4
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/dist/adapters/minikube/base/supabaseMigrations.d.ts +2 -0
- package/dist/adapters/minikube/base/supabaseMigrations.d.ts.map +1 -0
- package/dist/adapters/minikube/base/supabaseMigrations.js +6 -0
- package/dist/adapters/minikube/base/supabaseMigrations.js.map +1 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.js +2 -1
- package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/minikube/README.md +16 -3
- package/src/adapters/minikube/base/appImageBuild.test.ts +263 -0
- package/src/adapters/minikube/base/index.test.ts +45 -1
- package/src/adapters/minikube/base/index.ts +19 -4
- package/src/adapters/minikube/base/supabaseMigrations.test.ts +168 -0
- package/src/adapters/minikube/base/supabaseMigrations.ts +5 -0
- package/src/adapters/minikube/secrets/supabase-vault/index.ts +2 -1
- package/src/index.test.ts +4 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from 'node:child_process';
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { getSupabaseMigrationCommandScript } from './supabaseMigrations';
|
|
9
|
+
|
|
10
|
+
const FAKE_DB_URL = 'postgres://fake-user:fake-password@127.0.0.1:5432/fake-database';
|
|
11
|
+
|
|
12
|
+
describe('generated Supabase migration command', () => {
|
|
13
|
+
test('disables CLI telemetry for closed-stdin migrations and reports progress', async () => {
|
|
14
|
+
const result = await runGeneratedMigrationCommand(0);
|
|
15
|
+
|
|
16
|
+
expect(result.exitCode).toBe(0);
|
|
17
|
+
expect(result.timedOut).toBe(false);
|
|
18
|
+
expect(result.stdout).toContain('Applying pending Supabase migrations...');
|
|
19
|
+
expect(result.stdout).toContain('Supabase migrations applied.');
|
|
20
|
+
expect(result.stderr).toContain('fake Supabase migration output');
|
|
21
|
+
expect(result.args).toEqual(['--yes', 'migration', 'up', '--db-url', FAKE_DB_URL]);
|
|
22
|
+
expect(result.telemetryDisabled).toBe('1');
|
|
23
|
+
expect(result.stdout).not.toContain(FAKE_DB_URL);
|
|
24
|
+
expect(result.stderr).not.toContain(FAKE_DB_URL);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('propagates migration failures without reporting completion', async () => {
|
|
28
|
+
const result = await runGeneratedMigrationCommand(23);
|
|
29
|
+
|
|
30
|
+
expect(result.exitCode).toBe(23);
|
|
31
|
+
expect(result.timedOut).toBe(false);
|
|
32
|
+
expect(result.stdout).toContain('Applying pending Supabase migrations...');
|
|
33
|
+
expect(result.stdout).not.toContain('Supabase migrations applied.');
|
|
34
|
+
expect(result.stderr).toContain('fake Supabase migration output');
|
|
35
|
+
expect(result.telemetryDisabled).toBe('1');
|
|
36
|
+
expect(result.stdout).not.toContain(FAKE_DB_URL);
|
|
37
|
+
expect(result.stderr).not.toContain(FAKE_DB_URL);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function runGeneratedMigrationCommand(fakeExitCode: number): Promise<{
|
|
42
|
+
args: string[];
|
|
43
|
+
exitCode: number | null;
|
|
44
|
+
stderr: string;
|
|
45
|
+
stdout: string;
|
|
46
|
+
telemetryDisabled: string;
|
|
47
|
+
timedOut: boolean;
|
|
48
|
+
}> {
|
|
49
|
+
const root = await mkdtemp(path.join(tmpdir(), 'ankh-infra-supabase-migrations-'));
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const fakeBin = path.join(root, 'bin');
|
|
53
|
+
const fakeSupabase = path.join(fakeBin, 'supabase');
|
|
54
|
+
const argsFile = path.join(root, 'supabase-args');
|
|
55
|
+
const telemetryFile = path.join(root, 'supabase-telemetry-disabled');
|
|
56
|
+
const scriptFile = path.join(root, 'run-migrations.sh');
|
|
57
|
+
await mkdir(fakeBin, { recursive: true });
|
|
58
|
+
await writeFile(
|
|
59
|
+
fakeSupabase,
|
|
60
|
+
`#!/usr/bin/env bash
|
|
61
|
+
set -euo pipefail
|
|
62
|
+
|
|
63
|
+
printf '%s\\n' "$@" > "\${ARGS_FILE:?}"
|
|
64
|
+
printf '%s\\n' "\${SUPABASE_TELEMETRY_DISABLED:-}" > "\${TELEMETRY_FILE:?}"
|
|
65
|
+
|
|
66
|
+
echo "fake Supabase migration output" >&2
|
|
67
|
+
|
|
68
|
+
# Supabase CLI 2.106.0 reached this kind of unbounded telemetry shutdown after
|
|
69
|
+
# database work when PostHog was blocked. The generated lifecycle must suppress
|
|
70
|
+
# telemetry for this automation-only command so neither the wrapper nor its Go
|
|
71
|
+
# child can remain waiting after migration completion or failure.
|
|
72
|
+
if [[ "\${SUPABASE_TELEMETRY_DISABLED:-}" != "1" ]]; then
|
|
73
|
+
while true; do
|
|
74
|
+
sleep 1
|
|
75
|
+
done
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
exit "\${FAKE_SUPABASE_EXIT_CODE:-0}"
|
|
79
|
+
`,
|
|
80
|
+
'utf8',
|
|
81
|
+
);
|
|
82
|
+
await chmod(fakeSupabase, 0o755);
|
|
83
|
+
await writeFile(
|
|
84
|
+
scriptFile,
|
|
85
|
+
`#!/usr/bin/env bash
|
|
86
|
+
set -Eeuo pipefail
|
|
87
|
+
|
|
88
|
+
SUPABASE_DB_URL="${FAKE_DB_URL}"
|
|
89
|
+
export SUPABASE_DB_URL
|
|
90
|
+
|
|
91
|
+
${getSupabaseMigrationCommandScript()}
|
|
92
|
+
`,
|
|
93
|
+
'utf8',
|
|
94
|
+
);
|
|
95
|
+
await chmod(scriptFile, 0o755);
|
|
96
|
+
|
|
97
|
+
const child = spawn('bash', [scriptFile], {
|
|
98
|
+
cwd: root,
|
|
99
|
+
env: {
|
|
100
|
+
...process.env,
|
|
101
|
+
ARGS_FILE: argsFile,
|
|
102
|
+
FAKE_SUPABASE_EXIT_CODE: String(fakeExitCode),
|
|
103
|
+
PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
|
|
104
|
+
TELEMETRY_FILE: telemetryFile,
|
|
105
|
+
},
|
|
106
|
+
detached: process.platform !== 'win32',
|
|
107
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
108
|
+
});
|
|
109
|
+
let stdout = '';
|
|
110
|
+
let stderr = '';
|
|
111
|
+
child.stdout.setEncoding('utf8');
|
|
112
|
+
child.stderr.setEncoding('utf8');
|
|
113
|
+
child.stdout.on('data', (chunk: string) => {
|
|
114
|
+
stdout += chunk;
|
|
115
|
+
});
|
|
116
|
+
child.stderr.on('data', (chunk: string) => {
|
|
117
|
+
stderr += chunk;
|
|
118
|
+
});
|
|
119
|
+
const { exitCode, timedOut } = await waitForChild(child, 2_000);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
args: (await readFile(argsFile, 'utf8')).trim().split('\n'),
|
|
123
|
+
exitCode,
|
|
124
|
+
stderr,
|
|
125
|
+
stdout,
|
|
126
|
+
telemetryDisabled: (await readFile(telemetryFile, 'utf8')).trim(),
|
|
127
|
+
timedOut,
|
|
128
|
+
};
|
|
129
|
+
} finally {
|
|
130
|
+
await rm(root, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function waitForChild(
|
|
135
|
+
child: ChildProcess,
|
|
136
|
+
timeoutMs: number,
|
|
137
|
+
): Promise<{ exitCode: number | null; timedOut: boolean }> {
|
|
138
|
+
let timedOut = false;
|
|
139
|
+
const timeout = setTimeout(() => {
|
|
140
|
+
timedOut = true;
|
|
141
|
+
terminateChildTree(child);
|
|
142
|
+
}, timeoutMs);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
146
|
+
child.once('error', reject);
|
|
147
|
+
child.once('close', resolve);
|
|
148
|
+
});
|
|
149
|
+
return { exitCode, timedOut };
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function terminateChildTree(child: ChildProcess): void {
|
|
156
|
+
if (child.pid === undefined) return;
|
|
157
|
+
|
|
158
|
+
if (process.platform !== 'win32') {
|
|
159
|
+
try {
|
|
160
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
161
|
+
return;
|
|
162
|
+
} catch {
|
|
163
|
+
// The process may already have exited between the timeout and cleanup.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
child.kill('SIGKILL');
|
|
168
|
+
}
|
|
@@ -115,7 +115,8 @@ ${SUPABASE_VAULT_MIGRATION_PATH}
|
|
|
115
115
|
\`\`\`
|
|
116
116
|
|
|
117
117
|
is applied by the Kubernetes-owned Supabase migration lifecycle during \`scripts/up.sh\`
|
|
118
|
-
with
|
|
118
|
+
with command-scoped CLI telemetry disabled around
|
|
119
|
+
\`supabase --yes migration up --db-url "$SUPABASE_DB_URL"\`.
|
|
119
120
|
|
|
120
121
|
## Security boundary
|
|
121
122
|
|
package/src/index.test.ts
CHANGED
|
@@ -85,7 +85,10 @@ describe('generateInfrastructure', () => {
|
|
|
85
85
|
expect(upScript).not.toContain('SUPABASE_RUNTIME_ENABLED="${SUPABASE_RUNTIME_ENABLED');
|
|
86
86
|
expect(upScript).toContain('minikube start -p "${PROFILE}"');
|
|
87
87
|
expect(upScript).toContain('kubectl --context "${PROFILE}" apply -k "${K8S_DIR}"');
|
|
88
|
-
expect(upScript).toContain(
|
|
88
|
+
expect(upScript).toContain(
|
|
89
|
+
'SUPABASE_TELEMETRY_DISABLED=1 supabase --yes migration up --db-url "${SUPABASE_DB_URL}"',
|
|
90
|
+
);
|
|
91
|
+
expect(upScript).not.toContain('supabase migration up --db-url "${SUPABASE_DB_URL}"');
|
|
89
92
|
expect(upScript).toContain('create secret generic supabase-runtime-secrets');
|
|
90
93
|
expect(upScript).toContain('create secret generic supabase-public-runtime');
|
|
91
94
|
expect(upScript).toContain('rollout status deployment/app-runtime --timeout=180s');
|