@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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/adapters/minikube/base/index.d.ts.map +1 -1
- package/dist/adapters/minikube/base/index.js +11 -3
- 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 +9 -2
- package/src/adapters/minikube/base/index.test.ts +37 -1
- package/src/adapters/minikube/base/index.ts +11 -3
- 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 @@
|
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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');
|