From 14e0ba53409d2f6a179f5c7a3dcff91260a2960b Mon Sep 17 00:00:00 2001 From: Vojtech Dolezal Date: Thu, 23 Jul 2026 18:35:00 +0200 Subject: [PATCH 1/4] fix(zane): sync current envs, optional n1 --- .env.docker | 3 +- README.md | 2 +- apps/herbatika/package.json | 1 + .../NEW_ENGINE_CTL_ARCHITECTURE.md | 9 +- apps/new-engine-ctl/config/stack-inputs.yaml | 42 +- .../new-engine-ctl/config/stack-manifest.yaml | 27 +- .../__tests__/bootstrap-zane-project.test.ts | 238 ++++++++++ .../src/__tests__/local-env.test.ts | 74 ++++ .../src/__tests__/scope-preview.test.ts | 82 +++- .../src/contracts/stack-inputs.ts | 14 +- .../src/contracts/stack-manifest.ts | 31 +- .../orchestration/bootstrap/zane-project.ts | 417 ++++++++++++++---- apps/new-engine-ctl/src/orchestration/plan.ts | 12 +- .../src/orchestration/prepare.ts | 4 +- .../new-engine-ctl/src/orchestration/scope.ts | 10 +- docker-compose.prod.yaml | 1 + docker-compose.yaml | 4 +- docker/development/herbatika/Dockerfile | 31 ++ scripts/dev/run-zane-preview-lane.sh | 2 +- scripts/dev/setup-zane-project.sh | 27 +- 20 files changed, 874 insertions(+), 157 deletions(-) create mode 100644 apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts create mode 100644 apps/new-engine-ctl/src/__tests__/local-env.test.ts diff --git a/.env.docker b/.env.docker index 232593871b..5a220e5345 100644 --- a/.env.docker +++ b/.env.docker @@ -32,11 +32,10 @@ DC_HERBATIKA_PUBLIC_PORT=3001 DC_HERBATIKA_NEXT_PUBLIC_STOREFRONT_AUTH_MODE=session_proxy DC_HERBATIKA_MEDUSA_BACKEND_URL_INTERNAL=http://medusa-be:9000 DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000 -DC_HERBATIKA_MEILISEARCH_HOST=http://medusa-meilisearch:7700 DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES=sk DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY= +DC_HERBATIKA_NEXT_PUBLIC_PPL_WIDGET_API_KEY= DC_HERBATIKA_NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:8083 -DC_HERBATIKA_MEILISEARCH_SEARCH_API_KEY=MEILISEARCH_SEARCH_READONLY_KEY_FOR_FRONTEND DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= # Medusa BE container: app runtime diff --git a/README.md b/README.md index 975b18b66f..f189d84092 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ * `DC_ZANE_OPERATOR_ZANE_USERNAME` / `DC_ZANE_OPERATOR_ZANE_PASSWORD` are the login credentials for that ZaneOps instance; `zane-operator` uses session + CSRF login upstream, not a direct Zane token * create one canonical Zane project and note its slug; local CI-style deploy tests use that slug as `ZANE_PROJECT_SLUG` * each Zane project gets a protected `production` environment by default; preview clones in this repo always use that environment as the base - * service names in that Zane project must match `apps/new-engine-ctl/config/stack-manifest.yaml` currently: `medusa-db`, `medusa-valkey`, `medusa-minio`, `medusa-meilisearch`, `medusa-be`, `payload`, `n1` + * service names in that Zane project must match `apps/new-engine-ctl/config/stack-manifest.yaml` currently: `medusa-db`, `medusa-valkey`, `medusa-minio`, `medusa-meilisearch`, `medusa-be`, `payload`, `herbatika`; `n1` remains an explicitly selectable reference storefront and is excluded from automatic CI scope * preview environments are derived in CI script space as `pr-` by default * preview teardown is explicit in this repo's CI flow; do not rely on built-in Zane preview auto-teardown for these cloned environments diff --git a/apps/herbatika/package.json b/apps/herbatika/package.json index 46746dac8f..e63b92aa1e 100644 --- a/apps/herbatika/package.json +++ b/apps/herbatika/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "next dev --turbopack -p 3001", + "start:ci-dev": "next dev --turbopack --hostname 0.0.0.0 --port 3000", "build": "next build --turbopack", "start": "next start", "lint": "biome check", diff --git a/apps/new-engine-ctl/NEW_ENGINE_CTL_ARCHITECTURE.md b/apps/new-engine-ctl/NEW_ENGINE_CTL_ARCHITECTURE.md index 6e894f2094..0d544cd6b8 100644 --- a/apps/new-engine-ctl/NEW_ENGINE_CTL_ARCHITECTURE.md +++ b/apps/new-engine-ctl/NEW_ENGINE_CTL_ARCHITECTURE.md @@ -118,6 +118,9 @@ Phase intent: - `prepare` is for shared-resource prerequisites and input validation only. - preview `prepare` owns baseline-aware shared-resource ensure decisions. It must not rely only on the originally requested service set when a baseline replay will expand deploy scope to the full preview clone set. - runtime-provider execution belongs in deploy orchestration after the provider source service is deployed and healthy, except that consumer-only deploys may reuse already-persisted contract-owned provider outputs from healthy target deployments when the source service is not in the current plan. +- project bootstrap/sync must not materialize or clean service envs owned by + runtime providers; empty local provider-output placeholders are inputs for + local reconciliation, not empty upstream desired values. - deploy-to-verify/render runtime-provider handoff is generic: - deploy emits `runtime_provider_outputs_json` and `runtime_provider_output_keys_csv` - verify/render consume `--runtime-provider-outputs-json` @@ -133,7 +136,11 @@ Phase intent: - preview route identity is repo-owned. When a preview environment is cloned or reused, authenticated URL reconciliation, stale cloned/default URL removal, preview-excluded service cleanup, and preview service-spec convergence belong in `zane-operator`, while deploy/baseline policy stays in `apps/new-engine-ctl`. - preview shared-env reconciliation is also repo-owned and typed: preview deploy syncs the existing shared host/DB keys from the preview environment topology plus prepared DB credentials before service deploy stages consume them. - `service_reconciliation` should stay lean: source-sync behavior is the default for preview-cloned services, and the YAML should only encode non-default policy such as lane-specific build-stage targets. -- Current lane-specific builder-stage policy in `service_reconciliation`: `medusa-be`, `payload`, and `n1` use `ci-dev` for preview and `prod` for main. +- Current lane-specific builder-stage policy in `service_reconciliation`: `medusa-be`, `payload`, `herbatika`, and optional `n1` use `ci-dev` for preview and `prod` for main. +- `ci.enabled_by_default: false` keeps optional deployable services such as + `n1` out of affected-service scope and ordinary preview baselines while + preserving explicit `--services-csv n1` deployment and provider + reconciliation. - workflow YAML may use workflow-safe concurrency groups to cancel superseded runs, but workflow cancellation complements rather than replaces lane-owned deployment adoption/cancel rules. - Preview runtime address policy has two scopes: operator-side provisioners must use preview-scoped global/private identities, while shared/service env values consumed by services inside the preview environment may keep local private aliases from `preview_runtime_reconciliation` when those consumers run on the same preview environment network. - main deploys and redeploy-only preview runs should not reprovision unrelated runtime-provider outputs just because the provider source service is healthy; when required persisted env values already exist on in-scope consumers, CTL may reuse them instead of forcing a fresh provider run. diff --git a/apps/new-engine-ctl/config/stack-inputs.yaml b/apps/new-engine-ctl/config/stack-inputs.yaml index cad71ace65..9cb182a98d 100644 --- a/apps/new-engine-ctl/config/stack-inputs.yaml +++ b/apps/new-engine-ctl/config/stack-inputs.yaml @@ -231,8 +231,6 @@ runtime_providers: target_envs: - service_id: n1 env_var: NEXT_PUBLIC_MEILISEARCH_API_KEY - - service_id: herbatika - env_var: MEILISEARCH_SEARCH_API_KEY policy: kind: meilisearch_key uid: 4b7f7f7e-8798-4b3f-8e73-c0f76f8b35d6 @@ -264,6 +262,8 @@ runtime_providers: target_envs: - service_id: n1 env_var: NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY + - service_id: herbatika + env_var: NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY policy: kind: medusa_publishable_key title: Storefront Publishable Key @@ -282,11 +282,17 @@ local_env_aliases: env_var: NEXT_PUBLIC_MEILISEARCH_API_KEY local_env_var: DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEY - - provider_id: meili_api_credentials + - provider_id: medusa_publishable_key + output_id: frontend_key + service_id: n1 + env_var: NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY + local_env_var: DC_N1_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY + + - provider_id: medusa_publishable_key output_id: frontend_key service_id: herbatika - env_var: MEILISEARCH_SEARCH_API_KEY - local_env_var: DC_HERBATIKA_MEILISEARCH_SEARCH_API_KEY + env_var: NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY + local_env_var: DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY preview_runtime_reconciliation: shared_env: @@ -335,7 +341,7 @@ preview_runtime_reconciliation: env_var: STORE_CORS source: kind: service_public_origin - service_id: n1 + service_id: herbatika - service_id: medusa-be env_var: ADMIN_CORS @@ -404,30 +410,24 @@ preview_runtime_reconciliation: service_id: medusa-minio port: 9004 - - service_id: n1 + - service_id: herbatika env_var: MEDUSA_BACKEND_URL_INTERNAL source: kind: service_internal_origin service_id: medusa-be port: 9000 - - service_id: n1 + - service_id: herbatika env_var: NEXT_PUBLIC_MEDUSA_BACKEND_URL source: kind: service_public_origin service_id: medusa-be - - service_id: n1 - env_var: NEXT_PUBLIC_MEILISEARCH_URL - source: - kind: service_public_origin - service_id: medusa-meilisearch - - - service_id: n1 - env_var: NEXT_PUBLIC_SITE_URL + - service_id: herbatika + env_var: NEXT_PUBLIC_PAYLOAD_BASE_URL source: kind: service_public_origin - service_id: n1 + service_id: payload preview_verification: forbidden_service_env: @@ -541,7 +541,7 @@ preview_verification: - ZANE_USERNAME - ZANE_PASSWORD - - service_id: n1 + - service_id: herbatika env_keys: - API_AUTH_TOKEN - PGHOST @@ -891,6 +891,12 @@ service_reconciliation: preview: ci-dev main: prod + - service_id: herbatika + builder: + build_stage_target_by_lane: + preview: ci-dev + main: prod + - service_id: n1 builder: build_stage_target_by_lane: diff --git a/apps/new-engine-ctl/config/stack-manifest.yaml b/apps/new-engine-ctl/config/stack-manifest.yaml index 6884de0c37..393b9186f3 100644 --- a/apps/new-engine-ctl/config/stack-manifest.yaml +++ b/apps/new-engine-ctl/config/stack-manifest.yaml @@ -23,15 +23,15 @@ # deploy_lanes: [preview, main] # deploy_stage: 20 # service_dependencies: [medusa-db, medusa-valkey, medusa-minio, medusa-meilisearch] -# - id: n1 -# compose_service: n1 +# - id: herbatika +# compose_service: herbatika # local: # phase: frontend # ci: # deployable: true -# affected_path_globs: ["docker/development/n1/**"] +# affected_path_globs: ["docker/development/herbatika/**"] # zane: -# service_slug: n1 +# service_slug: herbatika # deploy_lanes: [preview, main] # deploy_stage: 25 # service_dependencies: [medusa-be] @@ -49,7 +49,7 @@ ci: - ".dockerignore" service_ids: - "medusa-be" - - "n1" + - "herbatika" - "payload" - "zane-operator" - "medusa-db" @@ -65,7 +65,7 @@ ci: - "tsconfig.json" service_ids: - "medusa-be" - - "n1" + - "herbatika" - "payload" services: @@ -182,7 +182,10 @@ services: # is the active storefront target. enabled_by_default: false ci: + # N1 remains explicitly deployable for compatibility checks, but normal + # affected-service scoping and preview baselines follow Herbatika. deployable: true + enabled_by_default: false affected_path_globs: - "docker/development/n1/**" zane: @@ -199,7 +202,17 @@ services: local: phase: "frontend" ci: - deployable: false + deployable: true + affected_path_globs: + - "apps/herbatika/**" + - "docker/development/herbatika/**" + - "libs/storefront-data/**" + - "libs/ui/**" + zane: + service_slug: "herbatika" + deploy_lanes: ["preview", "main"] + deploy_stage: 25 + service_dependencies: ["medusa-be"] - id: "zane-operator" compose_service: "zane-operator" diff --git a/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts b/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts new file mode 100644 index 0000000000..4154c94ae4 --- /dev/null +++ b/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts @@ -0,0 +1,238 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +import { afterEach, expect, test, vi } from "vitest" +import { parse } from "yaml" +import { executeBootstrapZaneProjectPlan } from "../orchestration/bootstrap/zane-project.js" + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../..") +const stackManifestPath = join( + repoRoot, + "apps/new-engine-ctl/config/stack-manifest.yaml" +) +const stackInputsPath = join( + repoRoot, + "apps/new-engine-ctl/config/stack-inputs.yaml" +) + +const serviceSlugs = [ + "medusa-db", + "medusa-valkey", + "medusa-minio", + "medusa-meilisearch", + "medusa-be", + "payload", + "herbatika", + "zane-operator", +] +afterEach(() => { + vi.unstubAllEnvs() +}) + +test("project sync manages Herbatika and current Medusa runtime envs", async () => { + vi.stubEnv("DC_ZANE_OPERATOR_ZANE_USERNAME", "admin") + vi.stubEnv("DC_ZANE_OPERATOR_ZANE_PASSWORD", "password") + vi.stubEnv("DC_ZANE_OPERATOR_API_AUTH_TOKEN", "operator-token") + vi.stubEnv("DC_ZANE_OPERATOR_PGPASSWORD", "operator-db-password") + vi.stubEnv( + "DC_ZANE_OPERATOR_DB_PREVIEW_APP_PASSWORD_SECRET", + "preview-password-secret" + ) + vi.stubEnv("DC_STOREFRONT_URL", "https://storefront.example.com") + vi.stubEnv("DC_STORE_NAME", "Herbatika") + vi.stubEnv("DC_FEATURE_PAYMENT_QR_ENABLED", "1") + vi.stubEnv("DC_HERBATICA_REVIEWS_XML_PATH", "https://example.com/reviews.xml") + vi.stubEnv("DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "") + vi.stubEnv("DC_MEDUSA_BE_RESEND_FROM_EMAIL", "") + vi.stubEnv("DC_RESEND_FROM_EMAIL", "noreply@example.com") + + const temporaryDirectory = await mkdtemp( + join(tmpdir(), "new-engine-zane-project-") + ) + const inspectJsonPath = join(temporaryDirectory, "inspect.json") + + await writeFile( + inspectJsonPath, + JSON.stringify({ + project_slug: "test-engine", + environment_name: "production", + project_exists: true, + environment_exists: true, + settings: { + root_domain: "example.com", + app_domain: "zane.example.com", + }, + shared_variables: [], + services: serviceSlugs.map((serviceSlug) => ({ + service_slug: serviceSlug, + exists: true, + details: { + id: serviceSlug, + slug: serviceSlug, + type: "git", + network_alias: `zn-${serviceSlug}`, + global_network_alias: + serviceSlug === "medusa-db" ? "zn-medusa-db.global" : null, + }, + })), + }), + "utf8" + ) + + try { + const plan = await executeBootstrapZaneProjectPlan({ + projectSlug: "test-engine", + projectDescription: "Test project", + environmentName: "production", + inspectJsonPath, + repositoryUrl: "https://github.com/example/new-engine.git", + branchName: "main", + publicDomain: "example.com", + publicUrlAffix: "-zane", + stackManifestPath, + stackInputsPath, + phase: "env", + }) + + expect(plan.status).toBe("ready") + expect(plan.services.map((service) => service.service_id)).toEqual( + serviceSlugs + ) + expect(plan.services.some((service) => service.service_id === "n1")).toBe( + false + ) + + const inspectWithN1JsonPath = join( + temporaryDirectory, + "inspect-with-n1.json" + ) + const inspectWithN1 = JSON.parse( + await readFile(inspectJsonPath, "utf8") + ) as { + services: Record[] + } + inspectWithN1.services.push({ + service_slug: "n1", + exists: true, + details: { + id: "n1", + slug: "n1", + type: "git", + network_alias: "zn-n1", + global_network_alias: null, + }, + }) + await writeFile( + inspectWithN1JsonPath, + JSON.stringify(inspectWithN1), + "utf8" + ) + + const planWithN1 = await executeBootstrapZaneProjectPlan({ + projectSlug: "test-engine", + projectDescription: "Test project", + environmentName: "production", + inspectJsonPath: inspectWithN1JsonPath, + repositoryUrl: "https://github.com/example/new-engine.git", + branchName: "main", + publicDomain: "example.com", + publicUrlAffix: "-zane", + stackManifestPath, + stackInputsPath, + phase: "env", + }) + const n1 = planWithN1.services.find( + (service) => service.service_id === "n1" + ) + expect(n1).toMatchObject({ + service_slug: "n1", + dockerfile_path: "./docker/development/n1/Dockerfile", + }) + expect(n1?.desired_env).not.toHaveProperty( + "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY" + ) + expect(n1?.desired_env).not.toHaveProperty( + "NEXT_PUBLIC_MEILISEARCH_API_KEY" + ) + expect(n1?.cleanup_env_keys).not.toEqual( + expect.arrayContaining([ + "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", + "NEXT_PUBLIC_MEILISEARCH_API_KEY", + ]) + ) + + const medusa = plan.services.find( + (service) => service.service_id === "medusa-be" + ) + expect(medusa?.desired_env).toMatchObject({ + STOREFRONT_URL: "https://storefront.example.com", + STORE_NAME: "Herbatika", + FEATURE_PAYMENT_QR_ENABLED: "1", + HERBATICA_REVIEWS_XML_PATH: "https://example.com/reviews.xml", + RESEND_FROM_EMAIL: "noreply@example.com", + }) + expect(medusa?.desired_env).toHaveProperty( + "WORKFLOW_QUEUE_RUNNER_BATCH_SIZE" + ) + expect(medusa?.desired_env).toHaveProperty("SENTRY_TRACES_SAMPLE_RATE") + expect(medusa?.desired_env).not.toHaveProperty("MEILISEARCH_API_KEY") + + const herbatika = plan.services.find( + (service) => service.service_id === "herbatika" + ) + expect(herbatika?.desired_env).toMatchObject({ + NEXT_PUBLIC_PPL_WIDGET_API_KEY: "", + }) + expect(herbatika?.desired_env).not.toHaveProperty( + "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY" + ) + expect(herbatika?.desired_env).not.toHaveProperty("MEILISEARCH_HOST") + expect(herbatika?.desired_env).not.toHaveProperty( + "MEILISEARCH_SEARCH_API_KEY" + ) + expect(herbatika?.cleanup_env_keys).toEqual( + expect.arrayContaining([ + "MEILISEARCH_HOST", + "MEILISEARCH_SEARCH_API_KEY", + "MEILISEARCH_PRODUCTS_INDEX", + "MEILISEARCH_CATEGORIES_INDEX", + "MEILISEARCH_PRODUCERS_INDEX", + ]) + ) + + const compose = parse( + await readFile(join(repoRoot, "docker-compose.yaml"), "utf8"), + { merge: true } + ) as { + services: Record }> + } + const composeMedusaEnv = compose.services["medusa-be"]?.environment + const composeHerbatikaEnv = compose.services.herbatika?.environment + if (!(composeMedusaEnv && composeHerbatikaEnv)) { + throw new Error("Compose storefront/backend environments are missing.") + } + + const missingMedusaEnvKeys = Object.keys(composeMedusaEnv).filter( + (key) => + !( + ["NODE_ENV", "LEGACY_DATABASE_URL", "MEILISEARCH_API_KEY"].includes( + key + ) || key in (medusa?.desired_env ?? {}) + ) + ) + const missingHerbatikaEnvKeys = Object.keys(composeHerbatikaEnv).filter( + (key) => + !( + ["NODE_ENV", "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY"].includes(key) || + key in (herbatika?.desired_env ?? {}) + ) + ) + + expect(missingMedusaEnvKeys).toEqual([]) + expect(missingHerbatikaEnvKeys).toEqual([]) + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }) + } +}) diff --git a/apps/new-engine-ctl/src/__tests__/local-env.test.ts b/apps/new-engine-ctl/src/__tests__/local-env.test.ts new file mode 100644 index 0000000000..367ff16b91 --- /dev/null +++ b/apps/new-engine-ctl/src/__tests__/local-env.test.ts @@ -0,0 +1,74 @@ +import { dirname, join, resolve } from "node:path" +import { fileURLToPath } from "node:url" + +import { expect, test } from "vitest" +import { listLocalRuntimeProviderOutputAliases } from "../contracts/stack-inputs.js" +import { loadDeployContracts } from "../orchestration/deploy-inputs.js" +import { executeLocalEnvRuntimeProviderOutputTargets } from "../orchestration/local-env.js" + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../..") +const stackManifestPath = join( + repoRoot, + "apps/new-engine-ctl/config/stack-manifest.yaml" +) +const stackInputsPath = join( + repoRoot, + "apps/new-engine-ctl/config/stack-inputs.yaml" +) + +test("local provider aliases include only selected configured consumers", async () => { + const result = await executeLocalEnvRuntimeProviderOutputTargets({ + providerId: "meili_api_credentials", + outputId: "frontend_key", + serviceIdsCsv: "herbatika,n1", + format: "json", + stackInputsPath, + }) + + expect(result.targets).toEqual([ + { + service_id: "n1", + env_var: "NEXT_PUBLIC_MEILISEARCH_API_KEY", + local_env_var: "DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEY", + }, + ]) +}) + +test("local provider aliases are empty for selected non-consumers", async () => { + const result = await executeLocalEnvRuntimeProviderOutputTargets({ + providerId: "meili_api_credentials", + outputId: "frontend_key", + serviceIdsCsv: "herbatika", + format: "json", + stackInputsPath, + }) + + expect(result.targets).toEqual([]) +}) + +test("local provider aliases still reject missing aliases for real consumers", async () => { + const { stackInputs } = await loadDeployContracts( + stackManifestPath, + stackInputsPath + ) + stackInputs.local_env_aliases.runtime_provider_outputs = + stackInputs.local_env_aliases.runtime_provider_outputs.filter( + (alias) => + !( + alias.provider_id === "meili_api_credentials" && + alias.output_id === "frontend_key" && + alias.service_id === "n1" + ) + ) + + expect(() => + listLocalRuntimeProviderOutputAliases( + stackInputs, + "meili_api_credentials", + "frontend_key", + ["n1"] + ) + ).toThrow( + "Missing local env alias for meili_api_credentials.frontend_key consumer service(s): n1." + ) +}) diff --git a/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts b/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts index 3ff656d021..d102a27116 100644 --- a/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts +++ b/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts @@ -2,7 +2,10 @@ import { delimiter, dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" import { expect, test } from "vitest" +import { listLaneServiceIds } from "../contracts/stack-manifest.js" +import { loadDeployContracts } from "../orchestration/deploy-inputs.js" import { executePlan } from "../orchestration/plan.js" +import { collectConfiguredRuntimeProviderNeeds } from "../orchestration/runtime-provider-orchestration.js" import { executeScope, withWorkspaceBinPath } from "../orchestration/scope.js" const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../../..") @@ -45,7 +48,7 @@ test("scope removes duplicate path casing when prefixing workspace bin", () => { test("preview scope prepares DB credentials for first baseline replay", async () => { const result = await executeScope({ lane: "preview", - servicesCsv: "n1", + servicesCsv: "herbatika", headSha: "HEAD", previewBaselineComplete: false, stackManifestPath, @@ -53,7 +56,7 @@ test("preview scope prepares DB credentials for first baseline replay", async () nxIsolatePlugins: true, }) - expect(result.services_csv).toBe("n1") + expect(result.services_csv).toBe("herbatika") expect(result.should_prepare).toBe(true) expect(result.requires_preview_db).toBe(true) expect(result.preview_db_service_ids).toBe("medusa-be,payload") @@ -62,7 +65,7 @@ test("preview scope prepares DB credentials for first baseline replay", async () test("preview scope skips prepare for non-DB services after baseline is complete", async () => { const result = await executeScope({ lane: "preview", - servicesCsv: "n1", + servicesCsv: "herbatika", headSha: "HEAD", previewBaselineComplete: true, stackManifestPath, @@ -70,10 +73,81 @@ test("preview scope skips prepare for non-DB services after baseline is complete nxIsolatePlugins: true, }) - expect(result.services_csv).toBe("n1") + expect(result.services_csv).toBe("herbatika") expect(result.should_prepare).toBe(false) expect(result.requires_preview_db).toBe(false) expect(result.preview_db_service_ids).toBe("") + + const plan = await executePlan({ + lane: "preview", + servicesCsv: result.services_csv, + prNumber: 123, + outputJson: undefined, + stackManifestPath, + previewEnvPrefix: "pr-", + }) + expect(plan.preview_cloned_service_ids_csv.split(",")).not.toContain("n1") +}) + +test("N1 is explicitly selectable but excluded from default CI scope", async () => { + const contracts = await loadDeployContracts( + stackManifestPath, + stackInputsPath + ) + + expect(listLaneServiceIds(contracts.manifest, "main")).toContain("n1") + expect(listLaneServiceIds(contracts.manifest, "main", true)).not.toContain( + "n1" + ) + + const result = await executeScope({ + lane: "main", + servicesCsv: "n1", + headSha: "HEAD", + previewBaselineComplete: true, + stackManifestPath, + stackInputsPath, + nxIsolatePlugins: true, + }) + + expect(result.services_csv).toBe("n1") +}) + +test("explicit N1 preview selection includes N1 and its provider outputs", async () => { + const contracts = await loadDeployContracts( + stackManifestPath, + stackInputsPath + ) + const plan = await executePlan({ + lane: "preview", + servicesCsv: "n1", + prNumber: 123, + outputJson: undefined, + stackManifestPath, + previewEnvPrefix: "pr-", + }) + const needs = collectConfiguredRuntimeProviderNeeds({ + lane: "preview", + manifest: contracts.manifest, + stackInputs: contracts.stackInputs, + services: plan.deploy_services, + meiliApiCredentialsProviderId: "meili_api_credentials", + }) + + expect(plan.deploy_services_csv).toBe("n1") + expect(plan.preview_cloned_service_ids_csv.split(",")).toContain("n1") + expect(needs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + providerId: "meili_api_credentials", + outputConsumerIds: { frontend_key: ["n1"] }, + }), + expect.objectContaining({ + providerId: "medusa_publishable_key", + outputConsumerIds: { frontend_key: ["n1"] }, + }), + ]) + ) }) test("preview scope rejects explicit services excluded from preview cloning", async () => { diff --git a/apps/new-engine-ctl/src/contracts/stack-inputs.ts b/apps/new-engine-ctl/src/contracts/stack-inputs.ts index 7a939238ee..9f24f4ae11 100644 --- a/apps/new-engine-ctl/src/contracts/stack-inputs.ts +++ b/apps/new-engine-ctl/src/contracts/stack-inputs.ts @@ -508,13 +508,19 @@ export function listLocalRuntimeProviderOutputAliases( } if (serviceIds.length > 0) { + const targetServiceIds = new Set( + listRuntimeProviderOutputTargets(inputs, providerId, outputId).map( + (target) => target.service_id + ) + ) const matchedServiceIds = new Set(aliases.map((alias) => alias.service_id)) - const missingServiceIds = serviceIds.filter( - (serviceId) => !matchedServiceIds.has(serviceId) + const missingConsumerAliases = serviceIds.filter( + (serviceId) => + targetServiceIds.has(serviceId) && !matchedServiceIds.has(serviceId) ) - if (missingServiceIds.length > 0) { + if (missingConsumerAliases.length > 0) { throw new Error( - `Missing local env alias for ${providerId}.${outputId} service(s): ${missingServiceIds.join(",")}.` + `Missing local env alias for ${providerId}.${outputId} consumer service(s): ${missingConsumerAliases.join(",")}.` ) } } diff --git a/apps/new-engine-ctl/src/contracts/stack-manifest.ts b/apps/new-engine-ctl/src/contracts/stack-manifest.ts index f37a718b1c..5e10990c3c 100644 --- a/apps/new-engine-ctl/src/contracts/stack-manifest.ts +++ b/apps/new-engine-ctl/src/contracts/stack-manifest.ts @@ -10,6 +10,7 @@ export const localPhaseSchema = z.enum([ const defaultCiConfig = { deployable: false, + enabled_by_default: true, affected_path_globs: [], prepare: { preview_db: false, @@ -51,6 +52,7 @@ const serviceSchema = z.looseObject({ ci: z .looseObject({ deployable: z.boolean().optional().default(false), + enabled_by_default: z.boolean().optional().default(true), affected_path_globs: z.array(z.string().min(1)).optional().default([]), prepare: prepareSchema.optional().default(defaultCiConfig.prepare), zane: zaneServiceSchema.optional(), @@ -78,6 +80,7 @@ export type StackManifest = z.infer export type DeployableService = { id: string serviceSlug: string + enabledByDefault: boolean cloneToPreview: boolean deployLanes: Lane[] deployStage: number @@ -101,6 +104,7 @@ function toDeployableService( return { id: service.id, serviceSlug: service.ci.zane.service_slug, + enabledByDefault: service.ci.enabled_by_default, cloneToPreview: service.ci.zane.clone_to_preview, deployLanes: service.ci.zane.deploy_lanes, deployStage: service.ci.zane.deploy_stage, @@ -135,6 +139,29 @@ export function getDeployableService( return toDeployableService(service) } +export function getZaneService( + manifest: StackManifest, + serviceId: string +): DeployableService { + const service = manifest.services.find( + (candidate) => candidate.id === serviceId + ) + if (!service?.ci.zane) { + throw new Error(`Service is missing Zane metadata: ${serviceId}`) + } + + return { + id: service.id, + serviceSlug: service.ci.zane.service_slug, + enabledByDefault: service.ci.enabled_by_default, + cloneToPreview: service.ci.zane.clone_to_preview, + deployLanes: service.ci.zane.deploy_lanes, + deployStage: service.ci.zane.deploy_stage, + downtimeRisk: service.ci.zane.downtime_risk, + serviceDependencies: service.ci.zane.service_dependencies, + } +} + export function listComposeServicesForPhase( manifest: StackManifest, phase: LocalPhase, @@ -168,11 +195,13 @@ export function listPrepareServiceIds( export function listLaneServiceIds( manifest: StackManifest, - lane: Lane + lane: Lane, + defaultOnly = false ): string[] { return listDeployableServices(manifest) .filter( (service) => + (!defaultOnly || service.enabledByDefault) && service.deployLanes.includes(lane) && (lane !== "preview" || service.cloneToPreview) ) diff --git a/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts b/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts index 981ac9caef..d9a092c483 100644 --- a/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts +++ b/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts @@ -9,7 +9,7 @@ import { getBootstrapZaneProjectSharedEnvDefinitions, type StackInputs, } from "../../contracts/stack-inputs.js" -import { listDeployableServices } from "../../contracts/stack-manifest.js" +import { getZaneService } from "../../contracts/stack-manifest.js" import { loadDeployContracts } from "../deploy-inputs.js" import type { BootstrapValueSource } from "./shared.js" import { @@ -149,6 +149,16 @@ const sharedEnvCleanupKeys = [ "MEILI_MASTER_KEY", "JWT_SECRET", "COOKIE_SECRET", + "MEDUSA_COOKIE_SECURE", + "MEDUSA_COOKIE_SAME_SITE", + "MEDUSA_ADMIN_DISABLED_FOR_BACKEND_BUILD", + "STOREFRONT_URL", + "STORE_NAME", + "PRODUCT_REVIEW_REQUEST_MESSAGE", + "PRODUCT_REVIEW_REQUEST_DELAY_MINUTES", + "PRODUCT_REVIEW_TOKEN_EXPIRY_DAYS", + "WORKFLOW_QUEUE_RUNNER_BATCH_SIZE", + "WORKFLOW_QUEUE_RUNNER_SCHEDULE", "SETTINGS_ENCRYPTION_KEY", "SUPERADMIN_EMAIL", "SUPERADMIN_PASSWORD", @@ -176,6 +186,10 @@ const sharedEnvCleanupKeys = [ "COMGATE_PAYMENT_LABEL", "HERBATICA_XML_PATH", "HERBATICA_CATEGORIES_XML_PATH", + "HERBATICA_MANUFACTURERS_CSV_PATH", + "HERBATICA_REVIEWS_XML_PATH", + "SENTRY_TRACES_SAMPLE_RATE", + "FEATURE_PAYMENT_QR_ENABLED", "MEDUSA_BE_NODE_ENV", "MEDUSA_BE_BACKEND_URL", "MEDUSA_BE_STORE_CORS", @@ -257,9 +271,11 @@ const sharedEnvCleanupKeys = [ "DC_NODE_ENV", "DC_SENTRY_NAME", "DC_SENTRY_DSN", + "DC_SENTRY_TRACES_SAMPLE_RATE", "DC_SETTINGS_ENCRYPTION_KEY", "DC_FEATURE_PPL_ENABLED", "DC_PPL_ENVIRONMENT", + "DC_FEATURE_PAYMENT_QR_ENABLED", "DC_SUPERADMIN_EMAIL", "DC_SUPERADMIN_PASSWORD", "DC_INITIAL_PUBLISHABLE_KEY_NAME", @@ -268,6 +284,14 @@ const sharedEnvCleanupKeys = [ "DC_N1_NEXT_PUBLIC_MEILISEARCH_URL", "DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEY", "DC_N1_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", + "DC_HERBATIKA_NEXT_PUBLIC_STOREFRONT_AUTH_MODE", + "DC_HERBATIKA_MEDUSA_BACKEND_URL_INTERNAL", + "DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_BACKEND_URL", + "DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES", + "DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY", + "DC_HERBATIKA_NEXT_PUBLIC_PPL_WIDGET_API_KEY", + "DC_HERBATIKA_NEXT_PUBLIC_PAYLOAD_BASE_URL", + "DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "DC_N1_NEXT_PUBLIC_META_PIXEL_ID", "DC_N1_NEXT_PUBLIC_GOOGLE_ADS_ID", "DC_N1_NEXT_PUBLIC_HEUREKA_API_KEY", @@ -307,6 +331,16 @@ const sharedEnvCleanupKeys = [ "DC_MINIO_ROOT_PASSWORD", "DC_JWT_SECRET", "DC_COOKIE_SECRET", + "DC_MEDUSA_COOKIE_SECURE", + "DC_MEDUSA_COOKIE_SAME_SITE", + "DC_MEDUSA_ADMIN_DISABLED_FOR_BACKEND_BUILD", + "DC_STOREFRONT_URL", + "DC_STORE_NAME", + "DC_PRODUCT_REVIEW_REQUEST_MESSAGE", + "DC_PRODUCT_REVIEW_REQUEST_DELAY_MINUTES", + "DC_PRODUCT_REVIEW_TOKEN_EXPIRY_DAYS", + "DC_WORKFLOW_QUEUE_RUNNER_BATCH_SIZE", + "DC_WORKFLOW_QUEUE_RUNNER_SCHEDULE", "DC_MEDUSA_DEV_DB_USER", "DC_MEDUSA_DEV_DB_PASSWORD", "DC_REDIS_URL", @@ -318,6 +352,8 @@ const sharedEnvCleanupKeys = [ "DC_MEDUSA_BE_MEILISEARCH_ENABLED", "DC_MEDUSA_BE_FILE_PROVIDER", "DC_MEDUSA_BE_FILE_LOCAL_UPLOAD_DIR", + "DC_HERBATICA_MANUFACTURERS_CSV_PATH", + "DC_HERBATICA_REVIEWS_XML_PATH", "DC_MEILISEARCH_BACKEND_API_KEY", "DC_N1_MEDUSA_BACKEND_URL_INTERNAL", "DC_N1_NEXT_PUBLIC_MEDUSA_BACKEND_URL", @@ -498,7 +534,8 @@ function buildZaneProjectServices( : `${protectedNamesBase},template_medusa` const medusaBeSlug = requiredServiceSlug(serviceSlugs, "medusa-be") const payloadSlug = requiredServiceSlug(serviceSlugs, "payload") - const n1Slug = requiredServiceSlug(serviceSlugs, "n1") + const herbatikaSlug = requiredServiceSlug(serviceSlugs, "herbatika") + const n1Slug = serviceSlugs.n1 const meilisearchSlug = requiredServiceSlug( serviceSlugs, "medusa-meilisearch" @@ -508,7 +545,7 @@ function buildZaneProjectServices( const servicePublicOrigins = { medusaBe: servicePublicOriginSource(medusaBeSlug), payload: servicePublicOriginSource(payloadSlug), - n1: servicePublicOriginSource(n1Slug), + herbatika: servicePublicOriginSource(herbatikaSlug), meilisearch: servicePublicOriginSource(meilisearchSlug), } const minioFileSource = context.minioFileUrlOverride @@ -732,7 +769,17 @@ function buildZaneProjectServices( "DC_NODE_ENV", "DC_JWT_SECRET", "DC_COOKIE_SECRET", + "DC_MEDUSA_COOKIE_SECURE", + "DC_MEDUSA_COOKIE_SAME_SITE", + "DC_MEDUSA_ADMIN_DISABLED_FOR_BACKEND_BUILD", "DC_MEDUSA_BACKEND_URL", + "DC_STOREFRONT_URL", + "DC_STORE_NAME", + "DC_PRODUCT_REVIEW_REQUEST_MESSAGE", + "DC_PRODUCT_REVIEW_REQUEST_DELAY_MINUTES", + "DC_PRODUCT_REVIEW_TOKEN_EXPIRY_DAYS", + "DC_WORKFLOW_QUEUE_RUNNER_BATCH_SIZE", + "DC_WORKFLOW_QUEUE_RUNNER_SCHEDULE", "DC_STORE_CORS", "DC_ADMIN_CORS", "DC_AUTH_CORS", @@ -742,13 +789,17 @@ function buildZaneProjectServices( "DC_SETTINGS_ENCRYPTION_KEY", "DC_SENTRY_NAME", "DC_SENTRY_DSN", + "DC_SENTRY_TRACES_SAMPLE_RATE", "DC_HERBATICA_XML_PATH", "DC_HERBATICA_CATEGORIES_XML_PATH", + "DC_HERBATICA_MANUFACTURERS_CSV_PATH", + "DC_HERBATICA_REVIEWS_XML_PATH", "DC_FEATURE_PPL_ENABLED", "DC_PPL_ENVIRONMENT", "DC_FEATURE_PACKETA_ENABLED", "DC_PACKETA_ENVIRONMENT", "DC_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY", + "DC_FEATURE_PAYMENT_QR_ENABLED", "DC_FEATURE_PAYKIT_ENABLED", "DC_FEATURE_PAYKIT_GOPAY_ENABLED", "DC_FEATURE_PAYKIT_STRIPE_ENABLED", @@ -817,6 +868,20 @@ function buildZaneProjectServices( envVar: "COOKIE_SECRET", source: literalSource(process.env.DC_COOKIE_SECRET ?? ""), }, + { + envVar: "MEDUSA_COOKIE_SECURE", + source: literalSource(process.env.DC_MEDUSA_COOKIE_SECURE ?? ""), + }, + { + envVar: "MEDUSA_COOKIE_SAME_SITE", + source: literalSource(process.env.DC_MEDUSA_COOKIE_SAME_SITE ?? ""), + }, + { + envVar: "MEDUSA_ADMIN_DISABLED_FOR_BACKEND_BUILD", + source: literalSource( + process.env.DC_MEDUSA_ADMIN_DISABLED_FOR_BACKEND_BUILD ?? "0" + ), + }, { envVar: "MEDUSA_BACKEND_URL", source: servicePublicOrigins.medusaBe }, { envVar: "STORE_CORS", source: literalSource(context.storeCors) }, { envVar: "ADMIN_CORS", source: literalSource(context.adminCors) }, @@ -848,7 +913,53 @@ function buildZaneProjectServices( envVar: "SENTRY_DSN", source: literalSource(process.env.DC_SENTRY_DSN ?? ""), }, - { envVar: "STOREFRONT_URL", source: servicePublicOrigins.n1 }, + { + envVar: "SENTRY_TRACES_SAMPLE_RATE", + source: literalSource( + process.env.DC_SENTRY_TRACES_SAMPLE_RATE ?? "0.1" + ), + }, + { + envVar: "STOREFRONT_URL", + source: process.env.DC_STOREFRONT_URL?.trim() + ? literalSource(process.env.DC_STOREFRONT_URL.trim()) + : servicePublicOrigins.herbatika, + }, + { + envVar: "STORE_NAME", + source: literalSource(process.env.DC_STORE_NAME ?? "Herbatika"), + }, + { + envVar: "PRODUCT_REVIEW_REQUEST_MESSAGE", + source: literalSource( + process.env.DC_PRODUCT_REVIEW_REQUEST_MESSAGE ?? + "Napiš recenzi produktu" + ), + }, + { + envVar: "PRODUCT_REVIEW_REQUEST_DELAY_MINUTES", + source: literalSource( + process.env.DC_PRODUCT_REVIEW_REQUEST_DELAY_MINUTES ?? "10080" + ), + }, + { + envVar: "PRODUCT_REVIEW_TOKEN_EXPIRY_DAYS", + source: literalSource( + process.env.DC_PRODUCT_REVIEW_TOKEN_EXPIRY_DAYS ?? "90" + ), + }, + { + envVar: "WORKFLOW_QUEUE_RUNNER_BATCH_SIZE", + source: literalSource( + process.env.DC_WORKFLOW_QUEUE_RUNNER_BATCH_SIZE ?? "500" + ), + }, + { + envVar: "WORKFLOW_QUEUE_RUNNER_SCHEDULE", + source: literalSource( + process.env.DC_WORKFLOW_QUEUE_RUNNER_SCHEDULE ?? "0 * * * *" + ), + }, { envVar: "HERBATICA_XML_PATH", source: literalSource(process.env.DC_HERBATICA_XML_PATH ?? ""), @@ -859,6 +970,18 @@ function buildZaneProjectServices( process.env.DC_HERBATICA_CATEGORIES_XML_PATH ?? "" ), }, + { + envVar: "HERBATICA_MANUFACTURERS_CSV_PATH", + source: literalSource( + process.env.DC_HERBATICA_MANUFACTURERS_CSV_PATH ?? "" + ), + }, + { + envVar: "HERBATICA_REVIEWS_XML_PATH", + source: literalSource( + process.env.DC_HERBATICA_REVIEWS_XML_PATH ?? "" + ), + }, { envVar: "FEATURE_PPL_ENABLED", source: literalSource(process.env.DC_FEATURE_PPL_ENABLED ?? "0"), @@ -883,6 +1006,12 @@ function buildZaneProjectServices( process.env.DC_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY ?? "" ), }, + { + envVar: "FEATURE_PAYMENT_QR_ENABLED", + source: literalSource( + process.env.DC_FEATURE_PAYMENT_QR_ENABLED ?? "0" + ), + }, { envVar: "FEATURE_PAYKIT_ENABLED", source: literalSource(process.env.DC_FEATURE_PAYKIT_ENABLED ?? "0"), @@ -998,12 +1127,6 @@ function buildZaneProjectServices( envVar: "MEILISEARCH_HOST", source: literalSource("http://{{env.MEDUSA_MEILISEARCH_HOST}}:7700"), }, - { - envVar: "MEILISEARCH_API_KEY", - source: literalSource( - process.env.DC_MEILISEARCH_BACKEND_API_KEY ?? "" - ), - }, { envVar: "MINIO_FILE_URL", source: minioFileSource }, { envVar: "MINIO_ENDPOINT", @@ -1022,25 +1145,28 @@ function buildZaneProjectServices( { envVar: "RESEND_API_KEY", source: literalSource( - process.env.DC_MEDUSA_BE_RESEND_API_KEY ?? - process.env.DC_RESEND_API_KEY ?? - "" + firstNonEmpty( + process.env.DC_MEDUSA_BE_RESEND_API_KEY, + process.env.DC_RESEND_API_KEY + ) ?? "" ), }, { envVar: "RESEND_FROM_EMAIL", source: literalSource( - process.env.DC_MEDUSA_BE_RESEND_FROM_EMAIL ?? - process.env.DC_RESEND_FROM_EMAIL ?? - "" + firstNonEmpty( + process.env.DC_MEDUSA_BE_RESEND_FROM_EMAIL, + process.env.DC_RESEND_FROM_EMAIL + ) ?? "" ), }, { envVar: "RESEND_WEBHOOK_SECRET", source: literalSource( - process.env.DC_MEDUSA_BE_RESEND_WEBHOOK_SECRET ?? - process.env.DC_RESEND_WEBHOOK_SECRET ?? - "" + firstNonEmpty( + process.env.DC_MEDUSA_BE_RESEND_WEBHOOK_SECRET, + process.env.DC_RESEND_WEBHOOK_SECRET + ) ?? "" ), }, ], @@ -1168,8 +1294,8 @@ function buildZaneProjectServices( }, ], }, - n1: { - dockerfilePath: "./docker/development/n1/Dockerfile", + herbatika: { + dockerfilePath: "./docker/development/herbatika/Dockerfile", buildContextDir: "./", command: null, volumes: [], @@ -1178,7 +1304,7 @@ function buildZaneProjectServices( domain: publicServiceDomain({ projectSlug: context.projectSlug, - serviceSlug: n1Slug, + serviceSlug: herbatikaSlug, publicUrlAffix: context.publicUrlAffix, publicDomain: context.publicDomain, }) ?? "", @@ -1189,7 +1315,7 @@ function buildZaneProjectServices( ].filter((url) => url.domain), healthcheck: { type: "PATH", - value: "/api/health", + value: "/", timeout_seconds: 120, interval_seconds: 30, associated_port: 3000, @@ -1199,29 +1325,20 @@ function buildZaneProjectServices( memory: { unit: "MEGABYTES", value: 1536 }, }, cleanupEnvKeys: [ - "NEXT_PUBLIC_META_PIXEL_ID", - "NEXT_PUBLIC_GOOGLE_ADS_ID", - "NEXT_PUBLIC_HEUREKA_API_KEY", - "NEXT_PUBLIC_LEADHUB_TRACKING_ID", - "RESEND_API_KEY", - "CONTACT_EMAIL", - "RESEND_FROM_EMAIL", - "DC_N1_MEDUSA_BACKEND_URL_INTERNAL", - "DC_N1_NEXT_PUBLIC_MEDUSA_BACKEND_URL", - "DC_N1_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", - "DC_N1_NEXT_PUBLIC_MEILISEARCH_URL", - "DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEY", - "DC_N1_NEXT_PUBLIC_SITE_URL", - "DC_N1_NEXT_PUBLIC_META_PIXEL_ID", - "DC_N1_NEXT_PUBLIC_GOOGLE_ADS_ID", - "DC_N1_NEXT_PUBLIC_HEUREKA_API_KEY", - "DC_N1_NEXT_PUBLIC_LEADHUB_TRACKING_ID", - "DC_N1_RESEND_API_KEY", - "DC_N1_CONTACT_EMAIL", - "DC_N1_RESEND_FROM_EMAIL", - "DC_N1_MEDUSA_RESEND_API_KEY", - "DC_N1_MEDUSA_CONTACT_EMAIL", - "DC_N1_MEDUSA_RESEND_FROM_EMAIL", + "MEILISEARCH_HOST", + "MEILISEARCH_SEARCH_API_KEY", + "MEILISEARCH_PRODUCTS_INDEX", + "MEILISEARCH_CATEGORIES_INDEX", + "MEILISEARCH_PRODUCERS_INDEX", + "DC_HERBATIKA_PUBLIC_PORT", + "DC_HERBATIKA_NEXT_PUBLIC_STOREFRONT_AUTH_MODE", + "DC_HERBATIKA_MEDUSA_BACKEND_URL_INTERNAL", + "DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_BACKEND_URL", + "DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES", + "DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY", + "DC_HERBATIKA_NEXT_PUBLIC_PPL_WIDGET_API_KEY", + "DC_HERBATIKA_NEXT_PUBLIC_PAYLOAD_BASE_URL", + "DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", ], env: [ { @@ -1236,72 +1353,169 @@ function buildZaneProjectServices( source: servicePublicOrigins.medusaBe, }, { - envVar: "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", + envVar: "NEXT_PUBLIC_STOREFRONT_AUTH_MODE", source: literalSource( - process.env.DC_N1_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ?? "" + process.env.DC_HERBATIKA_NEXT_PUBLIC_STOREFRONT_AUTH_MODE ?? + "session_proxy" ), }, { - envVar: "NEXT_PUBLIC_MEILISEARCH_URL", - source: servicePublicOrigins.meilisearch, - }, - { - envVar: "NEXT_PUBLIC_MEILISEARCH_API_KEY", + envVar: "NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES", source: literalSource( - process.env.DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEY ?? "" + process.env.DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES ?? + "sk" ), }, - { envVar: "NEXT_PUBLIC_SITE_URL", source: servicePublicOrigins.n1 }, { - envVar: "NEXT_PUBLIC_META_PIXEL_ID", + envVar: "NEXT_PUBLIC_PACKETA_WIDGET_API_KEY", source: literalSource( - process.env.DC_N1_NEXT_PUBLIC_META_PIXEL_ID ?? "" + process.env.DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY ?? "" ), }, { - envVar: "NEXT_PUBLIC_GOOGLE_ADS_ID", + envVar: "NEXT_PUBLIC_PPL_WIDGET_API_KEY", source: literalSource( - process.env.DC_N1_NEXT_PUBLIC_GOOGLE_ADS_ID ?? "" + process.env.DC_HERBATIKA_NEXT_PUBLIC_PPL_WIDGET_API_KEY ?? "" ), }, { - envVar: "NEXT_PUBLIC_HEUREKA_API_KEY", - source: literalSource( - process.env.DC_N1_NEXT_PUBLIC_HEUREKA_API_KEY ?? "" - ), - }, - { - envVar: "NEXT_PUBLIC_LEADHUB_TRACKING_ID", - source: literalSource( - process.env.DC_N1_NEXT_PUBLIC_LEADHUB_TRACKING_ID ?? "" - ), - }, - { - envVar: "RESEND_API_KEY", - source: literalSource( - process.env.DC_N1_RESEND_API_KEY ?? - process.env.DC_RESEND_API_KEY ?? - "" - ), - }, - { - envVar: "CONTACT_EMAIL", - source: literalSource( - process.env.DC_N1_CONTACT_EMAIL ?? - process.env.DC_CONTACT_EMAIL ?? - "" - ), - }, - { - envVar: "RESEND_FROM_EMAIL", - source: literalSource( - process.env.DC_N1_RESEND_FROM_EMAIL ?? - process.env.DC_RESEND_FROM_EMAIL ?? - "" - ), + envVar: "NEXT_PUBLIC_PAYLOAD_BASE_URL", + source: servicePublicOrigins.payload, }, ], }, + ...(n1Slug + ? { + n1: { + dockerfilePath: "./docker/development/n1/Dockerfile", + buildContextDir: "./", + command: null, + volumes: [], + urls: [ + { + domain: + publicServiceDomain({ + projectSlug: context.projectSlug, + serviceSlug: n1Slug, + publicUrlAffix: context.publicUrlAffix, + publicDomain: context.publicDomain, + }) ?? "", + base_path: "/", + strip_prefix: true, + associated_port: 3000, + }, + ].filter((url) => url.domain), + healthcheck: { + type: "PATH", + value: "/api/health", + timeout_seconds: 120, + interval_seconds: 30, + associated_port: 3000, + }, + resourceLimits: { + cpus: 0.75, + memory: { unit: "MEGABYTES", value: 1536 }, + }, + cleanupEnvKeys: [ + "NEXT_PUBLIC_META_PIXEL_ID", + "NEXT_PUBLIC_GOOGLE_ADS_ID", + "NEXT_PUBLIC_HEUREKA_API_KEY", + "NEXT_PUBLIC_LEADHUB_TRACKING_ID", + "RESEND_API_KEY", + "CONTACT_EMAIL", + "RESEND_FROM_EMAIL", + "DC_N1_MEDUSA_BACKEND_URL_INTERNAL", + "DC_N1_NEXT_PUBLIC_MEDUSA_BACKEND_URL", + "DC_N1_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", + "DC_N1_NEXT_PUBLIC_MEILISEARCH_URL", + "DC_N1_NEXT_PUBLIC_MEILISEARCH_API_KEY", + "DC_N1_NEXT_PUBLIC_SITE_URL", + "DC_N1_NEXT_PUBLIC_META_PIXEL_ID", + "DC_N1_NEXT_PUBLIC_GOOGLE_ADS_ID", + "DC_N1_NEXT_PUBLIC_HEUREKA_API_KEY", + "DC_N1_NEXT_PUBLIC_LEADHUB_TRACKING_ID", + "DC_N1_RESEND_API_KEY", + "DC_N1_CONTACT_EMAIL", + "DC_N1_RESEND_FROM_EMAIL", + "DC_N1_MEDUSA_RESEND_API_KEY", + "DC_N1_MEDUSA_CONTACT_EMAIL", + "DC_N1_MEDUSA_RESEND_FROM_EMAIL", + ], + env: [ + { + envVar: "MEDUSA_BACKEND_URL_INTERNAL", + source: serviceInternalOriginSource({ + serviceSlug: medusaBeSlug, + port: 9000, + }), + }, + { + envVar: "NEXT_PUBLIC_MEDUSA_BACKEND_URL", + source: servicePublicOrigins.medusaBe, + }, + { + envVar: "NEXT_PUBLIC_MEILISEARCH_URL", + source: servicePublicOrigins.meilisearch, + }, + { + envVar: "NEXT_PUBLIC_SITE_URL", + source: servicePublicOriginSource(n1Slug), + }, + { + envVar: "NEXT_PUBLIC_META_PIXEL_ID", + source: literalSource( + process.env.DC_N1_NEXT_PUBLIC_META_PIXEL_ID ?? "" + ), + }, + { + envVar: "NEXT_PUBLIC_GOOGLE_ADS_ID", + source: literalSource( + process.env.DC_N1_NEXT_PUBLIC_GOOGLE_ADS_ID ?? "" + ), + }, + { + envVar: "NEXT_PUBLIC_HEUREKA_API_KEY", + source: literalSource( + process.env.DC_N1_NEXT_PUBLIC_HEUREKA_API_KEY ?? "" + ), + }, + { + envVar: "NEXT_PUBLIC_LEADHUB_TRACKING_ID", + source: literalSource( + process.env.DC_N1_NEXT_PUBLIC_LEADHUB_TRACKING_ID ?? "" + ), + }, + { + envVar: "RESEND_API_KEY", + source: literalSource( + firstNonEmpty( + process.env.DC_N1_RESEND_API_KEY, + process.env.DC_RESEND_API_KEY + ) ?? "" + ), + }, + { + envVar: "CONTACT_EMAIL", + source: literalSource( + firstNonEmpty( + process.env.DC_N1_CONTACT_EMAIL, + process.env.DC_CONTACT_EMAIL + ) ?? "" + ), + }, + { + envVar: "RESEND_FROM_EMAIL", + source: literalSource( + firstNonEmpty( + process.env.DC_N1_RESEND_FROM_EMAIL, + process.env.DC_RESEND_FROM_EMAIL + ) ?? "" + ), + }, + ], + }, + } + : {}), "zane-operator": { dockerfilePath: "./docker/development/zane-operator/Dockerfile", buildContextDir: "./", @@ -1494,7 +1708,7 @@ function buildContext(input: { explicitValue: input.planInput.storeCorsOverride, envValue: process.env.DC_STORE_CORS, fallbackValue: publicDomain - ? `https://${input.planInput.projectSlug}-${"n1"}${input.planInput.publicUrlAffix}.${publicDomain}` + ? `https://${input.planInput.projectSlug}-${"herbatika"}${input.planInput.publicUrlAffix}.${publicDomain}` : "https://pending-public-domain.invalid", }), adminCors: preferPublicCsvOrUrl({ @@ -1916,12 +2130,19 @@ export async function executeBootstrapZaneProjectPlan( input.stackManifestPath, input.stackInputsPath ) - const deployableServices = listDeployableServices(manifest) const repositoryUrl = await deriveRepositoryUrl(input.repositoryUrl) const branchName = await deriveBranchName(input.branchName) const inspectResponse = bootstrapZaneProjectInspectResponseSchema.parse( await readJsonFile(input.inspectJsonPath) ) + const inspectedServiceSlugs = new Set( + inspectResponse.services.map((service) => service.service_slug) + ) + const bootstrapServices = manifest.services.flatMap((service) => + service.ci.zane && inspectedServiceSlugs.has(service.ci.zane.service_slug) + ? [getZaneService(manifest, service.id)] + : [] + ) const context = buildContext({ planInput: input, settings: inspectResponse.settings, @@ -1929,12 +2150,12 @@ export async function executeBootstrapZaneProjectPlan( branchName, }) const serviceSlugById = Object.fromEntries( - deployableServices.map((service) => [service.id, service.serviceSlug]) + bootstrapServices.map((service) => [service.id, service.serviceSlug]) ) as Record const plannedServices = buildZaneProjectServices(context, serviceSlugById) applySharedEnvServiceTargets({ plannedServices, stackInputs }) const inspectedServices = Object.fromEntries( - deployableServices.map((service) => { + bootstrapServices.map((service) => { const inspected = inspectResponse.services.find( (candidate) => candidate.service_slug === service.serviceSlug ) @@ -1987,7 +2208,7 @@ export async function executeBootstrapZaneProjectPlan( connect_base_url: context.operatorUpstreamConnectBaseUrl, connect_host_header: context.operatorUpstreamConnectHostHeader, }, - services: deployableServices.map((service) => { + services: bootstrapServices.map((service) => { const servicePlan = plannedServices[service.id] const serviceState = inspectedServices[service.id] if (!servicePlan) { diff --git a/apps/new-engine-ctl/src/orchestration/plan.ts b/apps/new-engine-ctl/src/orchestration/plan.ts index 5bb8319720..d4e0002b03 100644 --- a/apps/new-engine-ctl/src/orchestration/plan.ts +++ b/apps/new-engine-ctl/src/orchestration/plan.ts @@ -83,8 +83,14 @@ function buildPlanService( } } -function buildPreviewServiceSets(manifest: StackManifest): PreviewServiceSets { - const services = listDeployableServices(manifest) +function buildPreviewServiceSets( + manifest: StackManifest, + explicitlyRequestedServiceIds: Set +): PreviewServiceSets { + const services = listDeployableServices(manifest).filter( + (service) => + service.enabledByDefault || explicitlyRequestedServiceIds.has(service.id) + ) return { clonedServices: services.filter((service) => service.cloneToPreview), @@ -144,7 +150,7 @@ export async function executePlan( ) const previewServiceSets = input.lane === "preview" - ? buildPreviewServiceSets(manifest) + ? buildPreviewServiceSets(manifest, requestedServiceIds) : { clonedServices: [], excludedServices: [] } const response = planResponseSchema.parse({ diff --git a/apps/new-engine-ctl/src/orchestration/prepare.ts b/apps/new-engine-ctl/src/orchestration/prepare.ts index 0c0950bc20..5bd986df45 100644 --- a/apps/new-engine-ctl/src/orchestration/prepare.ts +++ b/apps/new-engine-ctl/src/orchestration/prepare.ts @@ -46,7 +46,9 @@ async function resolveRequiresPreviewDb( listDeployableServices(manifest) .filter( (service) => - service.cloneToPreview && service.deployLanes.includes("preview") + service.enabledByDefault && + service.cloneToPreview && + service.deployLanes.includes("preview") ) .map((service) => service.id) ) diff --git a/apps/new-engine-ctl/src/orchestration/scope.ts b/apps/new-engine-ctl/src/orchestration/scope.ts index 4e03dbdfa4..04be923665 100644 --- a/apps/new-engine-ctl/src/orchestration/scope.ts +++ b/apps/new-engine-ctl/src/orchestration/scope.ts @@ -250,7 +250,9 @@ function listPreviewBaselinePrepareServiceIds( listDeployableServices(manifest) .filter( (service) => - service.deployLanes.includes("preview") && service.cloneToPreview + service.enabledByDefault && + service.deployLanes.includes("preview") && + service.cloneToPreview ) .map((service) => service.id) ) @@ -398,8 +400,11 @@ function filterServicesAllowedInLane(input: { manifest: StackManifest lane: ScopeCommandInput["lane"] servicesCsv: string + defaultOnly?: boolean }): string { - const allowed = new Set(listLaneServiceIds(input.manifest, input.lane)) + const allowed = new Set( + listLaneServiceIds(input.manifest, input.lane, input.defaultOnly ?? false) + ) return normalizeCsvToArray(input.servicesCsv) .filter((serviceId) => allowed.has(serviceId)) @@ -461,6 +466,7 @@ export async function executeScope( manifest, lane: input.lane, servicesCsv, + defaultOnly: true, }) } diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index c76764c392..29a9798d8f 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -41,6 +41,7 @@ services: NEXT_PUBLIC_STOREFRONT_AUTH_MODE: ${DC_HERBATIKA_NEXT_PUBLIC_STOREFRONT_AUTH_MODE:-session_proxy} NEXT_PUBLIC_PACKETA_WIDGET_API_KEY: ${DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY:-} NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES: ${DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES:-sk} + NEXT_PUBLIC_PPL_WIDGET_API_KEY: ${DC_HERBATIKA_NEXT_PUBLIC_PPL_WIDGET_API_KEY:-} NEXT_PUBLIC_PAYLOAD_BASE_URL: ${DC_HERBATIKA_NEXT_PUBLIC_PAYLOAD_BASE_URL:-http://localhost:8083} environment: NODE_ENV: production diff --git a/docker-compose.yaml b/docker-compose.yaml index 608c0f7f0b..909c20f94c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -214,9 +214,8 @@ x-herbatika-env: &herbatika-env NEXT_PUBLIC_STOREFRONT_AUTH_MODE: ${DC_HERBATIKA_NEXT_PUBLIC_STOREFRONT_AUTH_MODE:-session_proxy} NEXT_PUBLIC_PACKETA_WIDGET_API_KEY: ${DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY:-} NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES: ${DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES:-sk} + NEXT_PUBLIC_PPL_WIDGET_API_KEY: ${DC_HERBATIKA_NEXT_PUBLIC_PPL_WIDGET_API_KEY:-} NEXT_PUBLIC_PAYLOAD_BASE_URL: ${DC_HERBATIKA_NEXT_PUBLIC_PAYLOAD_BASE_URL:-http://localhost:8083} - MEILISEARCH_HOST: ${DC_HERBATIKA_MEILISEARCH_HOST:-http://medusa-meilisearch:7700} - MEILISEARCH_SEARCH_API_KEY: ${DC_HERBATIKA_MEILISEARCH_SEARCH_API_KEY:-} x-payload-env: &payload-env DATABASE_URL: ${DC_PAYLOAD_DATABASE_URL:-postgresql://${DC_PAYLOAD_DATABASE_USER:-payload}:${DC_PAYLOAD_DATABASE_PASSWORD:-payload}@medusa-db:5432/${DC_MEDUSA_APP_DB_NAME:-medusa}?sslmode=${DC_POSTGRES_SSLMODE:-disable}&options=-csearch_path%3D${DC_PAYLOAD_DATABASE_SCHEMA_NAME:-payload}%2Cpg_catalog} @@ -334,6 +333,7 @@ services: NEXT_PUBLIC_STOREFRONT_AUTH_MODE: ${DC_HERBATIKA_NEXT_PUBLIC_STOREFRONT_AUTH_MODE:-session_proxy} NEXT_PUBLIC_PACKETA_WIDGET_API_KEY: ${DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY:-} NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES: ${DC_HERBATIKA_NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES:-sk} + NEXT_PUBLIC_PPL_WIDGET_API_KEY: ${DC_HERBATIKA_NEXT_PUBLIC_PPL_WIDGET_API_KEY:-} NEXT_PUBLIC_PAYLOAD_BASE_URL: ${DC_HERBATIKA_NEXT_PUBLIC_PAYLOAD_BASE_URL:-http://localhost:8083} depends_on: medusa-be: diff --git a/docker/development/herbatika/Dockerfile b/docker/development/herbatika/Dockerfile index 79b6ad0846..b14c32dc95 100644 --- a/docker/development/herbatika/Dockerfile +++ b/docker/development/herbatika/Dockerfile @@ -24,6 +24,35 @@ CMD ["sh", "-lc", "\ pnpm --filter=herbatika exec next dev --turbopack --hostname 0.0.0.0 --port 3000 \ "] +# ============================================ +# CI DEV TARGET - non-interactive preview runtime +# ============================================ +FROM base AS ci-dev +ENV NODE_ENV=development +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 +ENV CI=true + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY patches ./patches +RUN corepack install + +COPY tsconfig.base.json tsconfig.json nx.json ./ +COPY apps/herbatika ./apps/herbatika +COPY libs/ui ./libs/ui +COPY libs/storefront-data ./libs/storefront-data + +RUN --mount=type=cache,id=pnpm-store-herbatika,target=/pnpm/store \ + pnpm_config_store_dir=/pnpm/store \ + pnpm install --store-dir=/pnpm/store --prefer-offline --frozen-lockfile --filter=herbatika... && \ + pnpm --filter=@techsio/storefront-data build && \ + pnpm --filter=@techsio/ui-kit build && \ + pnpm install --store-dir=/pnpm/store --offline --frozen-lockfile --filter=herbatika... + +WORKDIR /var/www/apps/herbatika + +CMD ["npm", "run", "start:ci-dev"] + FROM base AS build ENV CI=true @@ -42,6 +71,7 @@ ARG NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY ARG NEXT_PUBLIC_STOREFRONT_AUTH_MODE ARG NEXT_PUBLIC_PACKETA_WIDGET_API_KEY ARG NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES +ARG NEXT_PUBLIC_PPL_WIDGET_API_KEY ARG NEXT_PUBLIC_PAYLOAD_BASE_URL ENV NEXT_PUBLIC_MEDUSA_BACKEND_URL=${NEXT_PUBLIC_MEDUSA_BACKEND_URL} @@ -49,6 +79,7 @@ ENV NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=${NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY} ENV NEXT_PUBLIC_STOREFRONT_AUTH_MODE=${NEXT_PUBLIC_STOREFRONT_AUTH_MODE} ENV NEXT_PUBLIC_PACKETA_WIDGET_API_KEY=${NEXT_PUBLIC_PACKETA_WIDGET_API_KEY} ENV NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES=${NEXT_PUBLIC_PACKETA_WIDGET_COUNTRIES} +ENV NEXT_PUBLIC_PPL_WIDGET_API_KEY=${NEXT_PUBLIC_PPL_WIDGET_API_KEY} ENV NEXT_PUBLIC_PAYLOAD_BASE_URL=${NEXT_PUBLIC_PAYLOAD_BASE_URL} RUN --mount=type=cache,id=pnpm-store-herbatika,target=/pnpm/store \ diff --git a/scripts/dev/run-zane-preview-lane.sh b/scripts/dev/run-zane-preview-lane.sh index 2de1bda3ce..663a99bdc9 100644 --- a/scripts/dev/run-zane-preview-lane.sh +++ b/scripts/dev/run-zane-preview-lane.sh @@ -46,7 +46,7 @@ Options: Notes: - Local defaults are sourced from .env.zane when present. - This helper stays thin: repo policy remains owned by apps/new-engine-ctl. - - Pass explicit services, for example: --services-csv medusa-be,n1 + - Pass explicit services, for example: --services-csv medusa-be,herbatika EOF } diff --git a/scripts/dev/setup-zane-project.sh b/scripts/dev/setup-zane-project.sh index 6f2a78ae94..635aaa832c 100644 --- a/scripts/dev/setup-zane-project.sh +++ b/scripts/dev/setup-zane-project.sh @@ -19,6 +19,7 @@ BRANCH_NAME="${ZANE_BRANCH_NAME:-}" GIT_APP_ID="${ZANE_GIT_APP_ID:-}" PUBLIC_DOMAIN="${ZANE_PUBLIC_DOMAIN:-}" PUBLIC_URL_AFFIX="${ZANE_PUBLIC_URL_AFFIX:--zane}" +SERVICES_CSV="" MINIO_FILE_URL_OVERRIDE="${ZANE_PUBLIC_MINIO_FILE_URL:-}" STORE_CORS_OVERRIDE="${ZANE_STORE_CORS:-}" @@ -61,6 +62,7 @@ Options: (default: auto-discovered from Zane API settings) --public-url-affix SUFFIX Service URL suffix between service slug and domain (default: -zane) + --services-csv CSV Override the complete service list to sync --minio-file-url URL Public MinIO file URL override --store-cors VALUE STORE_CORS override --admin-cors VALUE ADMIN_CORS override @@ -79,7 +81,7 @@ Options: --help Show this help Notes: - - The helper manages public routes for medusa-be, payload, n1, medusa-meilisearch, and zane-operator. + - Herbatika is in the default service list. Include n1 in --services-csv when N1 should also be synced. - The default branch is the current checked-out branch. Use --branch to target a different branch explicitly. EOF } @@ -131,6 +133,10 @@ setup::parse_args() { PUBLIC_URL_AFFIX="$2" shift 2 ;; + --services-csv) + SERVICES_CSV="$2" + shift 2 + ;; --minio-file-url) MINIO_FILE_URL_OVERRIDE="$2" shift 2 @@ -300,7 +306,6 @@ setup::resolve_ctl_plan() { [[ -n "$OPERATOR_UPSTREAM_ZANE_CONNECT_HOST_HEADER" ]] && ctl_args+=(--operator-upstream-zane-connect-host-header "$OPERATOR_UPSTREAM_ZANE_CONNECT_HOST_HEADER") [[ -n "$OPERATOR_UPSTREAM_ZANE_USERNAME" ]] && ctl_args+=(--operator-upstream-zane-username "$OPERATOR_UPSTREAM_ZANE_USERNAME") [[ -n "$OPERATOR_UPSTREAM_ZANE_PASSWORD" ]] && ctl_args+=(--operator-upstream-zane-password "$OPERATOR_UPSTREAM_ZANE_PASSWORD") - dev::run_ctl "${ctl_args[@]}" >"$PLAN_JSON_FILE" } @@ -1372,12 +1377,15 @@ setup::main() { medusa-meilisearch medusa-be payload - n1 + herbatika zane-operator ) local service_plan setup::parse_args "$@" + if [[ -n "$SERVICES_CSV" ]]; then + IFS=',' read -r -a services <<<"$SERVICES_CSV" + fi dev::load_env_file "$ENV_FILE" required setup::normalize_base_url setup::derive_repository_url @@ -1442,17 +1450,12 @@ Configured: - branch: ${BRANCH_NAME} Created or updated services: -- medusa-db -- medusa-valkey -- medusa-minio -- medusa-meilisearch -- medusa-be -- payload -- n1 -- zane-operator +EOF + printf -- '- %s\n' "${services[@]}" + cat < Date: Fri, 24 Jul 2026 17:34:26 +0200 Subject: [PATCH 2/4] fix(zane): rabbit and gretile fixes --- .../src/__tests__/scope-preview.test.ts | 29 ++++++++++++- .../src/contracts/stack-manifest.ts | 42 ++++++++++--------- docker/development/herbatika/Dockerfile | 5 +++ scripts/dev/setup-zane-project.sh | 38 +++++++++++++---- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts b/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts index d102a27116..d7993dffe7 100644 --- a/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts +++ b/apps/new-engine-ctl/src/__tests__/scope-preview.test.ts @@ -2,7 +2,12 @@ import { delimiter, dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" import { expect, test } from "vitest" -import { listLaneServiceIds } from "../contracts/stack-manifest.js" +import { + getDeployableService, + getZaneService, + listLaneServiceIds, + stackManifestSchema, +} from "../contracts/stack-manifest.js" import { loadDeployContracts } from "../orchestration/deploy-inputs.js" import { executePlan } from "../orchestration/plan.js" import { collectConfiguredRuntimeProviderNeeds } from "../orchestration/runtime-provider-orchestration.js" @@ -45,6 +50,28 @@ test("scope removes duplicate path casing when prefixing workspace bin", () => { expect(Object.hasOwn(env, "Path")).toBe(false) }) +test("Zane service lookup preserves its less restrictive deployability guard", () => { + const manifest = stackManifestSchema.parse({ + services: [ + { + id: "optional", + ci: { + deployable: false, + zane: { + service_slug: "optional", + deploy_lanes: ["main"], + }, + }, + }, + ], + }) + + expect(getZaneService(manifest, "optional").serviceSlug).toBe("optional") + expect(() => getDeployableService(manifest, "optional")).toThrow( + "Service is not deployable or missing Zane metadata: optional" + ) +}) + test("preview scope prepares DB credentials for first baseline replay", async () => { const result = await executeScope({ lane: "preview", diff --git a/apps/new-engine-ctl/src/contracts/stack-manifest.ts b/apps/new-engine-ctl/src/contracts/stack-manifest.ts index 5e10990c3c..64305f84b6 100644 --- a/apps/new-engine-ctl/src/contracts/stack-manifest.ts +++ b/apps/new-engine-ctl/src/contracts/stack-manifest.ts @@ -92,6 +92,26 @@ export type GlobalRuntimeRule = { serviceIds: string[] } +function buildDeployableService( + service: StackManifest["services"][number] +): DeployableService { + const zane = service.ci.zane + if (!zane) { + throw new Error(`Service is missing Zane metadata: ${service.id}`) + } + + return { + id: service.id, + serviceSlug: zane.service_slug, + enabledByDefault: service.ci.enabled_by_default, + cloneToPreview: zane.clone_to_preview, + deployLanes: zane.deploy_lanes, + deployStage: zane.deploy_stage, + downtimeRisk: zane.downtime_risk, + serviceDependencies: zane.service_dependencies, + } +} + function toDeployableService( service: StackManifest["services"][number] ): DeployableService { @@ -101,16 +121,7 @@ function toDeployableService( ) } - return { - id: service.id, - serviceSlug: service.ci.zane.service_slug, - enabledByDefault: service.ci.enabled_by_default, - cloneToPreview: service.ci.zane.clone_to_preview, - deployLanes: service.ci.zane.deploy_lanes, - deployStage: service.ci.zane.deploy_stage, - downtimeRisk: service.ci.zane.downtime_risk, - serviceDependencies: service.ci.zane.service_dependencies, - } + return buildDeployableService(service) } export function listDeployableServices( @@ -150,16 +161,7 @@ export function getZaneService( throw new Error(`Service is missing Zane metadata: ${serviceId}`) } - return { - id: service.id, - serviceSlug: service.ci.zane.service_slug, - enabledByDefault: service.ci.enabled_by_default, - cloneToPreview: service.ci.zane.clone_to_preview, - deployLanes: service.ci.zane.deploy_lanes, - deployStage: service.ci.zane.deploy_stage, - downtimeRisk: service.ci.zane.downtime_risk, - serviceDependencies: service.ci.zane.service_dependencies, - } + return buildDeployableService(service) } export function listComposeServicesForPhase( diff --git a/docker/development/herbatika/Dockerfile b/docker/development/herbatika/Dockerfile index b14c32dc95..c74416aa9a 100644 --- a/docker/development/herbatika/Dockerfile +++ b/docker/development/herbatika/Dockerfile @@ -49,6 +49,11 @@ RUN --mount=type=cache,id=pnpm-store-herbatika,target=/pnpm/store \ pnpm --filter=@techsio/ui-kit build && \ pnpm install --store-dir=/pnpm/store --offline --frozen-lockfile --filter=herbatika... +RUN mkdir -p /var/www/apps/herbatika/.next && \ + chown node:node /var/www/apps/herbatika /var/www/apps/herbatika/.next + +USER node + WORKDIR /var/www/apps/herbatika CMD ["npm", "run", "start:ci-dev"] diff --git a/scripts/dev/setup-zane-project.sh b/scripts/dev/setup-zane-project.sh index 635aaa832c..59048df2a2 100644 --- a/scripts/dev/setup-zane-project.sh +++ b/scripts/dev/setup-zane-project.sh @@ -1370,7 +1370,7 @@ setup::upsert_service_envs_from_plan() { } setup::main() { - local services=( + local baseline_services=( medusa-db medusa-valkey medusa-minio @@ -1380,12 +1380,30 @@ setup::main() { herbatika zane-operator ) + local services=("${baseline_services[@]}") + local inspection_services=("${baseline_services[@]}") + local parsed_services=() + local requested_service local service_plan setup::parse_args "$@" if [[ -n "$SERVICES_CSV" ]]; then - IFS=',' read -r -a services <<<"$SERVICES_CSV" + services=() + IFS=',' read -r -a parsed_services <<<"$SERVICES_CSV" + for requested_service in "${parsed_services[@]}"; do + requested_service="${requested_service//[[:space:]]/}" + [[ -n "$requested_service" ]] || continue + if [[ " ${services[*]} " != *" ${requested_service} "* ]]; then + services+=("$requested_service") + fi + done + [[ "${#services[@]}" -gt 0 ]] || common::die "--services-csv must contain at least one service id." fi + for requested_service in "${services[@]}"; do + if [[ " ${inspection_services[*]} " != *" ${requested_service} "* ]]; then + inspection_services+=("$requested_service") + fi + done dev::load_env_file "$ENV_FILE" required setup::normalize_base_url setup::derive_repository_url @@ -1403,7 +1421,7 @@ setup::main() { echo "Logging into Zane at ${ZANE_BASE_URL}..." zane::login setup::capture_zane_settings - setup::resolve_ctl_plan services "${services[@]}" + setup::resolve_ctl_plan services "${inspection_services[@]}" if [[ "$(jq -r '.warnings | length' <"$PLAN_JSON_FILE")" != "0" ]]; then setup::print_plan_summary fi @@ -1418,15 +1436,17 @@ setup::main() { fi zane::api GET "projects/${PROJECT_SLUG}/environment-details/${ENVIRONMENT_NAME}/" >/dev/null - while IFS= read -r service_plan; do + for requested_service in "${services[@]}"; do + service_plan="$(jq -c --arg service_id "$requested_service" '.services[] | select(.service_id == $service_id)' <"$PLAN_JSON_FILE")" + [[ -n "$service_plan" ]] || common::die "CTL bootstrap plan omitted requested service ${requested_service}." setup::apply_service_from_plan "$service_plan" - done < <(jq -c '.services[]' <"$PLAN_JSON_FILE") + done rm -f "$INSPECT_JSON_FILE" "$PLAN_JSON_FILE" INSPECT_JSON_FILE="" PLAN_JSON_FILE="" - setup::resolve_ctl_plan env "${services[@]}" + setup::resolve_ctl_plan env "${inspection_services[@]}" if [[ "$(jq -r '.warnings | length' <"$PLAN_JSON_FILE")" != "0" ]]; then setup::print_plan_summary fi @@ -1436,9 +1456,11 @@ setup::main() { fi setup::upsert_shared_envs_from_plan "$(cat "$PLAN_JSON_FILE")" - while IFS= read -r service_plan; do + for requested_service in "${services[@]}"; do + service_plan="$(jq -c --arg service_id "$requested_service" '.services[] | select(.service_id == $service_id)' <"$PLAN_JSON_FILE")" + [[ -n "$service_plan" ]] || common::die "CTL bootstrap plan omitted requested service ${requested_service}." setup::upsert_service_envs_from_plan "$service_plan" - done < <(jq -c '.services[]' <"$PLAN_JSON_FILE") + done cat < Date: Fri, 24 Jul 2026 18:03:19 +0200 Subject: [PATCH 3/4] fix(zane): GOPAY URL fallback to generate based on medusa-be public URL --- .../__tests__/bootstrap-zane-project.test.ts | 6 +++++ .../orchestration/bootstrap/zane-project.ts | 25 +++++++++++++------ scripts/dev/lib/common.sh | 1 + 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts b/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts index 4154c94ae4..01fd530ca8 100644 --- a/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts +++ b/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts @@ -43,6 +43,10 @@ test("project sync manages Herbatika and current Medusa runtime envs", async () vi.stubEnv("DC_STOREFRONT_URL", "https://storefront.example.com") vi.stubEnv("DC_STORE_NAME", "Herbatika") vi.stubEnv("DC_FEATURE_PAYMENT_QR_ENABLED", "1") + vi.stubEnv( + "DC_GOPAY_WEBHOOK_URL", + "http://localhost:9000/hooks/payment/paykit_gopay" + ) vi.stubEnv("DC_HERBATICA_REVIEWS_XML_PATH", "https://example.com/reviews.xml") vi.stubEnv("DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "") vi.stubEnv("DC_MEDUSA_BE_RESEND_FROM_EMAIL", "") @@ -170,6 +174,8 @@ test("project sync manages Herbatika and current Medusa runtime envs", async () STOREFRONT_URL: "https://storefront.example.com", STORE_NAME: "Herbatika", FEATURE_PAYMENT_QR_ENABLED: "1", + GOPAY_WEBHOOK_URL: + "https://test-engine-medusa-be-zane.example.com/hooks/payment/paykit_gopay", HERBATICA_REVIEWS_XML_PATH: "https://example.com/reviews.xml", RESEND_FROM_EMAIL: "noreply@example.com", }) diff --git a/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts b/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts index d9a092c483..6c268084a8 100644 --- a/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts +++ b/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts @@ -541,6 +541,21 @@ function buildZaneProjectServices( "medusa-meilisearch" ) const minioSlug = requiredServiceSlug(serviceSlugs, "medusa-minio") + const medusaBePublicDomain = publicServiceDomain({ + projectSlug: context.projectSlug, + serviceSlug: medusaBeSlug, + publicUrlAffix: context.publicUrlAffix, + publicDomain: context.publicDomain, + }) + const configuredGoPayWebhookUrl = + process.env.DC_GOPAY_WEBHOOK_URL?.trim() ?? "" + const generatedGoPayWebhookUrl = medusaBePublicDomain + ? `https://${medusaBePublicDomain}/hooks/payment/paykit_gopay` + : "" + const goPayWebhookUrl = + configuredGoPayWebhookUrl && !isLoopbackUrl(configuredGoPayWebhookUrl) + ? configuredGoPayWebhookUrl + : generatedGoPayWebhookUrl const servicePublicOrigins = { medusaBe: servicePublicOriginSource(medusaBeSlug), @@ -744,13 +759,7 @@ function buildZaneProjectServices( volumes: [], urls: [ { - domain: - publicServiceDomain({ - projectSlug: context.projectSlug, - serviceSlug: medusaBeSlug, - publicUrlAffix: context.publicUrlAffix, - publicDomain: context.publicDomain, - }) ?? "", + domain: medusaBePublicDomain ?? "", base_path: "/", strip_prefix: true, associated_port: 9000, @@ -1056,7 +1065,7 @@ function buildZaneProjectServices( }, { envVar: "GOPAY_WEBHOOK_URL", - source: literalSource(process.env.DC_GOPAY_WEBHOOK_URL ?? ""), + source: literalSource(goPayWebhookUrl), }, { envVar: "STRIPE_API_KEY", diff --git a/scripts/dev/lib/common.sh b/scripts/dev/lib/common.sh index a7e4e32891..ef228933ff 100644 --- a/scripts/dev/lib/common.sh +++ b/scripts/dev/lib/common.sh @@ -106,6 +106,7 @@ common::ensure_pnpm() { pnpm_spec="$(common::resolve_pnpm_spec "$root_dir")" pnpm_version="${pnpm_spec#pnpm@}" + pnpm_version="${pnpm_version%%+*}" if current_version="$(pnpm --version 2>/dev/null)"; then if [[ "$current_version" == "$pnpm_version" ]]; then From d245f667f9b764b51dc2ac2471cda86850a2c7ca Mon Sep 17 00:00:00 2001 From: Vojtech Dolezal Date: Fri, 24 Jul 2026 18:16:38 +0200 Subject: [PATCH 4/4] fix(zane): generic test URLs, keep localhost for CORS envs --- .../__tests__/bootstrap-zane-project.test.ts | 50 ++++++++++++------- .../src/orchestration/bootstrap/shared.ts | 4 +- .../orchestration/bootstrap/zane-project.ts | 8 +-- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts b/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts index 01fd530ca8..4b3bb1f063 100644 --- a/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts +++ b/apps/new-engine-ctl/src/__tests__/bootstrap-zane-project.test.ts @@ -16,6 +16,11 @@ const stackInputsPath = join( repoRoot, "apps/new-engine-ctl/config/stack-inputs.yaml" ) +const projectSlug = "example-project" +const publicDomain = "example.test" +const publicUrlAffix = "-deploy" +const medusaBePublicOrigin = `https://${projectSlug}-medusa-be${publicUrlAffix}.${publicDomain}` +const herbatikaPublicOrigin = `https://${projectSlug}-herbatika${publicUrlAffix}.${publicDomain}` const serviceSlugs = [ "medusa-db", @@ -40,17 +45,26 @@ test("project sync manages Herbatika and current Medusa runtime envs", async () "DC_ZANE_OPERATOR_DB_PREVIEW_APP_PASSWORD_SECRET", "preview-password-secret" ) - vi.stubEnv("DC_STOREFRONT_URL", "https://storefront.example.com") + vi.stubEnv("DC_STOREFRONT_URL", "https://storefront.example.test") vi.stubEnv("DC_STORE_NAME", "Herbatika") + vi.stubEnv( + "DC_STORE_CORS", + "http://localhost:3001,https://storefront.example.test/" + ) + vi.stubEnv("DC_ADMIN_CORS", `http://localhost:5173,${medusaBePublicOrigin}/`) + vi.stubEnv("DC_AUTH_CORS", "http://127.0.0.1:3001") vi.stubEnv("DC_FEATURE_PAYMENT_QR_ENABLED", "1") vi.stubEnv( "DC_GOPAY_WEBHOOK_URL", "http://localhost:9000/hooks/payment/paykit_gopay" ) - vi.stubEnv("DC_HERBATICA_REVIEWS_XML_PATH", "https://example.com/reviews.xml") + vi.stubEnv( + "DC_HERBATICA_REVIEWS_XML_PATH", + "https://assets.example.test/reviews.xml" + ) vi.stubEnv("DC_HERBATIKA_NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY", "") vi.stubEnv("DC_MEDUSA_BE_RESEND_FROM_EMAIL", "") - vi.stubEnv("DC_RESEND_FROM_EMAIL", "noreply@example.com") + vi.stubEnv("DC_RESEND_FROM_EMAIL", "noreply@example.test") const temporaryDirectory = await mkdtemp( join(tmpdir(), "new-engine-zane-project-") @@ -60,13 +74,13 @@ test("project sync manages Herbatika and current Medusa runtime envs", async () await writeFile( inspectJsonPath, JSON.stringify({ - project_slug: "test-engine", + project_slug: projectSlug, environment_name: "production", project_exists: true, environment_exists: true, settings: { - root_domain: "example.com", - app_domain: "zane.example.com", + root_domain: publicDomain, + app_domain: `control.${publicDomain}`, }, shared_variables: [], services: serviceSlugs.map((serviceSlug) => ({ @@ -87,14 +101,14 @@ test("project sync manages Herbatika and current Medusa runtime envs", async () try { const plan = await executeBootstrapZaneProjectPlan({ - projectSlug: "test-engine", + projectSlug, projectDescription: "Test project", environmentName: "production", inspectJsonPath, repositoryUrl: "https://github.com/example/new-engine.git", branchName: "main", - publicDomain: "example.com", - publicUrlAffix: "-zane", + publicDomain, + publicUrlAffix, stackManifestPath, stackInputsPath, phase: "env", @@ -135,14 +149,14 @@ test("project sync manages Herbatika and current Medusa runtime envs", async () ) const planWithN1 = await executeBootstrapZaneProjectPlan({ - projectSlug: "test-engine", + projectSlug, projectDescription: "Test project", environmentName: "production", inspectJsonPath: inspectWithN1JsonPath, repositoryUrl: "https://github.com/example/new-engine.git", branchName: "main", - publicDomain: "example.com", - publicUrlAffix: "-zane", + publicDomain, + publicUrlAffix, stackManifestPath, stackInputsPath, phase: "env", @@ -171,13 +185,15 @@ test("project sync manages Herbatika and current Medusa runtime envs", async () (service) => service.service_id === "medusa-be" ) expect(medusa?.desired_env).toMatchObject({ - STOREFRONT_URL: "https://storefront.example.com", + STOREFRONT_URL: "https://storefront.example.test", STORE_NAME: "Herbatika", + STORE_CORS: `http://localhost:3001,https://storefront.example.test,${herbatikaPublicOrigin}`, + ADMIN_CORS: `http://localhost:5173,${medusaBePublicOrigin}`, + AUTH_CORS: `http://127.0.0.1:3001,${medusaBePublicOrigin}`, FEATURE_PAYMENT_QR_ENABLED: "1", - GOPAY_WEBHOOK_URL: - "https://test-engine-medusa-be-zane.example.com/hooks/payment/paykit_gopay", - HERBATICA_REVIEWS_XML_PATH: "https://example.com/reviews.xml", - RESEND_FROM_EMAIL: "noreply@example.com", + GOPAY_WEBHOOK_URL: `${medusaBePublicOrigin}/hooks/payment/paykit_gopay`, + HERBATICA_REVIEWS_XML_PATH: "https://assets.example.test/reviews.xml", + RESEND_FROM_EMAIL: "noreply@example.test", }) expect(medusa?.desired_env).toHaveProperty( "WORKFLOW_QUEUE_RUNNER_BATCH_SIZE" diff --git a/apps/new-engine-ctl/src/orchestration/bootstrap/shared.ts b/apps/new-engine-ctl/src/orchestration/bootstrap/shared.ts index 5f5734e240..34290b59fc 100644 --- a/apps/new-engine-ctl/src/orchestration/bootstrap/shared.ts +++ b/apps/new-engine-ctl/src/orchestration/bootstrap/shared.ts @@ -96,7 +96,7 @@ export function normalizeOriginUrl(value?: string): string | undefined { return `https://${trimmed}` } -export function preferPublicCsvOrUrl(input: { +export function preferExplicitOrMergeCsv(input: { explicitValue?: string envValue?: string fallbackValue: string @@ -108,7 +108,7 @@ export function preferPublicCsvOrUrl(input: { const envEntries = (input.envValue ?? "") .split(",") .map((entry) => stripTrailingSlash(entry.trim())) - .filter((entry) => entry.length > 0 && !isLoopbackUrl(entry)) + .filter((entry) => entry.length > 0) const values = [ ...envEntries, stripTrailingSlash(input.fallbackValue.trim()), diff --git a/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts b/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts index 6c268084a8..15065d850f 100644 --- a/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts +++ b/apps/new-engine-ctl/src/orchestration/bootstrap/zane-project.ts @@ -19,7 +19,7 @@ import { isLoopbackUrl, literalSource, normalizeOriginUrl, - preferPublicCsvOrUrl, + preferExplicitOrMergeCsv, readJsonFile, serviceGlobalNetworkAliasSource, serviceInternalBucketUrlSource, @@ -1713,21 +1713,21 @@ function buildContext(input: { publicDomain, publicUrlAffix: input.planInput.publicUrlAffix, minioFileUrlOverride: input.planInput.minioFileUrlOverride?.trim() || null, - storeCors: preferPublicCsvOrUrl({ + storeCors: preferExplicitOrMergeCsv({ explicitValue: input.planInput.storeCorsOverride, envValue: process.env.DC_STORE_CORS, fallbackValue: publicDomain ? `https://${input.planInput.projectSlug}-${"herbatika"}${input.planInput.publicUrlAffix}.${publicDomain}` : "https://pending-public-domain.invalid", }), - adminCors: preferPublicCsvOrUrl({ + adminCors: preferExplicitOrMergeCsv({ explicitValue: input.planInput.adminCorsOverride, envValue: process.env.DC_ADMIN_CORS, fallbackValue: publicDomain ? `https://${input.planInput.projectSlug}-${"medusa-be"}${input.planInput.publicUrlAffix}.${publicDomain}` : "https://pending-public-domain.invalid", }), - authCors: preferPublicCsvOrUrl({ + authCors: preferExplicitOrMergeCsv({ explicitValue: input.planInput.authCorsOverride, envValue: process.env.DC_AUTH_CORS, fallbackValue: publicDomain