@ankhorage/infra 0.4.0 → 0.4.2
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 +13 -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 +245 -26
- 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/auth/supabase/profile.integration.test.ts +170 -2
- package/src/adapters/minikube/base/index.test.ts +552 -0
- package/src/adapters/minikube/base/index.ts +249 -25
- 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
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @ankhorage/infra
|
|
2
2
|
|
|
3
|
+
## 0.4.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 53c8e75: Make generated disabled Supabase profile verification safe when local generated profile state has
|
|
8
|
+
never existed.
|
|
9
|
+
|
|
10
|
+
## 0.4.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- f48b53f: Preserve app-specific Supabase local project identity while keeping the canonical Minikube workdir.
|
|
15
|
+
|
|
3
16
|
## 0.4.0
|
|
4
17
|
|
|
5
18
|
### Minor 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
|
|
|
@@ -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.
|
|
@@ -943,13 +948,21 @@ $$;`;
|
|
|
943
948
|
}
|
|
944
949
|
function getSupabaseProfileDisabledGuardSql() {
|
|
945
950
|
return `do $$
|
|
951
|
+
declare
|
|
952
|
+
has_profile_state boolean := false;
|
|
946
953
|
begin
|
|
947
|
-
if to_regclass('ankhorage_internal.generated_schema_state') is not null
|
|
948
|
-
|
|
949
|
-
select
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
954
|
+
if to_regclass('ankhorage_internal.generated_schema_state') is not null then
|
|
955
|
+
execute
|
|
956
|
+
'select exists (
|
|
957
|
+
select 1
|
|
958
|
+
from ankhorage_internal.generated_schema_state
|
|
959
|
+
where artifact_key = $1
|
|
960
|
+
)'
|
|
961
|
+
into has_profile_state
|
|
962
|
+
using 'auth.profile';
|
|
963
|
+
end if;
|
|
964
|
+
|
|
965
|
+
if has_profile_state then
|
|
953
966
|
raise exception 'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration';
|
|
954
967
|
end if;
|
|
955
968
|
end;
|
|
@@ -1293,7 +1306,7 @@ end;
|
|
|
1293
1306
|
$$;`;
|
|
1294
1307
|
}
|
|
1295
1308
|
function getStatusScript(args) {
|
|
1296
|
-
const { defaultNamespace, supabaseLocalEnabled, profileModel } = args;
|
|
1309
|
+
const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;
|
|
1297
1310
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1298
1311
|
const profileVerificationSql = profileModel.enabled
|
|
1299
1312
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1316,8 +1329,28 @@ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
|
1316
1329
|
SUPABASE_LOCAL_ENABLED="\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}"
|
|
1317
1330
|
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
1318
1331
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1332
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1319
1333
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1320
1334
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1335
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1336
|
+
|
|
1337
|
+
reject_supabase_project_id_override() {
|
|
1338
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1339
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1340
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1341
|
+
return 1
|
|
1342
|
+
fi
|
|
1343
|
+
|
|
1344
|
+
unset SUPABASE_PROJECT_ID
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
require_expected_supabase_project_identity() {
|
|
1348
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1349
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1350
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1351
|
+
return 1
|
|
1352
|
+
fi
|
|
1353
|
+
}
|
|
1321
1354
|
|
|
1322
1355
|
if ! command -v kubectl >/dev/null 2>&1; then
|
|
1323
1356
|
echo "kubectl is required but not installed."
|
|
@@ -1481,6 +1514,61 @@ run_supabase_sql() {
|
|
|
1481
1514
|
return 1
|
|
1482
1515
|
}
|
|
1483
1516
|
|
|
1517
|
+
validate_supabase_project_identity() {
|
|
1518
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1519
|
+
|
|
1520
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1521
|
+
return 0
|
|
1522
|
+
fi
|
|
1523
|
+
|
|
1524
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
1525
|
+
echo "- local Supabase project identity: config missing at \${config_file}"
|
|
1526
|
+
return 1
|
|
1527
|
+
fi
|
|
1528
|
+
|
|
1529
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1530
|
+
echo "- local Supabase project identity: python3 is required to read \${config_file}"
|
|
1531
|
+
return 1
|
|
1532
|
+
fi
|
|
1533
|
+
|
|
1534
|
+
python3 - "\${config_file}" <<'PY'
|
|
1535
|
+
import os
|
|
1536
|
+
import re
|
|
1537
|
+
import sys
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
def read_top_level_project_id(config_path):
|
|
1541
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
1542
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
1543
|
+
|
|
1544
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1545
|
+
for line in f:
|
|
1546
|
+
if section_re.match(line):
|
|
1547
|
+
return None
|
|
1548
|
+
match = key_re.match(line)
|
|
1549
|
+
if not match:
|
|
1550
|
+
continue
|
|
1551
|
+
raw = match.group(1).strip()
|
|
1552
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
1553
|
+
return quoted.group(1) if quoted else raw
|
|
1554
|
+
|
|
1555
|
+
return None
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
1559
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
1560
|
+
|
|
1561
|
+
if actual == expected:
|
|
1562
|
+
raise SystemExit(0)
|
|
1563
|
+
|
|
1564
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
1565
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
1566
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
1567
|
+
print('Destroy the invalid local stack and run Infra Up again.', file=sys.stderr)
|
|
1568
|
+
raise SystemExit(1)
|
|
1569
|
+
PY
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1484
1572
|
check_immutable_migrations_applied() {
|
|
1485
1573
|
local migrations_dir="\${SUPABASE_PROJECT_DIR}/supabase/migrations"
|
|
1486
1574
|
local applied_versions
|
|
@@ -1515,8 +1603,14 @@ if [[ "\${AUTH_RUNTIME_MODE}" == "local" && "\${SUPABASE_LOCAL_ENABLED}" == "tru
|
|
|
1515
1603
|
echo "Local Supabase database checks:"
|
|
1516
1604
|
status_failed=0
|
|
1517
1605
|
|
|
1518
|
-
if
|
|
1519
|
-
|
|
1606
|
+
if ! require_expected_supabase_project_identity; then
|
|
1607
|
+
status_failed=1
|
|
1608
|
+
elif ! reject_supabase_project_id_override; then
|
|
1609
|
+
status_failed=1
|
|
1610
|
+
elif require_supabase_cli_capabilities; then
|
|
1611
|
+
if ! validate_supabase_project_identity; then
|
|
1612
|
+
status_failed=1
|
|
1613
|
+
elif supabase --workdir "\${SUPABASE_PROJECT_DIR}" status >/dev/null 2>&1; then
|
|
1520
1614
|
STATUS_ENV="$(supabase --workdir "\${SUPABASE_PROJECT_DIR}" status -o env)"
|
|
1521
1615
|
SUPABASE_DB_URL="$(read_env_value "\${STATUS_ENV}" DB_URL)"
|
|
1522
1616
|
if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
@@ -1587,7 +1681,7 @@ fi
|
|
|
1587
1681
|
`;
|
|
1588
1682
|
}
|
|
1589
1683
|
function getSupabaseLocalEnvScript(args) {
|
|
1590
|
-
const { supabaseLocalPorts, profileModel } = args;
|
|
1684
|
+
const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;
|
|
1591
1685
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1592
1686
|
const profileVerificationSql = profileModel.enabled
|
|
1593
1687
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1609,9 +1703,29 @@ fi
|
|
|
1609
1703
|
|
|
1610
1704
|
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
1611
1705
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1706
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1612
1707
|
APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
|
|
1613
1708
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1614
1709
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1710
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1711
|
+
|
|
1712
|
+
reject_supabase_project_id_override() {
|
|
1713
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1714
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1715
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1716
|
+
exit 1
|
|
1717
|
+
fi
|
|
1718
|
+
|
|
1719
|
+
unset SUPABASE_PROJECT_ID
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
require_expected_supabase_project_identity() {
|
|
1723
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1724
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1725
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1726
|
+
exit 1
|
|
1727
|
+
fi
|
|
1728
|
+
}
|
|
1615
1729
|
|
|
1616
1730
|
SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
|
|
1617
1731
|
SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
|
|
@@ -1657,6 +1771,11 @@ require_supabase_cli_capabilities() {
|
|
|
1657
1771
|
exit 1
|
|
1658
1772
|
fi
|
|
1659
1773
|
|
|
1774
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1775
|
+
echo "python3 is required to reconcile supabase/config.toml before local Supabase startup."
|
|
1776
|
+
exit 1
|
|
1777
|
+
fi
|
|
1778
|
+
|
|
1660
1779
|
local version
|
|
1661
1780
|
version="$(supabase --version | awk '{print $NF}')"
|
|
1662
1781
|
if ! version_ge "\${version}" "2.106.0"; then
|
|
@@ -1718,6 +1837,8 @@ run_checked_sql_file() {
|
|
|
1718
1837
|
psql "\${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "\${sql_file}"
|
|
1719
1838
|
}
|
|
1720
1839
|
|
|
1840
|
+
require_expected_supabase_project_identity
|
|
1841
|
+
reject_supabase_project_id_override
|
|
1721
1842
|
require_supabase_cli_capabilities
|
|
1722
1843
|
|
|
1723
1844
|
if [[ ! -d "\${SUPABASE_PROJECT_DIR}" ]]; then
|
|
@@ -1728,18 +1849,6 @@ fi
|
|
|
1728
1849
|
configure_supabase_local_ports() {
|
|
1729
1850
|
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1730
1851
|
|
|
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
1852
|
python3 - "\${config_file}" <<'PY'
|
|
1744
1853
|
import os
|
|
1745
1854
|
import re
|
|
@@ -1843,8 +1952,114 @@ if __name__ == "__main__":
|
|
|
1843
1952
|
PY
|
|
1844
1953
|
}
|
|
1845
1954
|
|
|
1955
|
+
write_supabase_project_identity_for_new_config() {
|
|
1956
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1957
|
+
|
|
1958
|
+
require_expected_supabase_project_identity
|
|
1959
|
+
|
|
1960
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1961
|
+
echo "python3 is required to write Supabase project_id in \${config_file}."
|
|
1962
|
+
exit 1
|
|
1963
|
+
fi
|
|
1964
|
+
|
|
1965
|
+
python3 - "\${config_file}" <<'PY'
|
|
1966
|
+
import os
|
|
1967
|
+
import re
|
|
1968
|
+
import sys
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
def write_top_level_project_id(config_path, expected):
|
|
1972
|
+
key_re = re.compile(r'^(\\s*)project_id\\s*=\\s*(.*?)(\\s*(#.*)?)?$')
|
|
1973
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
1974
|
+
|
|
1975
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1976
|
+
lines = f.readlines()
|
|
1977
|
+
|
|
1978
|
+
section_start = len(lines)
|
|
1979
|
+
for i, line in enumerate(lines):
|
|
1980
|
+
if section_re.match(line):
|
|
1981
|
+
section_start = i
|
|
1982
|
+
break
|
|
1983
|
+
|
|
1984
|
+
for i in range(section_start):
|
|
1985
|
+
match = key_re.match(lines[i])
|
|
1986
|
+
if not match:
|
|
1987
|
+
continue
|
|
1988
|
+
indent = match.group(1) or ''
|
|
1989
|
+
trailing = (match.group(3) or '').rstrip('\\n')
|
|
1990
|
+
lines[i] = f'{indent}project_id = "{expected}"{trailing}\\n'
|
|
1991
|
+
break
|
|
1992
|
+
else:
|
|
1993
|
+
insert_at = section_start
|
|
1994
|
+
lines.insert(insert_at, f'project_id = "{expected}"\\n')
|
|
1995
|
+
|
|
1996
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
1997
|
+
f.writelines(lines)
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
write_top_level_project_id(sys.argv[1], os.environ['EXPECTED_SUPABASE_PROJECT_ID'])
|
|
2001
|
+
PY
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
validate_supabase_project_identity() {
|
|
2005
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
2006
|
+
|
|
2007
|
+
require_expected_supabase_project_identity
|
|
2008
|
+
|
|
2009
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
2010
|
+
echo "Supabase project identity mismatch."
|
|
2011
|
+
printf 'Expected "%s", found missing.\n' "\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
2012
|
+
echo "The existing local Supabase project belongs to a different identity."
|
|
2013
|
+
echo "Destroy the invalid local stack and its project-owned resources, then run Infra Up again."
|
|
2014
|
+
exit 1
|
|
2015
|
+
fi
|
|
2016
|
+
|
|
2017
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
2018
|
+
echo "python3 is required to read Supabase project_id in \${config_file}."
|
|
2019
|
+
exit 1
|
|
2020
|
+
fi
|
|
2021
|
+
|
|
2022
|
+
python3 - "\${config_file}" <<'PY'
|
|
2023
|
+
import os
|
|
2024
|
+
import re
|
|
2025
|
+
import sys
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
def read_top_level_project_id(config_path):
|
|
2029
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
2030
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
2031
|
+
|
|
2032
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
2033
|
+
for line in f:
|
|
2034
|
+
if section_re.match(line):
|
|
2035
|
+
return None
|
|
2036
|
+
match = key_re.match(line)
|
|
2037
|
+
if not match:
|
|
2038
|
+
continue
|
|
2039
|
+
raw = match.group(1).strip()
|
|
2040
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
2041
|
+
return quoted.group(1) if quoted else raw
|
|
2042
|
+
|
|
2043
|
+
return None
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
2047
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
2048
|
+
|
|
2049
|
+
if actual == expected:
|
|
2050
|
+
raise SystemExit(0)
|
|
2051
|
+
|
|
2052
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
2053
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
2054
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
2055
|
+
print('The existing local Supabase project belongs to a different identity.', file=sys.stderr)
|
|
2056
|
+
print('Destroy the invalid local stack and its project-owned resources, then run Infra Up again.', file=sys.stderr)
|
|
2057
|
+
raise SystemExit(1)
|
|
2058
|
+
PY
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1846
2061
|
assert_supabase_local_ports_available() {
|
|
1847
|
-
local project_label="\${
|
|
2062
|
+
local project_label="\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
1848
2063
|
|
|
1849
2064
|
ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
|
|
1850
2065
|
import os
|
|
@@ -1922,8 +2137,10 @@ PY
|
|
|
1922
2137
|
if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
|
|
1923
2138
|
echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
|
|
1924
2139
|
supabase --workdir "\${SUPABASE_PROJECT_DIR}" init --yes >/dev/null
|
|
2140
|
+
write_supabase_project_identity_for_new_config
|
|
1925
2141
|
fi
|
|
1926
2142
|
|
|
2143
|
+
validate_supabase_project_identity
|
|
1927
2144
|
configure_supabase_local_ports
|
|
1928
2145
|
|
|
1929
2146
|
STATUS_ENV="$(
|
|
@@ -1995,6 +2212,7 @@ start_supabase_local_stack() {
|
|
|
1995
2212
|
}
|
|
1996
2213
|
|
|
1997
2214
|
if [[ -z "\${STATUS_ENV}" ]]; then
|
|
2215
|
+
validate_supabase_project_identity
|
|
1998
2216
|
assert_supabase_local_ports_available
|
|
1999
2217
|
echo "Supabase local stack not detected. Starting with 'supabase start'..."
|
|
2000
2218
|
start_supabase_local_stack
|
|
@@ -2008,6 +2226,7 @@ if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
|
2008
2226
|
fi
|
|
2009
2227
|
|
|
2010
2228
|
echo "Applying pending immutable Supabase migrations..."
|
|
2229
|
+
validate_supabase_project_identity
|
|
2011
2230
|
run_checked_command \
|
|
2012
2231
|
"Immutable Supabase migration application" \
|
|
2013
2232
|
"" \
|