@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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export function getSupabaseMigrationCommandScript(): string {
2
+ return `echo "Applying pending Supabase migrations..."
3
+ SUPABASE_TELEMETRY_DISABLED=1 supabase --yes migration up --db-url "\${SUPABASE_DB_URL}"
4
+ echo "Supabase migrations applied."`;
5
+ }
@@ -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 \`supabase migration up --db-url "$SUPABASE_DB_URL"\`.
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('supabase migration up --db-url "${SUPABASE_DB_URL}"');
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');