@ankhorage/infra 0.4.0 → 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 +6 -0
- package/README.md +1 -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 +231 -20
- 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 +26 -0
- package/dist/adapters/minikube/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/minikube/base/index.test.ts +456 -0
- package/src/adapters/minikube/base/index.ts +235 -19
- package/src/adapters/minikube/index.ts +37 -0
- package/src/adapters/minikube/secrets/index.test.ts +37 -1
- package/src/index.test.ts +151 -12
package/CHANGELOG.md
CHANGED
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
|
|
|
@@ -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,13 +4,13 @@ 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';
|
|
@@ -35,6 +35,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
35
35
|
authzEngine,
|
|
36
36
|
extraResources,
|
|
37
37
|
supabaseLocalPorts,
|
|
38
|
+
supabaseProjectId,
|
|
38
39
|
supabaseLocalEnabled,
|
|
39
40
|
profileModel,
|
|
40
41
|
}),
|
|
@@ -107,6 +108,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
107
108
|
path: `${scriptsRoot}/status.sh`,
|
|
108
109
|
content: getStatusScript({
|
|
109
110
|
defaultNamespace: namespace,
|
|
111
|
+
supabaseProjectId,
|
|
110
112
|
supabaseLocalEnabled,
|
|
111
113
|
profileModel,
|
|
112
114
|
}),
|
|
@@ -115,6 +117,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
115
117
|
{
|
|
116
118
|
path: `${scriptsRoot}/supabase-local-env.sh`,
|
|
117
119
|
content: getSupabaseLocalEnvScript({
|
|
120
|
+
supabaseProjectId,
|
|
118
121
|
supabaseLocalPorts,
|
|
119
122
|
profileModel,
|
|
120
123
|
}),
|
|
@@ -127,7 +130,7 @@ export function generateMinikubeBaseArtifacts(args) {
|
|
|
127
130
|
];
|
|
128
131
|
}
|
|
129
132
|
function getReadmeMarkdown(args) {
|
|
130
|
-
const { namespace, defaultAppImage, monitoringEnabled, authProvider, authzEngine, extraResources, supabaseLocalPorts, supabaseLocalEnabled, profileModel, } = args;
|
|
133
|
+
const { namespace, defaultAppImage, monitoringEnabled, authProvider, authzEngine, extraResources, supabaseLocalPorts, supabaseProjectId, supabaseLocalEnabled, profileModel, } = args;
|
|
131
134
|
const resourceLines = [
|
|
132
135
|
'namespace.yaml',
|
|
133
136
|
'app.configmap.yaml',
|
|
@@ -190,6 +193,7 @@ ${resourceLines}
|
|
|
190
193
|
## Runtime Conventions
|
|
191
194
|
|
|
192
195
|
- Namespace: \`${namespace}\`
|
|
196
|
+
- Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`
|
|
193
197
|
- Monitoring requested: \`${monitoringEnabled ? 'true' : 'false'}\`
|
|
194
198
|
- Auth provider: \`${authProvider}\`
|
|
195
199
|
- Authorization engine: \`${authzEngine}\`
|
|
@@ -277,6 +281,7 @@ Build flow:
|
|
|
277
281
|
|
|
278
282
|
- \`./scripts/up.sh\` calls \`./scripts/build-app-image.sh\` when \`APP_BUILD_ENABLED=true\`.
|
|
279
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.
|
|
280
285
|
- Immutable migrations live in \`supabase/migrations/\` and are applied with \`supabase migration up --local\`.
|
|
281
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\`.
|
|
282
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.
|
|
@@ -1293,7 +1298,7 @@ end;
|
|
|
1293
1298
|
$$;`;
|
|
1294
1299
|
}
|
|
1295
1300
|
function getStatusScript(args) {
|
|
1296
|
-
const { defaultNamespace, supabaseLocalEnabled, profileModel } = args;
|
|
1301
|
+
const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;
|
|
1297
1302
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1298
1303
|
const profileVerificationSql = profileModel.enabled
|
|
1299
1304
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1316,8 +1321,28 @@ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
|
1316
1321
|
SUPABASE_LOCAL_ENABLED="\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}"
|
|
1317
1322
|
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
1318
1323
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1324
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1319
1325
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1320
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
|
+
}
|
|
1321
1346
|
|
|
1322
1347
|
if ! command -v kubectl >/dev/null 2>&1; then
|
|
1323
1348
|
echo "kubectl is required but not installed."
|
|
@@ -1481,6 +1506,61 @@ run_supabase_sql() {
|
|
|
1481
1506
|
return 1
|
|
1482
1507
|
}
|
|
1483
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
|
+
|
|
1484
1564
|
check_immutable_migrations_applied() {
|
|
1485
1565
|
local migrations_dir="\${SUPABASE_PROJECT_DIR}/supabase/migrations"
|
|
1486
1566
|
local applied_versions
|
|
@@ -1515,8 +1595,14 @@ if [[ "\${AUTH_RUNTIME_MODE}" == "local" && "\${SUPABASE_LOCAL_ENABLED}" == "tru
|
|
|
1515
1595
|
echo "Local Supabase database checks:"
|
|
1516
1596
|
status_failed=0
|
|
1517
1597
|
|
|
1518
|
-
if
|
|
1519
|
-
|
|
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
|
|
1520
1606
|
STATUS_ENV="$(supabase --workdir "\${SUPABASE_PROJECT_DIR}" status -o env)"
|
|
1521
1607
|
SUPABASE_DB_URL="$(read_env_value "\${STATUS_ENV}" DB_URL)"
|
|
1522
1608
|
if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
@@ -1587,7 +1673,7 @@ fi
|
|
|
1587
1673
|
`;
|
|
1588
1674
|
}
|
|
1589
1675
|
function getSupabaseLocalEnvScript(args) {
|
|
1590
|
-
const { supabaseLocalPorts, profileModel } = args;
|
|
1676
|
+
const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;
|
|
1591
1677
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1592
1678
|
const profileVerificationSql = profileModel.enabled
|
|
1593
1679
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1609,9 +1695,29 @@ fi
|
|
|
1609
1695
|
|
|
1610
1696
|
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
1611
1697
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1698
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1612
1699
|
APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
|
|
1613
1700
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1614
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
|
+
}
|
|
1615
1721
|
|
|
1616
1722
|
SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
|
|
1617
1723
|
SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
|
|
@@ -1657,6 +1763,11 @@ require_supabase_cli_capabilities() {
|
|
|
1657
1763
|
exit 1
|
|
1658
1764
|
fi
|
|
1659
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
|
+
|
|
1660
1771
|
local version
|
|
1661
1772
|
version="$(supabase --version | awk '{print $NF}')"
|
|
1662
1773
|
if ! version_ge "\${version}" "2.106.0"; then
|
|
@@ -1718,6 +1829,8 @@ run_checked_sql_file() {
|
|
|
1718
1829
|
psql "\${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "\${sql_file}"
|
|
1719
1830
|
}
|
|
1720
1831
|
|
|
1832
|
+
require_expected_supabase_project_identity
|
|
1833
|
+
reject_supabase_project_id_override
|
|
1721
1834
|
require_supabase_cli_capabilities
|
|
1722
1835
|
|
|
1723
1836
|
if [[ ! -d "\${SUPABASE_PROJECT_DIR}" ]]; then
|
|
@@ -1728,18 +1841,6 @@ fi
|
|
|
1728
1841
|
configure_supabase_local_ports() {
|
|
1729
1842
|
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1730
1843
|
|
|
1731
|
-
if ! command -v python3 >/dev/null 2>&1; then
|
|
1732
|
-
echo "python3 is required to patch supabase/config.toml with generated local port defaults."
|
|
1733
|
-
echo "Install python3 (recommended) or manually edit \${config_file} ports:"
|
|
1734
|
-
echo "- [api] port = \${SUPABASE_LOCAL_API_PORT}"
|
|
1735
|
-
echo "- [db] port = \${SUPABASE_LOCAL_DB_PORT}"
|
|
1736
|
-
echo "- [db] shadow_port = \${SUPABASE_LOCAL_SHADOW_PORT}"
|
|
1737
|
-
echo "- [studio] port = \${SUPABASE_LOCAL_STUDIO_PORT}"
|
|
1738
|
-
echo "- [inbucket] port = \${SUPABASE_LOCAL_INBUCKET_PORT}"
|
|
1739
|
-
echo "- [analytics] port = \${SUPABASE_LOCAL_ANALYTICS_PORT}"
|
|
1740
|
-
exit 1
|
|
1741
|
-
fi
|
|
1742
|
-
|
|
1743
1844
|
python3 - "\${config_file}" <<'PY'
|
|
1744
1845
|
import os
|
|
1745
1846
|
import re
|
|
@@ -1843,8 +1944,114 @@ if __name__ == "__main__":
|
|
|
1843
1944
|
PY
|
|
1844
1945
|
}
|
|
1845
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
|
+
|
|
1846
2053
|
assert_supabase_local_ports_available() {
|
|
1847
|
-
local project_label="\${
|
|
2054
|
+
local project_label="\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
1848
2055
|
|
|
1849
2056
|
ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
|
|
1850
2057
|
import os
|
|
@@ -1922,8 +2129,10 @@ PY
|
|
|
1922
2129
|
if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
|
|
1923
2130
|
echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
|
|
1924
2131
|
supabase --workdir "\${SUPABASE_PROJECT_DIR}" init --yes >/dev/null
|
|
2132
|
+
write_supabase_project_identity_for_new_config
|
|
1925
2133
|
fi
|
|
1926
2134
|
|
|
2135
|
+
validate_supabase_project_identity
|
|
1927
2136
|
configure_supabase_local_ports
|
|
1928
2137
|
|
|
1929
2138
|
STATUS_ENV="$(
|
|
@@ -1995,6 +2204,7 @@ start_supabase_local_stack() {
|
|
|
1995
2204
|
}
|
|
1996
2205
|
|
|
1997
2206
|
if [[ -z "\${STATUS_ENV}" ]]; then
|
|
2207
|
+
validate_supabase_project_identity
|
|
1998
2208
|
assert_supabase_local_ports_available
|
|
1999
2209
|
echo "Supabase local stack not detected. Starting with 'supabase start'..."
|
|
2000
2210
|
start_supabase_local_stack
|
|
@@ -2008,6 +2218,7 @@ if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
|
2008
2218
|
fi
|
|
2009
2219
|
|
|
2010
2220
|
echo "Applying pending immutable Supabase migrations..."
|
|
2221
|
+
validate_supabase_project_identity
|
|
2011
2222
|
run_checked_command \
|
|
2012
2223
|
"Immutable Supabase migration application" \
|
|
2013
2224
|
"" \
|