@ankhorage/infra 0.3.7 → 0.4.1
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 +2 -1
- package/dist/adapters/minikube/base/index.d.ts +1 -0
- package/dist/adapters/minikube/base/index.d.ts.map +1 -1
- package/dist/adapters/minikube/base/index.js +239 -22
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +34 -0
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/adapters/minikube/secrets/index.d.ts +7 -0
- package/dist/adapters/minikube/secrets/index.d.ts.map +1 -0
- package/dist/adapters/minikube/secrets/index.js +13 -0
- package/dist/adapters/minikube/secrets/index.js.map +1 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts +8 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts.map +1 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.js +74 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/secretStore.d.ts +19 -0
- package/dist/secretStore.d.ts.map +1 -0
- package/dist/secretStore.js +22 -0
- package/dist/secretStore.js.map +1 -0
- package/package.json +7 -3
- package/src/adapters/minikube/base/index.test.ts +456 -0
- package/src/adapters/minikube/base/index.ts +244 -20
- package/src/adapters/minikube/index.ts +44 -0
- package/src/adapters/minikube/secrets/index.test.ts +139 -0
- package/src/adapters/minikube/secrets/index.ts +19 -0
- package/src/adapters/minikube/secrets/supabase-vault/index.ts +91 -0
- package/src/index.test.ts +151 -12
- package/src/index.ts +5 -0
- package/src/package.test.ts +8 -1
- package/src/secretStore.test.ts +76 -0
- package/src/secretStore.ts +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ankhorage/infra
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f48b53f: Preserve app-specific Supabase local project identity while keeping the canonical Minikube workdir.
|
|
8
|
+
|
|
9
|
+
## 0.4.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- dd7c668: Compose the canonical secret-store adapter from `infra.secretStore.provider`, generate the released Supabase Vault migration through the existing local Supabase lifecycle, and validate OAuth credential references before infrastructure generation.
|
|
14
|
+
|
|
3
15
|
## 0.3.7
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# INFRA
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Executable infra provider and standalone CLI for Ankhorage project workflows.
|
|
9
9
|
|
|
@@ -56,3 +56,4 @@ bunx @ankhorage/infra
|
|
|
56
56
|
- [Module relationships](././paradox/diagrams/module-relationships.mmd)
|
|
57
57
|
- [Export graph](././paradox/diagrams/export-graph.mmd)
|
|
58
58
|
- [ankhorage-infra sequence](././paradox/diagrams/sequences/ankhorage-infra.mmd)
|
|
59
|
+
- [createInfraSecretStoreAdapter sequence](././paradox/diagrams/sequences/create-infra-secret-store-adapter.mmd)
|
|
@@ -3,6 +3,7 @@ import type { InfraManifestInput } from '../../../types';
|
|
|
3
3
|
export declare function generateMinikubeBaseArtifacts(args: {
|
|
4
4
|
manifest: InfraManifestInput;
|
|
5
5
|
namespace: string;
|
|
6
|
+
supabaseProjectId: string | null;
|
|
6
7
|
extraResources: string[];
|
|
7
8
|
extraEnvEntries: string[];
|
|
8
9
|
}): GeneratedInfrastructureFile[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AA4BzD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B,GAAG,2BAA2B,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/adapters/minikube/base/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AA4BzD,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B,GAAG,2BAA2B,EAAE,CAiIhC"}
|
|
@@ -4,22 +4,26 @@ const SUPABASE_LOCAL_PORT_BUCKET_SIZE = 10;
|
|
|
4
4
|
const SUPABASE_LOCAL_PORT_BUCKET_COUNT = 1000;
|
|
5
5
|
const SUPABASE_LOCAL_PORT_REFERENCE_PROJECT = 'my-app';
|
|
6
6
|
export function generateMinikubeBaseArtifacts(args) {
|
|
7
|
-
const { manifest, namespace, extraResources, extraEnvEntries } = args;
|
|
7
|
+
const { manifest, namespace, supabaseProjectId, extraResources, extraEnvEntries } = args;
|
|
8
8
|
const root = 'infra/minikube';
|
|
9
9
|
const k8sRoot = `${root}/k8s`;
|
|
10
10
|
const scriptsRoot = `${root}/scripts`;
|
|
11
11
|
const appImageRoot = `${root}/app-image`;
|
|
12
12
|
const defaultAppImage = getDefaultAppImage(namespace);
|
|
13
|
-
const supabaseLocalPorts = resolveSupabaseLocalPorts(namespace);
|
|
13
|
+
const supabaseLocalPorts = resolveSupabaseLocalPorts(supabaseProjectId ?? namespace);
|
|
14
14
|
const authScope = manifest.auth?.scope ?? 'none';
|
|
15
15
|
const authProvider = manifest.auth?.provider ?? 'none';
|
|
16
16
|
const authzEngine = manifest.auth?.authorization?.engine ?? 'none';
|
|
17
17
|
const databaseProvider = manifest.database?.provider ?? 'none';
|
|
18
|
+
const secretStoreProvider = manifest.secretStore?.provider ?? 'none';
|
|
18
19
|
const storageMetadata = resolveAppInfraStorageMetadata(manifest);
|
|
19
20
|
const profileModel = resolveSupabaseProfileModel(manifest);
|
|
20
21
|
const monitoringEnabled = manifest.deployment?.monitoring === true;
|
|
21
22
|
const domain = manifest.networking?.domain ?? '';
|
|
22
|
-
const supabaseLocalEnabled = authProvider === 'supabase' ||
|
|
23
|
+
const supabaseLocalEnabled = authProvider === 'supabase' ||
|
|
24
|
+
databaseProvider === 'supabase' ||
|
|
25
|
+
storageMetadata !== null ||
|
|
26
|
+
secretStoreProvider === 'supabase-vault';
|
|
23
27
|
return [
|
|
24
28
|
{
|
|
25
29
|
path: `${root}/README.md`,
|
|
@@ -31,6 +35,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
31
35
|
authzEngine,
|
|
32
36
|
extraResources,
|
|
33
37
|
supabaseLocalPorts,
|
|
38
|
+
supabaseProjectId,
|
|
34
39
|
supabaseLocalEnabled,
|
|
35
40
|
profileModel,
|
|
36
41
|
}),
|
|
@@ -57,6 +62,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
57
62
|
authScope,
|
|
58
63
|
authProvider,
|
|
59
64
|
databaseProvider,
|
|
65
|
+
secretStoreProvider,
|
|
60
66
|
storageMetadata,
|
|
61
67
|
monitoringEnabled,
|
|
62
68
|
domain,
|
|
@@ -102,6 +108,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
102
108
|
path: `${scriptsRoot}/status.sh`,
|
|
103
109
|
content: getStatusScript({
|
|
104
110
|
defaultNamespace: namespace,
|
|
111
|
+
supabaseProjectId,
|
|
105
112
|
supabaseLocalEnabled,
|
|
106
113
|
profileModel,
|
|
107
114
|
}),
|
|
@@ -110,6 +117,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
110
117
|
{
|
|
111
118
|
path: `${scriptsRoot}/supabase-local-env.sh`,
|
|
112
119
|
content: getSupabaseLocalEnvScript({
|
|
120
|
+
supabaseProjectId,
|
|
113
121
|
supabaseLocalPorts,
|
|
114
122
|
profileModel,
|
|
115
123
|
}),
|
|
@@ -122,7 +130,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
122
130
|
];
|
|
123
131
|
}
|
|
124
132
|
function getReadmeMarkdown(args) {
|
|
125
|
-
const { namespace, defaultAppImage, monitoringEnabled, authProvider, authzEngine, extraResources, supabaseLocalPorts, supabaseLocalEnabled, profileModel, } = args;
|
|
133
|
+
const { namespace, defaultAppImage, monitoringEnabled, authProvider, authzEngine, extraResources, supabaseLocalPorts, supabaseProjectId, supabaseLocalEnabled, profileModel, } = args;
|
|
126
134
|
const resourceLines = [
|
|
127
135
|
'namespace.yaml',
|
|
128
136
|
'app.configmap.yaml',
|
|
@@ -185,6 +193,7 @@ ${resourceLines}
|
|
|
185
193
|
## Runtime Conventions
|
|
186
194
|
|
|
187
195
|
- Namespace: \`${namespace}\`
|
|
196
|
+
- Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`
|
|
188
197
|
- Monitoring requested: \`${monitoringEnabled ? 'true' : 'false'}\`
|
|
189
198
|
- Auth provider: \`${authProvider}\`
|
|
190
199
|
- Authorization engine: \`${authzEngine}\`
|
|
@@ -272,6 +281,7 @@ Build flow:
|
|
|
272
281
|
|
|
273
282
|
- \`./scripts/up.sh\` calls \`./scripts/build-app-image.sh\` when \`APP_BUILD_ENABLED=true\`.
|
|
274
283
|
- Supabase local project root: \`infra/minikube\`${supabaseLocalEnabled ? '' : ' (unused unless Supabase-backed auth/database/storage is enabled)'}.
|
|
284
|
+
- Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`; existing local configs with another \`project_id\` must be destroyed and recreated manually.
|
|
275
285
|
- Immutable migrations live in \`supabase/migrations/\` and are applied with \`supabase migration up --local\`.
|
|
276
286
|
${profileModel.enabled ? '- Generated profile desired-state reconciliation lives in `supabase/generated/auth_profiles.sql` and records checksum state in `ankhorage_internal.generated_schema_state`.\n' : ''}- \`./scripts/supabase-local-env.sh\` can populate Supabase keys in \`.env\` from local \`supabase status -o env\`.
|
|
277
287
|
- \`./scripts/supabase-local-env.sh\` writes \`EXPO_PUBLIC_SUPABASE_URL\` + \`EXPO_PUBLIC_SUPABASE_ANON_KEY\` into \`$APP_SOURCE_DIR/.env.local\` (e.g. \`apps/card/.env.local\`) for local app runs.
|
|
@@ -356,7 +366,7 @@ metadata:
|
|
|
356
366
|
`;
|
|
357
367
|
}
|
|
358
368
|
function getAppConfigMap(args) {
|
|
359
|
-
const { namespace, authScope, authProvider, databaseProvider, storageMetadata, monitoringEnabled, domain, } = args;
|
|
369
|
+
const { namespace, authScope, authProvider, databaseProvider, secretStoreProvider, storageMetadata, monitoringEnabled, domain, } = args;
|
|
360
370
|
const storageLines = storageMetadata
|
|
361
371
|
? ` STORAGE_PROVIDER: "${storageMetadata.provider}"
|
|
362
372
|
STORAGE_BUCKETS: "${escapeYamlDoubleQuoted(storageMetadata.bucketsCsv)}"
|
|
@@ -374,6 +384,7 @@ data:
|
|
|
374
384
|
AUTH_SCOPE: "${authScope}"
|
|
375
385
|
AUTH_PROVIDER: "${authProvider}"
|
|
376
386
|
DATABASE_PROVIDER: "${databaseProvider}"
|
|
387
|
+
SECRET_STORE_PROVIDER: "${secretStoreProvider}"
|
|
377
388
|
${storageLines} NETWORK_DOMAIN: "${domain}"
|
|
378
389
|
`;
|
|
379
390
|
}
|
|
@@ -1287,7 +1298,7 @@ end;
|
|
|
1287
1298
|
$$;`;
|
|
1288
1299
|
}
|
|
1289
1300
|
function getStatusScript(args) {
|
|
1290
|
-
const { defaultNamespace, supabaseLocalEnabled, profileModel } = args;
|
|
1301
|
+
const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;
|
|
1291
1302
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1292
1303
|
const profileVerificationSql = profileModel.enabled
|
|
1293
1304
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1310,8 +1321,28 @@ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
|
1310
1321
|
SUPABASE_LOCAL_ENABLED="\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}"
|
|
1311
1322
|
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
1312
1323
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1324
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1313
1325
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1314
1326
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1327
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1328
|
+
|
|
1329
|
+
reject_supabase_project_id_override() {
|
|
1330
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1331
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1332
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1333
|
+
return 1
|
|
1334
|
+
fi
|
|
1335
|
+
|
|
1336
|
+
unset SUPABASE_PROJECT_ID
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
require_expected_supabase_project_identity() {
|
|
1340
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1341
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1342
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1343
|
+
return 1
|
|
1344
|
+
fi
|
|
1345
|
+
}
|
|
1315
1346
|
|
|
1316
1347
|
if ! command -v kubectl >/dev/null 2>&1; then
|
|
1317
1348
|
echo "kubectl is required but not installed."
|
|
@@ -1475,6 +1506,61 @@ run_supabase_sql() {
|
|
|
1475
1506
|
return 1
|
|
1476
1507
|
}
|
|
1477
1508
|
|
|
1509
|
+
validate_supabase_project_identity() {
|
|
1510
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1511
|
+
|
|
1512
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1513
|
+
return 0
|
|
1514
|
+
fi
|
|
1515
|
+
|
|
1516
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
1517
|
+
echo "- local Supabase project identity: config missing at \${config_file}"
|
|
1518
|
+
return 1
|
|
1519
|
+
fi
|
|
1520
|
+
|
|
1521
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1522
|
+
echo "- local Supabase project identity: python3 is required to read \${config_file}"
|
|
1523
|
+
return 1
|
|
1524
|
+
fi
|
|
1525
|
+
|
|
1526
|
+
python3 - "\${config_file}" <<'PY'
|
|
1527
|
+
import os
|
|
1528
|
+
import re
|
|
1529
|
+
import sys
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
def read_top_level_project_id(config_path):
|
|
1533
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
1534
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
1535
|
+
|
|
1536
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1537
|
+
for line in f:
|
|
1538
|
+
if section_re.match(line):
|
|
1539
|
+
return None
|
|
1540
|
+
match = key_re.match(line)
|
|
1541
|
+
if not match:
|
|
1542
|
+
continue
|
|
1543
|
+
raw = match.group(1).strip()
|
|
1544
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
1545
|
+
return quoted.group(1) if quoted else raw
|
|
1546
|
+
|
|
1547
|
+
return None
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
1551
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
1552
|
+
|
|
1553
|
+
if actual == expected:
|
|
1554
|
+
raise SystemExit(0)
|
|
1555
|
+
|
|
1556
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
1557
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
1558
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
1559
|
+
print('Destroy the invalid local stack and run Infra Up again.', file=sys.stderr)
|
|
1560
|
+
raise SystemExit(1)
|
|
1561
|
+
PY
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1478
1564
|
check_immutable_migrations_applied() {
|
|
1479
1565
|
local migrations_dir="\${SUPABASE_PROJECT_DIR}/supabase/migrations"
|
|
1480
1566
|
local applied_versions
|
|
@@ -1509,8 +1595,14 @@ if [[ "\${AUTH_RUNTIME_MODE}" == "local" && "\${SUPABASE_LOCAL_ENABLED}" == "tru
|
|
|
1509
1595
|
echo "Local Supabase database checks:"
|
|
1510
1596
|
status_failed=0
|
|
1511
1597
|
|
|
1512
|
-
if
|
|
1513
|
-
|
|
1598
|
+
if ! require_expected_supabase_project_identity; then
|
|
1599
|
+
status_failed=1
|
|
1600
|
+
elif ! reject_supabase_project_id_override; then
|
|
1601
|
+
status_failed=1
|
|
1602
|
+
elif require_supabase_cli_capabilities; then
|
|
1603
|
+
if ! validate_supabase_project_identity; then
|
|
1604
|
+
status_failed=1
|
|
1605
|
+
elif supabase --workdir "\${SUPABASE_PROJECT_DIR}" status >/dev/null 2>&1; then
|
|
1514
1606
|
STATUS_ENV="$(supabase --workdir "\${SUPABASE_PROJECT_DIR}" status -o env)"
|
|
1515
1607
|
SUPABASE_DB_URL="$(read_env_value "\${STATUS_ENV}" DB_URL)"
|
|
1516
1608
|
if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
@@ -1581,7 +1673,7 @@ fi
|
|
|
1581
1673
|
`;
|
|
1582
1674
|
}
|
|
1583
1675
|
function getSupabaseLocalEnvScript(args) {
|
|
1584
|
-
const { supabaseLocalPorts, profileModel } = args;
|
|
1676
|
+
const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;
|
|
1585
1677
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1586
1678
|
const profileVerificationSql = profileModel.enabled
|
|
1587
1679
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1603,9 +1695,29 @@ fi
|
|
|
1603
1695
|
|
|
1604
1696
|
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
1605
1697
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1698
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1606
1699
|
APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
|
|
1607
1700
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1608
1701
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1702
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1703
|
+
|
|
1704
|
+
reject_supabase_project_id_override() {
|
|
1705
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1706
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1707
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1708
|
+
exit 1
|
|
1709
|
+
fi
|
|
1710
|
+
|
|
1711
|
+
unset SUPABASE_PROJECT_ID
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
require_expected_supabase_project_identity() {
|
|
1715
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1716
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1717
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1718
|
+
exit 1
|
|
1719
|
+
fi
|
|
1720
|
+
}
|
|
1609
1721
|
|
|
1610
1722
|
SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
|
|
1611
1723
|
SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
|
|
@@ -1651,6 +1763,11 @@ require_supabase_cli_capabilities() {
|
|
|
1651
1763
|
exit 1
|
|
1652
1764
|
fi
|
|
1653
1765
|
|
|
1766
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1767
|
+
echo "python3 is required to reconcile supabase/config.toml before local Supabase startup."
|
|
1768
|
+
exit 1
|
|
1769
|
+
fi
|
|
1770
|
+
|
|
1654
1771
|
local version
|
|
1655
1772
|
version="$(supabase --version | awk '{print $NF}')"
|
|
1656
1773
|
if ! version_ge "\${version}" "2.106.0"; then
|
|
@@ -1712,6 +1829,8 @@ run_checked_sql_file() {
|
|
|
1712
1829
|
psql "\${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "\${sql_file}"
|
|
1713
1830
|
}
|
|
1714
1831
|
|
|
1832
|
+
require_expected_supabase_project_identity
|
|
1833
|
+
reject_supabase_project_id_override
|
|
1715
1834
|
require_supabase_cli_capabilities
|
|
1716
1835
|
|
|
1717
1836
|
if [[ ! -d "\${SUPABASE_PROJECT_DIR}" ]]; then
|
|
@@ -1722,18 +1841,6 @@ fi
|
|
|
1722
1841
|
configure_supabase_local_ports() {
|
|
1723
1842
|
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1724
1843
|
|
|
1725
|
-
if ! command -v python3 >/dev/null 2>&1; then
|
|
1726
|
-
echo "python3 is required to patch supabase/config.toml with generated local port defaults."
|
|
1727
|
-
echo "Install python3 (recommended) or manually edit \${config_file} ports:"
|
|
1728
|
-
echo "- [api] port = \${SUPABASE_LOCAL_API_PORT}"
|
|
1729
|
-
echo "- [db] port = \${SUPABASE_LOCAL_DB_PORT}"
|
|
1730
|
-
echo "- [db] shadow_port = \${SUPABASE_LOCAL_SHADOW_PORT}"
|
|
1731
|
-
echo "- [studio] port = \${SUPABASE_LOCAL_STUDIO_PORT}"
|
|
1732
|
-
echo "- [inbucket] port = \${SUPABASE_LOCAL_INBUCKET_PORT}"
|
|
1733
|
-
echo "- [analytics] port = \${SUPABASE_LOCAL_ANALYTICS_PORT}"
|
|
1734
|
-
exit 1
|
|
1735
|
-
fi
|
|
1736
|
-
|
|
1737
1844
|
python3 - "\${config_file}" <<'PY'
|
|
1738
1845
|
import os
|
|
1739
1846
|
import re
|
|
@@ -1837,8 +1944,114 @@ if __name__ == "__main__":
|
|
|
1837
1944
|
PY
|
|
1838
1945
|
}
|
|
1839
1946
|
|
|
1947
|
+
write_supabase_project_identity_for_new_config() {
|
|
1948
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1949
|
+
|
|
1950
|
+
require_expected_supabase_project_identity
|
|
1951
|
+
|
|
1952
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1953
|
+
echo "python3 is required to write Supabase project_id in \${config_file}."
|
|
1954
|
+
exit 1
|
|
1955
|
+
fi
|
|
1956
|
+
|
|
1957
|
+
python3 - "\${config_file}" <<'PY'
|
|
1958
|
+
import os
|
|
1959
|
+
import re
|
|
1960
|
+
import sys
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
def write_top_level_project_id(config_path, expected):
|
|
1964
|
+
key_re = re.compile(r'^(\\s*)project_id\\s*=\\s*(.*?)(\\s*(#.*)?)?$')
|
|
1965
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
1966
|
+
|
|
1967
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1968
|
+
lines = f.readlines()
|
|
1969
|
+
|
|
1970
|
+
section_start = len(lines)
|
|
1971
|
+
for i, line in enumerate(lines):
|
|
1972
|
+
if section_re.match(line):
|
|
1973
|
+
section_start = i
|
|
1974
|
+
break
|
|
1975
|
+
|
|
1976
|
+
for i in range(section_start):
|
|
1977
|
+
match = key_re.match(lines[i])
|
|
1978
|
+
if not match:
|
|
1979
|
+
continue
|
|
1980
|
+
indent = match.group(1) or ''
|
|
1981
|
+
trailing = (match.group(3) or '').rstrip('\\n')
|
|
1982
|
+
lines[i] = f'{indent}project_id = "{expected}"{trailing}\\n'
|
|
1983
|
+
break
|
|
1984
|
+
else:
|
|
1985
|
+
insert_at = section_start
|
|
1986
|
+
lines.insert(insert_at, f'project_id = "{expected}"\\n')
|
|
1987
|
+
|
|
1988
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
1989
|
+
f.writelines(lines)
|
|
1990
|
+
|
|
1991
|
+
|
|
1992
|
+
write_top_level_project_id(sys.argv[1], os.environ['EXPECTED_SUPABASE_PROJECT_ID'])
|
|
1993
|
+
PY
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
validate_supabase_project_identity() {
|
|
1997
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1998
|
+
|
|
1999
|
+
require_expected_supabase_project_identity
|
|
2000
|
+
|
|
2001
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
2002
|
+
echo "Supabase project identity mismatch."
|
|
2003
|
+
printf 'Expected "%s", found missing.\n' "\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
2004
|
+
echo "The existing local Supabase project belongs to a different identity."
|
|
2005
|
+
echo "Destroy the invalid local stack and its project-owned resources, then run Infra Up again."
|
|
2006
|
+
exit 1
|
|
2007
|
+
fi
|
|
2008
|
+
|
|
2009
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
2010
|
+
echo "python3 is required to read Supabase project_id in \${config_file}."
|
|
2011
|
+
exit 1
|
|
2012
|
+
fi
|
|
2013
|
+
|
|
2014
|
+
python3 - "\${config_file}" <<'PY'
|
|
2015
|
+
import os
|
|
2016
|
+
import re
|
|
2017
|
+
import sys
|
|
2018
|
+
|
|
2019
|
+
|
|
2020
|
+
def read_top_level_project_id(config_path):
|
|
2021
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
2022
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
2023
|
+
|
|
2024
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
2025
|
+
for line in f:
|
|
2026
|
+
if section_re.match(line):
|
|
2027
|
+
return None
|
|
2028
|
+
match = key_re.match(line)
|
|
2029
|
+
if not match:
|
|
2030
|
+
continue
|
|
2031
|
+
raw = match.group(1).strip()
|
|
2032
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
2033
|
+
return quoted.group(1) if quoted else raw
|
|
2034
|
+
|
|
2035
|
+
return None
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
2039
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
2040
|
+
|
|
2041
|
+
if actual == expected:
|
|
2042
|
+
raise SystemExit(0)
|
|
2043
|
+
|
|
2044
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
2045
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
2046
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
2047
|
+
print('The existing local Supabase project belongs to a different identity.', file=sys.stderr)
|
|
2048
|
+
print('Destroy the invalid local stack and its project-owned resources, then run Infra Up again.', file=sys.stderr)
|
|
2049
|
+
raise SystemExit(1)
|
|
2050
|
+
PY
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1840
2053
|
assert_supabase_local_ports_available() {
|
|
1841
|
-
local project_label="\${
|
|
2054
|
+
local project_label="\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
1842
2055
|
|
|
1843
2056
|
ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
|
|
1844
2057
|
import os
|
|
@@ -1916,8 +2129,10 @@ PY
|
|
|
1916
2129
|
if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
|
|
1917
2130
|
echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
|
|
1918
2131
|
supabase --workdir "\${SUPABASE_PROJECT_DIR}" init --yes >/dev/null
|
|
2132
|
+
write_supabase_project_identity_for_new_config
|
|
1919
2133
|
fi
|
|
1920
2134
|
|
|
2135
|
+
validate_supabase_project_identity
|
|
1921
2136
|
configure_supabase_local_ports
|
|
1922
2137
|
|
|
1923
2138
|
STATUS_ENV="$(
|
|
@@ -1989,6 +2204,7 @@ start_supabase_local_stack() {
|
|
|
1989
2204
|
}
|
|
1990
2205
|
|
|
1991
2206
|
if [[ -z "\${STATUS_ENV}" ]]; then
|
|
2207
|
+
validate_supabase_project_identity
|
|
1992
2208
|
assert_supabase_local_ports_available
|
|
1993
2209
|
echo "Supabase local stack not detected. Starting with 'supabase start'..."
|
|
1994
2210
|
start_supabase_local_stack
|
|
@@ -2002,6 +2218,7 @@ if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
|
2002
2218
|
fi
|
|
2003
2219
|
|
|
2004
2220
|
echo "Applying pending immutable Supabase migrations..."
|
|
2221
|
+
validate_supabase_project_identity
|
|
2005
2222
|
run_checked_command \
|
|
2006
2223
|
"Immutable Supabase migration application" \
|
|
2007
2224
|
"" \
|