@ankhorage/infra 1.0.2 → 1.0.3

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,2 @@
1
+ export declare function getSupabaseMigrationCommandScript(): string;
2
+ //# sourceMappingURL=supabaseMigrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supabaseMigrations.d.ts","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/supabaseMigrations.ts"],"names":[],"mappings":"AAAA,wBAAgB,iCAAiC,IAAI,MAAM,CAI1D"}
@@ -0,0 +1,6 @@
1
+ export function getSupabaseMigrationCommandScript() {
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
+ }
6
+ //# sourceMappingURL=supabaseMigrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supabaseMigrations.js","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/supabaseMigrations.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iCAAiC;IAC/C,OAAO;;oCAE2B,CAAC;AACrC,CAAC","sourcesContent":["export function getSupabaseMigrationCommandScript(): string {\n return `echo \"Applying pending Supabase migrations...\"\nSUPABASE_TELEMETRY_DISABLED=1 supabase --yes migration up --db-url \"\\${SUPABASE_DB_URL}\"\necho \"Supabase migrations applied.\"`;\n}\n"]}
@@ -93,7 +93,8 @@ ${SUPABASE_VAULT_MIGRATION_PATH}
93
93
  \`\`\`
94
94
 
95
95
  is applied by the Kubernetes-owned Supabase migration lifecycle during \`scripts/up.sh\`
96
- with \`supabase migration up --db-url "$SUPABASE_DB_URL"\`.
96
+ with command-scoped CLI telemetry disabled around
97
+ \`supabase --yes migration up --db-url "$SUPABASE_DB_URL"\`.
97
98
 
98
99
  ## Security boundary
99
100
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/adapters/minikube/secrets/supabase-vault/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,oCAAoC,GACrC,MAAM,2BAA2B,CAAC;AAKnC,MAAM,CAAC,MAAM,6BAA6B,GACxC,8EAA8E,CAAC;AAEjF,MAAM,UAAU,yCAAyC,CAAC,IAGzD;IACC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAE7C,OAAO;QACL,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,6BAA6B;gBACnC,OAAO,EAAE,mCAAmC,EAAE;aAC/C;YACD;gBACE,IAAI,EAAE,0CAA0C;gBAChD,OAAO,EAAE,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC;aACzC;SACF;QACD,SAAS,EAAE,EAAE;QACb,iBAAiB,EAAE,EAAE;QACrB,UAAU,EAAE,CAAC,yBAAyB,oCAAoC,EAAE,CAAC;QAC7E,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAED,SAAS,mCAAmC;IAC1C,OAAO,GAAG,4BAA4B,CAAC,IAAI,EAAE;SAC1C,OAAO,CACN,uEAAuE,EACvE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAuCR,CACC;SACA,IAAI,EAAE,IAAI,CAAC;AAChB,CAAC;AAED,SAAS,6BAA6B,CAAC,QAA4B;IACjE,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,OAAO;QAAE,OAAO;IAE5B,MAAM,gBAAgB,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC;IAC1F,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,MAAM,QAAQ,IAAI,gBAAgB,EAAE,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CACb,mBAAmB,QAAQ,CAAC,EAAE,kDAAkD,CACjF,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,SAAiB;IACxC,OAAO;;;;;+BAKsB,oCAAoC;;;;;;EAMjE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;oCAsBK,SAAS;CAC5C,CAAC;AACF,CAAC","sourcesContent":["import {\n SUPABASE_VAULT_MIGRATION_SQL,\n SUPABASE_VAULT_SECRET_STORE_PROVIDER,\n} from '@ankhorage/supabase-vault';\n\nimport type { InfraManifestInput } from '../../../../types';\nimport type { MinikubeAdapterArtifacts } from '../../contracts';\n\nexport const SUPABASE_VAULT_MIGRATION_PATH =\n 'infra/minikube/supabase/migrations/202607120001_ankhorage_supabase_vault.sql';\n\nexport function generateSupabaseVaultSecretStoreArtifacts(args: {\n readonly manifest: InfraManifestInput;\n readonly namespace: string;\n}): MinikubeAdapterArtifacts {\n validateOAuthSecretReferences(args.manifest);\n\n return {\n files: [\n {\n path: SUPABASE_VAULT_MIGRATION_PATH,\n content: getCurrentSupabaseVaultMigrationSql(),\n },\n {\n path: 'infra/minikube/secrets/supabase-vault.md',\n content: getRuntimeGuide(args.namespace),\n },\n ],\n resources: [],\n providerLifecycle: [],\n envEntries: [`SECRET_STORE_PROVIDER=${SUPABASE_VAULT_SECRET_STORE_PROVIDER}`],\n warnings: [],\n };\n}\n\nfunction getCurrentSupabaseVaultMigrationSql(): string {\n return `${SUPABASE_VAULT_MIGRATION_SQL.trim()\n .replace(\n 'create extension if not exists supabase_vault with schema extensions;',\n () => `create schema if not exists vault;\ndo $$\nbegin\n create extension if not exists supabase_vault with schema vault;\nexception\n when insufficient_privilege then\n create table if not exists vault.secrets (\n id uuid primary key default gen_random_uuid(),\n secret text not null,\n name text,\n description text,\n created_at timestamptz not null default now()\n );\n\n create or replace function vault.create_secret(\n secret text,\n name text default null,\n description text default null\n )\n returns uuid\n language plpgsql\n security definer\n set search_path = vault, public\n as $vault$\n declare\n secret_id uuid;\n begin\n insert into vault.secrets (secret, name, description)\n values (secret, name, description)\n returning id into secret_id;\n\n return secret_id;\n end;\n $vault$;\n\n create or replace view vault.decrypted_secrets as\n select id, secret as decrypted_secret\n from vault.secrets;\nend\n$$;`,\n )\n .trim()}\\n`;\n}\n\nfunction validateOAuthSecretReferences(manifest: InfraManifestInput): void {\n const oauth = manifest.auth?.oauth;\n if (!oauth?.enabled) return;\n\n const enabledProviders = oauth.providers.filter((provider) => provider.enabled !== false);\n if (enabledProviders.length === 0) {\n throw new Error('OAuth is enabled but no OAuth provider is enabled.');\n }\n\n for (const provider of enabledProviders) {\n if (!provider.credentialsRef?.trim()) {\n throw new Error(\n `OAuth provider \"${provider.id}\" is enabled but does not define credentialsRef.`,\n );\n }\n }\n}\n\nfunction getRuntimeGuide(namespace: string): string {\n return `# Supabase Vault secret store\n\nThis project selects the provider-neutral secret-store backend through:\n\n\\`\\`\\`text\ninfra.secretStore.provider = ${SUPABASE_VAULT_SECRET_STORE_PROVIDER}\n\\`\\`\\`\n\nThe immutable migration at:\n\n\\`\\`\\`text\n${SUPABASE_VAULT_MIGRATION_PATH}\n\\`\\`\\`\n\nis applied by the Kubernetes-owned Supabase migration lifecycle during \\`scripts/up.sh\\`\nwith \\`supabase migration up --db-url \"$SUPABASE_DB_URL\"\\`.\n\n## Security boundary\n\n- Secret values are stored only through Supabase Vault.\n- Public manifests contain logical references such as \\`auth/oauth/google\\`, never values.\n- Project and environment scope are supplied by trusted server code.\n- The browser may receive secret metadata, but must never receive resolved values or Vault IDs.\n- Bootstrap database access must come from trusted process environment or workload identity.\n- Do not expose service-role keys, provider secrets, or resolved payloads through\n \\`NEXT_PUBLIC_*\\`, \\`EXPO_PUBLIC_*\\`, generated YAML, tracked JSON, or app bundles.\n\n## Trusted adapter composition\n\nUse \\`createInfraSecretStoreAdapter\\` from \\`@ankhorage/infra\\` in server-only code and pass\na trusted Supabase Vault SQL client. The app manifest chooses the provider; privileged connection\nconfiguration does not belong in the manifest.\n\nGenerated Kubernetes namespace: \\`${namespace}\\`.\n`;\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/adapters/minikube/secrets/supabase-vault/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,oCAAoC,GACrC,MAAM,2BAA2B,CAAC;AAKnC,MAAM,CAAC,MAAM,6BAA6B,GACxC,8EAA8E,CAAC;AAEjF,MAAM,UAAU,yCAAyC,CAAC,IAGzD;IACC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAE7C,OAAO;QACL,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,6BAA6B;gBACnC,OAAO,EAAE,mCAAmC,EAAE;aAC/C;YACD;gBACE,IAAI,EAAE,0CAA0C;gBAChD,OAAO,EAAE,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC;aACzC;SACF;QACD,SAAS,EAAE,EAAE;QACb,iBAAiB,EAAE,EAAE;QACrB,UAAU,EAAE,CAAC,yBAAyB,oCAAoC,EAAE,CAAC;QAC7E,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAED,SAAS,mCAAmC;IAC1C,OAAO,GAAG,4BAA4B,CAAC,IAAI,EAAE;SAC1C,OAAO,CACN,uEAAuE,EACvE,GAAG,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAuCR,CACC;SACA,IAAI,EAAE,IAAI,CAAC;AAChB,CAAC;AAED,SAAS,6BAA6B,CAAC,QAA4B;IACjE,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,OAAO;QAAE,OAAO;IAE5B,MAAM,gBAAgB,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC;IAC1F,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,MAAM,QAAQ,IAAI,gBAAgB,EAAE,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CACb,mBAAmB,QAAQ,CAAC,EAAE,kDAAkD,CACjF,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,SAAiB;IACxC,OAAO;;;;;+BAKsB,oCAAoC;;;;;;EAMjE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;oCAuBK,SAAS;CAC5C,CAAC;AACF,CAAC","sourcesContent":["import {\n SUPABASE_VAULT_MIGRATION_SQL,\n SUPABASE_VAULT_SECRET_STORE_PROVIDER,\n} from '@ankhorage/supabase-vault';\n\nimport type { InfraManifestInput } from '../../../../types';\nimport type { MinikubeAdapterArtifacts } from '../../contracts';\n\nexport const SUPABASE_VAULT_MIGRATION_PATH =\n 'infra/minikube/supabase/migrations/202607120001_ankhorage_supabase_vault.sql';\n\nexport function generateSupabaseVaultSecretStoreArtifacts(args: {\n readonly manifest: InfraManifestInput;\n readonly namespace: string;\n}): MinikubeAdapterArtifacts {\n validateOAuthSecretReferences(args.manifest);\n\n return {\n files: [\n {\n path: SUPABASE_VAULT_MIGRATION_PATH,\n content: getCurrentSupabaseVaultMigrationSql(),\n },\n {\n path: 'infra/minikube/secrets/supabase-vault.md',\n content: getRuntimeGuide(args.namespace),\n },\n ],\n resources: [],\n providerLifecycle: [],\n envEntries: [`SECRET_STORE_PROVIDER=${SUPABASE_VAULT_SECRET_STORE_PROVIDER}`],\n warnings: [],\n };\n}\n\nfunction getCurrentSupabaseVaultMigrationSql(): string {\n return `${SUPABASE_VAULT_MIGRATION_SQL.trim()\n .replace(\n 'create extension if not exists supabase_vault with schema extensions;',\n () => `create schema if not exists vault;\ndo $$\nbegin\n create extension if not exists supabase_vault with schema vault;\nexception\n when insufficient_privilege then\n create table if not exists vault.secrets (\n id uuid primary key default gen_random_uuid(),\n secret text not null,\n name text,\n description text,\n created_at timestamptz not null default now()\n );\n\n create or replace function vault.create_secret(\n secret text,\n name text default null,\n description text default null\n )\n returns uuid\n language plpgsql\n security definer\n set search_path = vault, public\n as $vault$\n declare\n secret_id uuid;\n begin\n insert into vault.secrets (secret, name, description)\n values (secret, name, description)\n returning id into secret_id;\n\n return secret_id;\n end;\n $vault$;\n\n create or replace view vault.decrypted_secrets as\n select id, secret as decrypted_secret\n from vault.secrets;\nend\n$$;`,\n )\n .trim()}\\n`;\n}\n\nfunction validateOAuthSecretReferences(manifest: InfraManifestInput): void {\n const oauth = manifest.auth?.oauth;\n if (!oauth?.enabled) return;\n\n const enabledProviders = oauth.providers.filter((provider) => provider.enabled !== false);\n if (enabledProviders.length === 0) {\n throw new Error('OAuth is enabled but no OAuth provider is enabled.');\n }\n\n for (const provider of enabledProviders) {\n if (!provider.credentialsRef?.trim()) {\n throw new Error(\n `OAuth provider \"${provider.id}\" is enabled but does not define credentialsRef.`,\n );\n }\n }\n}\n\nfunction getRuntimeGuide(namespace: string): string {\n return `# Supabase Vault secret store\n\nThis project selects the provider-neutral secret-store backend through:\n\n\\`\\`\\`text\ninfra.secretStore.provider = ${SUPABASE_VAULT_SECRET_STORE_PROVIDER}\n\\`\\`\\`\n\nThe immutable migration at:\n\n\\`\\`\\`text\n${SUPABASE_VAULT_MIGRATION_PATH}\n\\`\\`\\`\n\nis applied by the Kubernetes-owned Supabase migration lifecycle during \\`scripts/up.sh\\`\nwith command-scoped CLI telemetry disabled around\n\\`supabase --yes migration up --db-url \"$SUPABASE_DB_URL\"\\`.\n\n## Security boundary\n\n- Secret values are stored only through Supabase Vault.\n- Public manifests contain logical references such as \\`auth/oauth/google\\`, never values.\n- Project and environment scope are supplied by trusted server code.\n- The browser may receive secret metadata, but must never receive resolved values or Vault IDs.\n- Bootstrap database access must come from trusted process environment or workload identity.\n- Do not expose service-role keys, provider secrets, or resolved payloads through\n \\`NEXT_PUBLIC_*\\`, \\`EXPO_PUBLIC_*\\`, generated YAML, tracked JSON, or app bundles.\n\n## Trusted adapter composition\n\nUse \\`createInfraSecretStoreAdapter\\` from \\`@ankhorage/infra\\` in server-only code and pass\na trusted Supabase Vault SQL client. The app manifest chooses the provider; privileged connection\nconfiguration does not belong in the manifest.\n\nGenerated Kubernetes namespace: \\`${namespace}\\`.\n`;\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/infra",
3
3
  "type": "module",
4
- "version": "1.0.2",
4
+ "version": "1.0.3",
5
5
  "description": "Project-aware infrastructure generation library and executable Ankh provider for Ankhorage.",
6
6
  "homepage": "https://github.com/ankhorage/infra#readme",
7
7
  "bugs": {
@@ -25,7 +25,7 @@ when provided by caller.
25
25
 
26
26
  ## Runtime Scripts
27
27
 
28
- - `scripts/up.sh`: starts `minikube -p <slug>`, applies manifests, waits for Supabase, runs migrations with `supabase migration up --db-url "$SUPABASE_DB_URL"`, builds/loads the app image, and starts slug-owned port-forwards.
28
+ - `scripts/up.sh`: starts `minikube -p <slug>`, applies manifests, waits for Supabase, runs migrations with CLI telemetry disabled for that command, builds/loads the app image, and starts slug-owned port-forwards.
29
29
  - `scripts/down.sh`: stops slug-owned port-forwards, then runs `minikube stop -p <slug>`. Persistent profile data remains.
30
30
  - `scripts/reset.sh`: requires `ANKH_RESET_CONFIRM=<slug>` and deletes/recreates namespaces `app` and `supabase`, including Supabase DB and Storage PVC data. It does not delete the Minikube profile.
31
31
  - `scripts/destroy.sh`: stops slug-owned port-forwards and runs `minikube delete -p <slug>`.
@@ -37,7 +37,14 @@ when provided by caller.
37
37
 
38
38
  Runtime ownership is Kubernetes. Migration authoring/history remains Supabase migration
39
39
  files. Migration execution targets the Kubernetes Postgres endpoint via
40
- `supabase migration up --db-url "$SUPABASE_DB_URL"`.
40
+ `SUPABASE_TELEMETRY_DISABLED=1 supabase --yes migration up --db-url "$SUPABASE_DB_URL"`.
41
+ The migration process does not require stdin. `--yes` remains defense in depth for future
42
+ CLI prompts; it is not the hang fix. Command-scoped telemetry disable avoids the unbounded
43
+ telemetry shutdown in Supabase CLI 2.106.0 when PostHog is blocked, without changing the
44
+ user's persistent telemetry preference. Migration failures remain fatal under
45
+ `set -Eeuo pipefail`, and the completion message is printed only after a zero exit status.
46
+ The lifecycle adds no migration deadline and supports the affected CLI version; using a
47
+ current Supabase CLI is still recommended.
41
48
 
42
49
  The generated manifests are based on the current official Supabase self-hosting Docker
43
50
  topology, service documentation, environment-variable contracts, and pinned official
@@ -69,10 +69,22 @@ describe('generateMinikubeBaseArtifacts app-owned cluster model', () => {
69
69
  const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
70
70
  const readme = getFile(result.files, 'infra/minikube/README.md');
71
71
 
72
- expect(upScript).toContain('supabase migration up --db-url "${SUPABASE_DB_URL}"');
72
+ const migrationFunction = upScript.slice(
73
+ upScript.indexOf('run_supabase_migrations()'),
74
+ upScript.indexOf('reconcile_supabase_profile()'),
75
+ );
76
+
77
+ expect(migrationFunction).toContain(
78
+ 'SUPABASE_TELEMETRY_DISABLED=1 supabase --yes migration up --db-url "${SUPABASE_DB_URL}"',
79
+ );
80
+ expect(migrationFunction).not.toContain('supabase migration up --db-url "${SUPABASE_DB_URL}"');
81
+ expect(migrationFunction).toContain('echo "Applying pending Supabase migrations..."');
82
+ expect(migrationFunction).toContain('echo "Supabase migrations applied."');
73
83
  expect(upScript).toContain('export SUPABASE_DB_URL PGSSLMODE=disable');
74
84
  expect(upScript).not.toContain('migration up --local');
75
85
  expect(upScript).toContain('cd "${ROOT_DIR}"');
86
+ expect(migrationFunction).toContain('if [[ "${SUPABASE_RUNTIME_ENABLED}" != "true" ]]; then');
87
+ expect(migrationFunction).toContain('if [[ -d "${ROOT_DIR}/supabase/migrations" ]]; then');
76
88
  expect(upScript).toContain('set_required_env_default POSTGRES_PASSWORD "$(random_hex 32)"');
77
89
  expect(upScript).not.toContain('set_env_default POSTGRES_PASSWORD "$(random_base64');
78
90
  expect(upScript).toContain(
@@ -82,6 +94,30 @@ describe('generateMinikubeBaseArtifacts app-owned cluster model', () => {
82
94
  expect(readme).toContain('Migration authoring/history remains Supabase');
83
95
  });
84
96
 
97
+ test('preserves Supabase-disabled migration behavior', () => {
98
+ const result = generateInfrastructure(
99
+ {
100
+ deployment: {
101
+ target: 'minikube',
102
+ monitoring: false,
103
+ },
104
+ plugins: [],
105
+ },
106
+ {
107
+ appManifest: createAppManifest('frontend-only'),
108
+ },
109
+ );
110
+ const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
111
+ const migrationFunction = upScript.slice(
112
+ upScript.indexOf('run_supabase_migrations()'),
113
+ upScript.indexOf('reconcile_supabase_profile()'),
114
+ );
115
+
116
+ expect(upScript).toContain('SUPABASE_RUNTIME_ENABLED="false"');
117
+ expect(migrationFunction).toContain('if [[ "${SUPABASE_RUNTIME_ENABLED}" != "true" ]]; then');
118
+ expect(migrationFunction).toContain('return 0');
119
+ });
120
+
85
121
  test('lists and generates Supabase Kubernetes bootstrap and gateway contract files', () => {
86
122
  const result = generateInfrastructure(createSupabaseManifest(), {
87
123
  appManifest: createAppManifest('scanner'),
@@ -3,6 +3,7 @@ import type { InfraManifestInput } from '../../../types';
3
3
  import type { ResolvedProfileModel } from '../auth/supabase/profile';
4
4
  import { resolveSupabaseProfileModel } from '../auth/supabase/profile';
5
5
  import type { MinikubeProviderLifecycle } from '../contracts';
6
+ import { getSupabaseMigrationCommandScript } from './supabaseMigrations';
6
7
 
7
8
  export const APP_NAMESPACE = 'app';
8
9
  const SUPABASE_NAMESPACE = 'supabase';
@@ -379,7 +380,7 @@ host-level Supabase Compose runtime.
379
380
 
380
381
  ## Lifecycle Semantics
381
382
 
382
- - \`up.sh\`: starts \`minikube -p ${appSlug}\`, deploys provider namespaces, runs migrations with \`supabase migration up --db-url "$SUPABASE_DB_URL"\`, starts slug-owned port-forwards, and deploys the app runtime.
383
+ - \`up.sh\`: starts \`minikube -p ${appSlug}\`, deploys provider namespaces, runs migrations with CLI telemetry disabled for that command, starts slug-owned port-forwards, and deploys the app runtime.
383
384
  - \`down.sh\`: stops slug-owned port-forwards, then stops \`minikube -p ${appSlug}\`. Persistent data remains in the profile.
384
385
  - \`reset.sh\`: requires \`ANKH_RESET_CONFIRM=${appSlug}\`; ${resetDescription}. It does not delete the Minikube profile.
385
386
  - \`destroy.sh\`: deletes only \`minikube -p ${appSlug}\`.
@@ -392,7 +393,13 @@ ${resourceLines}
392
393
 
393
394
  Supabase runtime ownership is Kubernetes. Migration authoring/history remains Supabase
394
395
  migration files. Migration execution targets the Kubernetes Postgres endpoint through
395
- \`supabase migration up --db-url "$SUPABASE_DB_URL"\`.
396
+ \`SUPABASE_TELEMETRY_DISABLED=1 supabase --yes migration up --db-url "$SUPABASE_DB_URL"\`.
397
+ The migration process does not require stdin. \`--yes\` remains defense in depth for future
398
+ CLI prompts; it is not the hang fix. Command-scoped telemetry disable avoids the unbounded
399
+ telemetry shutdown in Supabase CLI 2.106.0 when PostHog is blocked, without changing the
400
+ user's persistent telemetry preference. Migration failures stop \`up.sh\`, and the completion
401
+ message is printed only after a zero exit status. No migration deadline is added; using a
402
+ current Supabase CLI remains recommended.
396
403
 
397
404
  Supabase manifests are generated from the current official Supabase self-hosting Docker
398
405
  topology, service documentation, environment-variable contracts, and pinned official images.
@@ -2266,6 +2273,7 @@ function getUpScript(args: {
2266
2273
  oauthProviders,
2267
2274
  secretStoreProvider,
2268
2275
  });
2276
+ const supabaseMigrationCommand = getSupabaseMigrationCommandScript();
2269
2277
 
2270
2278
  return `#!/usr/bin/env bash
2271
2279
  set -Eeuo pipefail
@@ -2621,7 +2629,7 @@ run_supabase_migrations() {
2621
2629
  require_command supabase
2622
2630
  (
2623
2631
  cd "\${ROOT_DIR}"
2624
- supabase migration up --db-url "\${SUPABASE_DB_URL}"
2632
+ ${indentShellBlock(supabaseMigrationCommand, 6)}
2625
2633
  )
2626
2634
  fi
2627
2635
  }
@@ -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');