@ankhorage/infra 4.1.2 → 4.1.4
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 +1 -1
- package/dist/adapters/minikube/base/index.js +17 -13
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/package.json +12 -13
- package/src/adapters/minikube/README.md +6 -3
- package/src/adapters/minikube/base/appImageBuild.test.ts +103 -14
- package/src/adapters/minikube/base/index.test.ts +6 -2
- package/src/adapters/minikube/base/index.ts +17 -13
- package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +1 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/infra",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.1.
|
|
4
|
+
"version": "4.1.4",
|
|
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": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"openapi"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ankhorage/contracts": "^8.0.
|
|
39
|
-
"@ankhorage/supabase-vault": "^0.2.
|
|
38
|
+
"@ankhorage/contracts": "^8.0.1",
|
|
39
|
+
"@ankhorage/supabase-vault": "^0.2.5"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"dist",
|
|
@@ -89,10 +89,9 @@
|
|
|
89
89
|
},
|
|
90
90
|
"scripts": {
|
|
91
91
|
"build": "rm -rf dist tsconfig.tsbuildinfo && bun x tsc -p tsconfig.json",
|
|
92
|
-
"changeset": "changeset",
|
|
93
|
-
"changeset:status": "changeset status --since=origin/main",
|
|
92
|
+
"changeset": "ankhorage-changeset",
|
|
93
|
+
"changeset:status": "ankhorage-changeset status --since=origin/main",
|
|
94
94
|
"docs": "paradox",
|
|
95
|
-
"knip": "ankhorage-knip",
|
|
96
95
|
"lint": "ankhorage-eslint . --max-warnings=0",
|
|
97
96
|
"lint:fix": "ankhorage-eslint . --fix --max-warnings=0",
|
|
98
97
|
"format": "ankhorage-prettier --write .",
|
|
@@ -103,16 +102,16 @@
|
|
|
103
102
|
"test:e2e:minikube": "ANKH_MINIKUBE_ISOLATION=1 bun test src/adapters/minikube/base/twoAppIsolation.integration.test.ts",
|
|
104
103
|
"test:e2e:supabase": "ANKH_MINIKUBE_SUPABASE_ISOLATION=1 bun test src/adapters/minikube/base/twoAppIsolation.integration.test.ts",
|
|
105
104
|
"typecheck": "bun x tsc --noEmit -p tsconfig.json",
|
|
106
|
-
"version-packages": "changeset version"
|
|
105
|
+
"version-packages": "ankhorage-changeset version",
|
|
106
|
+
"knip:check": "ankhorage-knip"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
|
-
"@ankhorage/ankh": "^0.8.
|
|
110
|
-
"@ankhorage/devtools": "^1.
|
|
111
|
-
"@ankhorage/paradox": "^0.1.
|
|
112
|
-
"@
|
|
113
|
-
"@types/bun": "^1.3.14",
|
|
109
|
+
"@ankhorage/ankh": "^0.8.4",
|
|
110
|
+
"@ankhorage/devtools": "^1.8.3",
|
|
111
|
+
"@ankhorage/paradox": "^0.1.24",
|
|
112
|
+
"@types/bun": "^1.4.0",
|
|
114
113
|
"@types/node": "^25.6.0",
|
|
115
114
|
"typescript": "^5.9.3"
|
|
116
115
|
},
|
|
117
|
-
"packageManager": "bun@1.
|
|
116
|
+
"packageManager": "bun@1.4.0"
|
|
118
117
|
}
|
|
@@ -73,9 +73,12 @@ official Supabase Kubernetes distribution.
|
|
|
73
73
|
|
|
74
74
|
- Default app image: `ankh/<slug>:dev`.
|
|
75
75
|
- By default `up.sh` triggers `build-app-image.sh` (`APP_BUILD_ENABLED=true`) before apply.
|
|
76
|
-
- `build-app-image.sh`
|
|
77
|
-
`
|
|
78
|
-
|
|
76
|
+
- `build-app-image.sh` requires the app's installed `node_modules/.bin/expo`, runs
|
|
77
|
+
`expo export --platform web --clear` through that pinned local CLI, then builds via
|
|
78
|
+
`app-image/Dockerfile`. Missing app dependencies fail with an explicit frozen-install
|
|
79
|
+
instruction; the script never resolves Expo through `bunx` or the network. Clearing the
|
|
80
|
+
Expo/Metro cache ensures rotated `EXPO_PUBLIC_*` values replace cached transforms before the
|
|
81
|
+
browser bundle and Docker image are created.
|
|
79
82
|
- `up.sh` syncs runtime image using `APP_IMAGE_SYNC_STRATEGY`:
|
|
80
83
|
- `docker-load` (default): loads existing local Docker image into the app profile.
|
|
81
84
|
- `minikube-build`: builds exported web artifacts directly into the profile image store.
|
|
@@ -25,7 +25,7 @@ test('re-exports rotated public environment values before building the app image
|
|
|
25
25
|
const buildScriptPath = path.join(scriptsRoot, 'build-app-image.sh');
|
|
26
26
|
const dockerfilePath = path.join(infraRoot, 'app-image', 'Dockerfile');
|
|
27
27
|
const metroCachePath = path.join(workspaceRoot, 'fake-metro-cache');
|
|
28
|
-
const
|
|
28
|
+
const expoArgsPath = path.join(workspaceRoot, 'expo-args');
|
|
29
29
|
const dockerArgsPath = path.join(workspaceRoot, 'docker-args');
|
|
30
30
|
const exportDir = path.join(appRoot, '.ankh', 'web-export');
|
|
31
31
|
const bundlePath = path.join(exportDir, '_expo', 'static', 'js', 'web', 'index-fixture.js');
|
|
@@ -49,14 +49,14 @@ test('re-exports rotated public environment values before building the app image
|
|
|
49
49
|
await writeFile(dockerfilePath, 'FROM scratch\n', 'utf8');
|
|
50
50
|
await writeFile(buildScriptPath, buildScript, 'utf8');
|
|
51
51
|
await chmod(buildScriptPath, 0o755);
|
|
52
|
-
await
|
|
52
|
+
await writeFakeExpo(path.join(appRoot, 'node_modules', '.bin', 'expo'));
|
|
53
53
|
await writeFakeDocker(path.join(fakeBin, 'docker'));
|
|
54
54
|
|
|
55
55
|
await writePublicEnvironment(appRoot, infraRoot, PUBLIC_KEY_A);
|
|
56
56
|
await runBuildScript({
|
|
57
57
|
appRoot,
|
|
58
58
|
buildScriptPath,
|
|
59
|
-
|
|
59
|
+
expoArgsPath,
|
|
60
60
|
dockerArgsPath,
|
|
61
61
|
fakeBin,
|
|
62
62
|
metroCachePath,
|
|
@@ -70,7 +70,7 @@ test('re-exports rotated public environment values before building the app image
|
|
|
70
70
|
await runBuildScript({
|
|
71
71
|
appRoot,
|
|
72
72
|
buildScriptPath,
|
|
73
|
-
|
|
73
|
+
expoArgsPath,
|
|
74
74
|
dockerArgsPath,
|
|
75
75
|
fakeBin,
|
|
76
76
|
metroCachePath,
|
|
@@ -80,8 +80,7 @@ test('re-exports rotated public environment values before building the app image
|
|
|
80
80
|
expect(secondBundle).toContain(PUBLIC_KEY_B);
|
|
81
81
|
expect(secondBundle).not.toContain(PUBLIC_KEY_A);
|
|
82
82
|
expect(secondBundle).not.toContain(PRIVATE_SERVICE_ROLE_KEY);
|
|
83
|
-
expect((await readFile(
|
|
84
|
-
'expo',
|
|
83
|
+
expect((await readFile(expoArgsPath, 'utf8')).trim().split('\n')).toEqual([
|
|
85
84
|
'export',
|
|
86
85
|
'--platform',
|
|
87
86
|
'web',
|
|
@@ -98,6 +97,64 @@ test('re-exports rotated public environment values before building the app image
|
|
|
98
97
|
}
|
|
99
98
|
});
|
|
100
99
|
|
|
100
|
+
test('fails before global or network Expo resolution when app dependencies are missing', async () => {
|
|
101
|
+
const workspaceRoot = await mkdtemp(path.join(tmpdir(), 'ankh-infra-app-image-missing-expo-'));
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const appRoot = path.join(workspaceRoot, 'apps', APP_SLUG);
|
|
105
|
+
const infraRoot = path.join(appRoot, 'infra', 'minikube');
|
|
106
|
+
const scriptsRoot = path.join(infraRoot, 'scripts');
|
|
107
|
+
const fakeBin = path.join(workspaceRoot, 'bin');
|
|
108
|
+
const buildScriptPath = path.join(scriptsRoot, 'build-app-image.sh');
|
|
109
|
+
const dockerfilePath = path.join(infraRoot, 'app-image', 'Dockerfile');
|
|
110
|
+
const fallbackMarkerPath = path.join(workspaceRoot, 'forbidden-expo-fallback');
|
|
111
|
+
const generated = generateInfrastructure(
|
|
112
|
+
{
|
|
113
|
+
deployment: { target: 'minikube', monitoring: false },
|
|
114
|
+
modules: [],
|
|
115
|
+
},
|
|
116
|
+
{ appManifest: createAppManifest(APP_SLUG) },
|
|
117
|
+
);
|
|
118
|
+
const buildScript = getGeneratedFile(
|
|
119
|
+
generated.files,
|
|
120
|
+
'infra/minikube/scripts/build-app-image.sh',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await mkdir(path.dirname(dockerfilePath), { recursive: true });
|
|
124
|
+
await mkdir(fakeBin, { recursive: true });
|
|
125
|
+
await mkdir(scriptsRoot, { recursive: true });
|
|
126
|
+
await writeFile(path.join(appRoot, 'package.json'), '{}\n', 'utf8');
|
|
127
|
+
await writeFile(dockerfilePath, 'FROM scratch\n', 'utf8');
|
|
128
|
+
await writeFile(buildScriptPath, buildScript, 'utf8');
|
|
129
|
+
await chmod(buildScriptPath, 0o755);
|
|
130
|
+
await writeFakeDocker(path.join(fakeBin, 'docker'));
|
|
131
|
+
await writeForbiddenExpoFallback(path.join(fakeBin, 'bunx'));
|
|
132
|
+
await writeForbiddenExpoFallback(path.join(fakeBin, 'expo'));
|
|
133
|
+
|
|
134
|
+
const result = await executeBuildScript({
|
|
135
|
+
appRoot,
|
|
136
|
+
buildScriptPath,
|
|
137
|
+
dockerArgsPath: path.join(workspaceRoot, 'docker-args'),
|
|
138
|
+
expoArgsPath: path.join(workspaceRoot, 'expo-args'),
|
|
139
|
+
fakeBin,
|
|
140
|
+
fallbackMarkerPath,
|
|
141
|
+
metroCachePath: path.join(workspaceRoot, 'fake-metro-cache'),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.exitCode).toBe(1);
|
|
145
|
+
expect(result.stderr).toContain(
|
|
146
|
+
`App-owned Expo CLI is missing: ${appRoot}/node_modules/.bin/expo`,
|
|
147
|
+
);
|
|
148
|
+
expect(result.stderr).toContain(
|
|
149
|
+
`Run 'bun install --frozen-lockfile' in ${appRoot} before building the app image.`,
|
|
150
|
+
);
|
|
151
|
+
expect(buildScript).not.toContain('bunx');
|
|
152
|
+
expect(await Bun.file(fallbackMarkerPath).exists()).toBe(false);
|
|
153
|
+
} finally {
|
|
154
|
+
await rm(workspaceRoot, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
101
158
|
async function writePublicEnvironment(
|
|
102
159
|
appRoot: string,
|
|
103
160
|
infraRoot: string,
|
|
@@ -122,19 +179,20 @@ SUPABASE_SERVICE_ROLE_KEY=${PRIVATE_SERVICE_ROLE_KEY}
|
|
|
122
179
|
);
|
|
123
180
|
}
|
|
124
181
|
|
|
125
|
-
async function
|
|
182
|
+
async function writeFakeExpo(filePath: string): Promise<void> {
|
|
183
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
126
184
|
await writeFile(
|
|
127
185
|
filePath,
|
|
128
186
|
`#!/usr/bin/env bash
|
|
129
187
|
set -euo pipefail
|
|
130
188
|
|
|
131
|
-
printf '%s\\n' "$@" > "\${
|
|
189
|
+
printf '%s\\n' "$@" > "\${FAKE_EXPO_ARGS_PATH:?}"
|
|
132
190
|
|
|
133
|
-
if [[ "\${1:-}" != "
|
|
191
|
+
if [[ "\${1:-}" != "export" ]]; then
|
|
134
192
|
echo "Expected expo export invocation." >&2
|
|
135
193
|
exit 64
|
|
136
194
|
fi
|
|
137
|
-
shift
|
|
195
|
+
shift
|
|
138
196
|
|
|
139
197
|
clear_cache=false
|
|
140
198
|
output_dir=""
|
|
@@ -208,20 +266,38 @@ fi
|
|
|
208
266
|
await chmod(filePath, 0o755);
|
|
209
267
|
}
|
|
210
268
|
|
|
211
|
-
async function
|
|
269
|
+
async function writeForbiddenExpoFallback(filePath: string): Promise<void> {
|
|
270
|
+
await writeFile(
|
|
271
|
+
filePath,
|
|
272
|
+
`#!/usr/bin/env bash
|
|
273
|
+
set -euo pipefail
|
|
274
|
+
|
|
275
|
+
: > "\${FAKE_FALLBACK_MARKER_PATH:?}"
|
|
276
|
+
exit 86
|
|
277
|
+
`,
|
|
278
|
+
'utf8',
|
|
279
|
+
);
|
|
280
|
+
await chmod(filePath, 0o755);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function executeBuildScript(args: {
|
|
212
284
|
appRoot: string;
|
|
213
285
|
buildScriptPath: string;
|
|
214
|
-
|
|
286
|
+
expoArgsPath: string;
|
|
215
287
|
dockerArgsPath: string;
|
|
216
288
|
fakeBin: string;
|
|
289
|
+
fallbackMarkerPath?: string;
|
|
217
290
|
metroCachePath: string;
|
|
218
|
-
}): Promise<
|
|
291
|
+
}): Promise<{ exitCode: number | null; stderr: string; stdout: string }> {
|
|
219
292
|
const child = spawn('bash', [args.buildScriptPath], {
|
|
220
293
|
cwd: args.appRoot,
|
|
221
294
|
env: {
|
|
222
295
|
...process.env,
|
|
223
|
-
FAKE_BUNX_ARGS_PATH: args.bunxArgsPath,
|
|
224
296
|
FAKE_DOCKER_ARGS_PATH: args.dockerArgsPath,
|
|
297
|
+
FAKE_EXPO_ARGS_PATH: args.expoArgsPath,
|
|
298
|
+
...(args.fallbackMarkerPath === undefined
|
|
299
|
+
? {}
|
|
300
|
+
: { FAKE_FALLBACK_MARKER_PATH: args.fallbackMarkerPath }),
|
|
225
301
|
FAKE_METRO_CACHE_PATH: args.metroCachePath,
|
|
226
302
|
PATH: `${args.fakeBin}:${process.env.PATH ?? ''}`,
|
|
227
303
|
},
|
|
@@ -237,6 +313,19 @@ async function runBuildScript(args: {
|
|
|
237
313
|
exitCodePromise,
|
|
238
314
|
]);
|
|
239
315
|
|
|
316
|
+
return { exitCode, stderr, stdout };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function runBuildScript(args: {
|
|
320
|
+
appRoot: string;
|
|
321
|
+
buildScriptPath: string;
|
|
322
|
+
expoArgsPath: string;
|
|
323
|
+
dockerArgsPath: string;
|
|
324
|
+
fakeBin: string;
|
|
325
|
+
metroCachePath: string;
|
|
326
|
+
}): Promise<void> {
|
|
327
|
+
const { exitCode, stderr, stdout } = await executeBuildScript(args);
|
|
328
|
+
|
|
240
329
|
if (exitCode !== 0) {
|
|
241
330
|
throw new Error(
|
|
242
331
|
`Generated build script exited with ${String(exitCode)}. stdout=${stdout} stderr=${stderr}`,
|
|
@@ -398,12 +398,16 @@ describe('generateMinikubeBaseArtifacts app-owned cluster model', () => {
|
|
|
398
398
|
),
|
|
399
399
|
);
|
|
400
400
|
expect(upScript).not.toContain('SUPABASE_SERVICE_ROLE_KEY" "${SUPABASE_SERVICE_ROLE_KEY}"');
|
|
401
|
+
expect(buildScript).toContain('EXPO_CLI="${APP_SOURCE_DIR}/node_modules/.bin/expo"');
|
|
401
402
|
expect(buildScript).toContain(
|
|
402
|
-
'
|
|
403
|
+
'"${EXPO_CLI}" export --platform web --clear --output-dir "${APP_WEB_EXPORT_DIR}"',
|
|
403
404
|
);
|
|
405
|
+
expect(buildScript).not.toContain('bunx');
|
|
404
406
|
expect(readme).toContain(
|
|
405
|
-
'`build-app-image.sh`
|
|
407
|
+
'`build-app-image.sh` requires the app-installed `node_modules/.bin/expo` and never resolves',
|
|
406
408
|
);
|
|
409
|
+
expect(readme).toContain('Expo globally or through the network.');
|
|
410
|
+
expect(readme).toContain('It clears the Expo/Metro bundler cache before every web');
|
|
407
411
|
});
|
|
408
412
|
|
|
409
413
|
test('starts Supabase schema owners before app migrations and profile reconciliation', () => {
|
|
@@ -451,10 +451,12 @@ ${appRuntimeEnabled ? `- App: \`http://127.0.0.1:${supabaseHostPorts.app}\`\n` :
|
|
|
451
451
|
|
|
452
452
|
## App Image Export
|
|
453
453
|
|
|
454
|
-
\`build-app-image.sh\`
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
454
|
+
\`build-app-image.sh\` requires the app-installed \`node_modules/.bin/expo\` and never resolves
|
|
455
|
+
Expo globally or through the network. It clears the Expo/Metro bundler cache before every web
|
|
456
|
+
export. Expo inlines \`EXPO_PUBLIC_*\` values into the browser bundle, so cache invalidation
|
|
457
|
+
ensures a rotated browser-safe Supabase URL or anon key replaces the previous value before the
|
|
458
|
+
Docker image is built. Privileged Supabase credentials are not written to the app's public env
|
|
459
|
+
file.
|
|
458
460
|
|
|
459
461
|
## Runtime Conventions
|
|
460
462
|
|
|
@@ -2958,29 +2960,31 @@ if [[ "\${APP_BUILD_ENABLED}" != "true" ]]; then
|
|
|
2958
2960
|
exit 0
|
|
2959
2961
|
fi
|
|
2960
2962
|
|
|
2961
|
-
if !
|
|
2962
|
-
echo "
|
|
2963
|
+
if [[ ! -f "\${APP_SOURCE_DIR}/package.json" ]]; then
|
|
2964
|
+
echo "APP_SOURCE_DIR does not look like an app root: \${APP_SOURCE_DIR}"
|
|
2963
2965
|
exit 1
|
|
2964
2966
|
fi
|
|
2965
2967
|
|
|
2966
|
-
if !
|
|
2967
|
-
echo "
|
|
2968
|
+
if [[ ! -f "\${DOCKERFILE_PATH}" ]]; then
|
|
2969
|
+
echo "Dockerfile for app image build is missing: \${DOCKERFILE_PATH}"
|
|
2968
2970
|
exit 1
|
|
2969
2971
|
fi
|
|
2970
2972
|
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
+
EXPO_CLI="\${APP_SOURCE_DIR}/node_modules/.bin/expo"
|
|
2974
|
+
if [[ ! -x "\${EXPO_CLI}" ]]; then
|
|
2975
|
+
echo "App-owned Expo CLI is missing: \${EXPO_CLI}" >&2
|
|
2976
|
+
echo "Run 'bun install --frozen-lockfile' in \${APP_SOURCE_DIR} before building the app image." >&2
|
|
2973
2977
|
exit 1
|
|
2974
2978
|
fi
|
|
2975
2979
|
|
|
2976
|
-
if
|
|
2977
|
-
echo "
|
|
2980
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
2981
|
+
echo "docker is required to build app image."
|
|
2978
2982
|
exit 1
|
|
2979
2983
|
fi
|
|
2980
2984
|
|
|
2981
2985
|
(
|
|
2982
2986
|
cd "\${APP_SOURCE_DIR}"
|
|
2983
|
-
|
|
2987
|
+
"\${EXPO_CLI}" export --platform web --clear --output-dir "\${APP_WEB_EXPORT_DIR}"
|
|
2984
2988
|
)
|
|
2985
2989
|
|
|
2986
2990
|
if [[ "\${APP_WEB_EXPORT_DIR}" = /* ]]; then
|
|
@@ -22,8 +22,7 @@ const DIAGNOSTIC_OUTPUT_LIMIT = 20_000;
|
|
|
22
22
|
const APP_ONLY_TEST_PROFILES = ['ankh-isolation-a', 'ankh-isolation-b'] as const;
|
|
23
23
|
const SUPABASE_TEST_PROFILES = ['ankh-isolation-supa-a', 'ankh-isolation-supa-b'] as const;
|
|
24
24
|
type TestOwnedMinikubeProfile =
|
|
25
|
-
| (typeof
|
|
26
|
-
| (typeof SUPABASE_TEST_PROFILES)[number];
|
|
25
|
+
(typeof APP_ONLY_TEST_PROFILES)[number] | (typeof SUPABASE_TEST_PROFILES)[number];
|
|
27
26
|
|
|
28
27
|
describe('generated Minikube two-app isolation', () => {
|
|
29
28
|
isolationTest(
|