diff --git a/README.md b/README.md index 1859ad82..5ed78add 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ wires the repo. Live pitch: ## Docs - **[Public docs](https://simpros.github.io/sprout/docs/index.html)** — guides and reference - ([source](docs/site/index.html) · [llms.txt](llms.txt)) + ([source](docs/) · [llms.txt](llms.txt)) - [Getting started](docs/getting-started.md) — first preview in one sitting - [Adopting a repo](docs/adopting-a-repo.md) — write `.sprout.yaml`, migrate at startup - [CI integration](docs/ci-integration.md) — GitHub / GitLab wiring, reset diff --git a/apps/cli/src/yaml.test.ts b/apps/cli/src/yaml.test.ts index 238e9581..09c8a3b4 100644 --- a/apps/cli/src/yaml.test.ts +++ b/apps/cli/src/yaml.test.ts @@ -768,11 +768,11 @@ preview: }); expect(base(" path: /health\n interval: 2x\n timeout: 120s\n expect: 200\n")).toEqual({ ok: false, - error: "health.interval is invalid (expected Ns, e.g. 2s)", + error: "health.interval is invalid (expected e.g. 2s, 30m, 2h, 7d)", }); expect(base(" path: /health\n interval: 2s\n timeout: 0s\n expect: 200\n")).toEqual({ ok: false, - error: "health.timeout is invalid (expected Ns, e.g. 2s)", + error: "health.timeout is invalid (expected e.g. 2s, 30m, 2h, 7d)", }); expect(base(" path: /health\n interval: 2s\n timeout: 120s\n expect: 99\n")).toEqual({ ok: false, diff --git a/apps/cli/src/yaml.ts b/apps/cli/src/yaml.ts index 490fadbb..41d6b263 100644 --- a/apps/cli/src/yaml.ts +++ b/apps/cli/src/yaml.ts @@ -3,6 +3,7 @@ import { dbRolesIssueMessage, dbSpecIssueMessage, governanceIssueMessage, + healthIssueMessage, isServicePort, labelIssueMessage, mailSpecIssueMessage, @@ -15,6 +16,7 @@ import { parsePreviewGovernanceField, parsePreviewVolumes, parseServiceEnvMap, + previewEnvIssueMessage, previewVolumeIssueMessage, requiresDatabase, resolveDbRoles, @@ -106,19 +108,6 @@ function unknownKey(path: string): Result { return { ok: false, error: `unknown key: ${path}` }; } -function healthIssueMessage(issue: HealthIssue): string { - switch (issue.code) { - case "invalid_health_path": - return "health.path must start with /"; - case "invalid_health_interval": - return "health.interval is invalid (expected Ns, e.g. 2s)"; - case "invalid_health_timeout": - return "health.timeout is invalid (expected Ns, e.g. 2s)"; - case "invalid_health_expect": - return "health.expect must be a number between 100 and 599"; - } -} - function parseHostnameField( raw: string, label: string, @@ -152,25 +141,7 @@ function parsePreviewEnv( const parsed = parsePreviewEnvForProvider(raw, provider); if (!parsed.ok) { - const { issue } = parsed; - switch (issue.code) { - case "unknown_env_key": - return unknownKey(`preview.env.${issue.key}`); - case "empty_env_target": - return { ok: false, error: `preview.env.${issue.key} is required` }; - case "invalid_env_target": - return { ok: false, error: `preview.env.${issue.key} is invalid` }; - case "env_target_collision": - return { - ok: false, - error: `preview.env: target collision: ${issue.target}`, - }; - case "env_requires_provider": - return { - ok: false, - error: `preview.env.${issue.key} requires db.provider ${issue.home}`, - }; - } + return { ok: false, error: previewEnvIssueMessage("preview.env", parsed.issue) }; } return { ok: true, value: parsed.value }; } @@ -635,7 +606,8 @@ export function parseSproutYaml(raw: string): Result { expect: parsed.health.expect, }); if (!resolved.ok) { - return { ok: false, error: healthIssueMessage(resolved.issue) }; + const issue: HealthIssue = resolved.issue; + return { ok: false, error: healthIssueMessage(issue) }; } value.health = { path: resolved.value.path, diff --git a/apps/server/src/app-deployment/pg-env.ts b/apps/server/src/app-deployment/pg-env.ts index 3c55d705..40bc9d10 100644 --- a/apps/server/src/app-deployment/pg-env.ts +++ b/apps/server/src/app-deployment/pg-env.ts @@ -6,9 +6,8 @@ import { } from "@sprout/preview-env"; import { deriveRestrictedPassword, + companionRoleLimitMessage, companionRoleName, - COMPANION_ROLE_SUFFIX, - PG_IDENT_MAX, } from "@sprout/preview-db"; import { applyEnvRemap } from "./env-remap.ts"; @@ -35,9 +34,7 @@ export function pgConnectionEnv( if (roles === "dual") { const restrictedUser = companionRoleName(dbName); if (restrictedUser === null) { - throw new Error( - `companion role name exceeds Postgres identifier limit (${PG_IDENT_MAX}): ${dbName}${COMPANION_ROLE_SUFFIX}`, - ); + throw new Error(companionRoleLimitMessage(dbName)); } const restrictedPassword = deriveRestrictedPassword(pg.password, dbName); fields.push(["PGAPPUSER", restrictedUser]); diff --git a/apps/server/src/http/deploy.ts b/apps/server/src/http/deploy.ts index c836caac..2cce6a50 100644 --- a/apps/server/src/http/deploy.ts +++ b/apps/server/src/http/deploy.ts @@ -18,6 +18,7 @@ import { parsePreviewVolumes, parseServiceEnvMap, previewAuthMode, + previewEnvIssueMessage, previewVolumeIssueMessage, requiresDatabase, resolveDbRoles, @@ -238,7 +239,7 @@ function resolveDeploySpecs( ok: false, status: 422, error: "invalid_env_for_provider", - detail: `preview.env.${connectionEnv.issue.key} requires db.provider ${connectionEnv.issue.home}`, + detail: previewEnvIssueMessage("preview.env", connectionEnv.issue), }; } if (connectionEnv.issue.code === "empty_env_target") { diff --git a/apps/server/src/preview-db/names.ts b/apps/server/src/preview-db/names.ts index 7e584b3e..533092f8 100644 --- a/apps/server/src/preview-db/names.ts +++ b/apps/server/src/preview-db/names.ts @@ -1,6 +1,8 @@ -import { COMPANION_ROLE_SUFFIX, PG_IDENT_MAX } from "@sprout/preview-db"; +import { COMPANION_ROLE_SUFFIX, PG_IDENT_MAX, previewDbName } from "@sprout/preview-db"; import { SERVICE_NAME_RE, type DbRolesMode } from "@sprout/preview-env"; +export { previewDbName }; + const PREVIEW_DB_NAME_RE = /^sprout_([a-z][a-z0-9]*)_pr([1-9][0-9]*)$/; /** Max dbName length so _app fits Postgres NAMEDATALEN (dual mode). */ @@ -14,14 +16,16 @@ export type IdentifierError = | "invalid_pr_id" | "invalid_service_name"; +function validateName(value: string, error: IdentifierError): IdentifierError | null { + return SERVICE_NAME_RE.test(value) ? null : error; +} + export function validateSlug(slug: string): IdentifierError | null { - if (!SERVICE_NAME_RE.test(slug)) return "invalid_slug"; - return null; + return validateName(slug, "invalid_slug"); } export function validateServiceName(name: string): IdentifierError | null { - if (!SERVICE_NAME_RE.test(name)) return "invalid_service_name"; - return null; + return validateName(name, "invalid_service_name"); } export function validatePrId(prId: number): IdentifierError | null { @@ -46,10 +50,6 @@ export function validatePreviewIdentity( return null; } -export function previewDbName(slug: string, prId: number): string { - return `sprout_${slug}_pr${prId}`; -} - export function isPreviewDbName(dbName: string): boolean { return ( PREVIEW_DB_NAME_RE.test(dbName) && diff --git a/apps/server/src/preview/naming.ts b/apps/server/src/preview/naming.ts index a770b635..65ff0377 100644 --- a/apps/server/src/preview/naming.ts +++ b/apps/server/src/preview/naming.ts @@ -1,14 +1,13 @@ // Container names only; volume names live in @sprout/preview-env. +import { previewContainerName } from "@sprout/preview-env"; import type { CatalogContainer } from "../docker/port.ts"; +export { previewContainerName }; + const PREVIEW_APP_CONTAINER_RE = /^sprout-([a-zA-Z0-9]+)-pr-(\d+)$/; const PREVIEW_SERVICE_CONTAINER_RE = /^sprout-([a-zA-Z0-9]+)-pr-(\d+)-svc-([a-zA-Z0-9]+)$/; -export function previewContainerName(slug: string, prId: number): string { - return `sprout-${slug}-pr-${prId}`; -} - export function previewServiceContainerName( slug: string, prId: number, diff --git a/docs/adopting-a-repo.md b/docs/adopting-a-repo.md index 57542887..dd69c99c 100644 --- a/docs/adopting-a-repo.md +++ b/docs/adopting-a-repo.md @@ -47,7 +47,7 @@ value grammar, merge order) are the contract. Seeding order lives in | `db.path` | no | `/data` | Container directory the SQLite volume mounts at (`sqlite` only). | | `db.file` | no | `preview.db` | SQLite file name inside `db.path` (`sqlite` only). | | `health.path` | when seeding | `/health` | HTTP path the gateway polls on the Postgres-network container IP. | -| `health.interval` | when seeding | `2s` | Poll interval (`Ns` form; malformed durations fail at manifest parse). | +| `health.interval` | when seeding | `2s` | Poll interval (duration like `2s`, `30m`, `2h`, `7d`; malformed durations fail at manifest parse). | | `health.timeout` | when seeding | `120s` | How long the gateway polls before `health_timeout`. Never starts the seed. | | `health.expect` | when seeding | `200` | Expected status (100–599). Gates the after-healthy seed hook. | | `build.dockerfile` | no | `Dockerfile` | App Dockerfile for `sprout ci preview`. An empty `build: {}` takes the default. | diff --git a/e2e/harness/docker.ts b/e2e/harness/docker.ts index a4720697..9a892c2f 100644 --- a/e2e/harness/docker.ts +++ b/e2e/harness/docker.ts @@ -1,11 +1,13 @@ -import { dataVolumeName, sqliteVolumeName } from "@sprout/preview-env"; +import { + dataVolumeName, + previewContainerName, + sqliteVolumeName, +} from "@sprout/preview-env"; import { run } from "./exec.ts"; -export { dataVolumeName, sqliteVolumeName }; +export { dataVolumeName, previewContainerName, sqliteVolumeName }; -export function previewAppContainerName(slug: string, prId: number): string { - return `sprout-${slug}-pr-${prId}`; -} +export const previewAppContainerName = previewContainerName; async function dockerText(args: string[]): Promise { const { stdout } = await run(["docker", ...args]); diff --git a/e2e/lifecycle.test.ts b/e2e/lifecycle.test.ts index a86a940f..af2ee723 100644 --- a/e2e/lifecycle.test.ts +++ b/e2e/lifecycle.test.ts @@ -1,4 +1,5 @@ import { createApiClient } from "@sprout/api-client"; +import { previewDbName } from "@sprout/preview-db"; import { describe, expect, test } from "bun:test"; import { COMPOSE_E2E_ENV_PATH, @@ -103,7 +104,7 @@ describe.skipIf(!enabled)("preview lifecycle", () => { expect(env.has("PGUSER")).toBe(false); expect(env.has("PGPASSWORD")).toBe(false); - const dbName = `sprout_${e2eConfig.slug}_pr${prId}`; + const dbName = previewDbName(e2eConfig.slug, prId); expect(await companionRoleExists(dbName)).toBe(true); } finally { const torn = await client.v1.teardown.post({ @@ -146,7 +147,7 @@ describe.skipIf(!enabled)("preview lifecycle", () => { expect(env.has("PGAPPUSER")).toBe(false); expect(env.has("PGAPPPASSWORD")).toBe(false); - const dbName = `sprout_${e2eConfig.slug}_pr${prId}`; + const dbName = previewDbName(e2eConfig.slug, prId); expect(await companionRoleExists(dbName)).toBe(false); } finally { const torn = await client.v1.teardown.post({ diff --git a/packages/preview-db/src/index.ts b/packages/preview-db/src/index.ts index 1ef478b7..a651de75 100644 --- a/packages/preview-db/src/index.ts +++ b/packages/preview-db/src/index.ts @@ -8,12 +8,14 @@ export { } from "./ensure-role.ts"; export { COMPANION_ROLE_SUFFIX, + companionRoleLimitMessage, companionRoleName, deriveRestrictedPassword, dropRestrictedRole, ensureRestrictedRole, PG_IDENT_MAX, } from "./restricted-role.ts"; +export { previewDbName } from "./preview-names.ts"; export { isWorktreeInputError, } from "./errors.ts"; diff --git a/packages/preview-db/src/preview-names.ts b/packages/preview-db/src/preview-names.ts new file mode 100644 index 00000000..6f2ac834 --- /dev/null +++ b/packages/preview-db/src/preview-names.ts @@ -0,0 +1,3 @@ +export function previewDbName(slug: string, prId: number): string { + return `sprout_${slug}_pr${prId}`; +} diff --git a/packages/preview-db/src/restricted-role.test.ts b/packages/preview-db/src/restricted-role.test.ts index fb85e4e8..1dfbd38c 100644 --- a/packages/preview-db/src/restricted-role.test.ts +++ b/packages/preview-db/src/restricted-role.test.ts @@ -4,6 +4,7 @@ import { ensureDatabase } from "./catalog.ts"; import { assertSafeRole, ensureLoginRole } from "./ensure-role.ts"; import { dockerAvailable, startTempPostgres } from "./postgres-it.ts"; import { + companionRoleLimitMessage, companionRoleName, COMPANION_ROLE_SUFFIX, deriveRestrictedPassword, @@ -11,6 +12,7 @@ import { ensureRestrictedRole, PG_IDENT_MAX, } from "./restricted-role.ts"; +import { previewDbName } from "./preview-names.ts"; const hasDocker = await dockerAvailable(); @@ -21,6 +23,16 @@ describe("companionRoleName / deriveRestrictedPassword", () => { ); }); + test("limit message names the db and the identifier budget", () => { + expect(companionRoleLimitMessage("sprout_myapp_pr1")).toBe( + `companion role name exceeds Postgres identifier limit (${PG_IDENT_MAX}): sprout_myapp_pr1${COMPANION_ROLE_SUFFIX}`, + ); + }); + + test("previewDbName builds the canonical database name", () => { + expect(previewDbName("widgets", 42)).toBe("sprout_widgets_pr42"); + }); + test("returns null for companion names longer than Postgres NAMEDATALEN", () => { const tooLongDb = "a".repeat(PG_IDENT_MAX - COMPANION_ROLE_SUFFIX.length + 1); expect(companionRoleName(tooLongDb)).toBeNull(); diff --git a/packages/preview-db/src/restricted-role.ts b/packages/preview-db/src/restricted-role.ts index bed91844..b85d2105 100644 --- a/packages/preview-db/src/restricted-role.ts +++ b/packages/preview-db/src/restricted-role.ts @@ -14,6 +14,10 @@ export function companionRoleName(dbName: string): string | null { return role; } +export function companionRoleLimitMessage(dbName: string): string { + return `companion role name exceeds Postgres identifier limit (${PG_IDENT_MAX}): ${dbName}${COMPANION_ROLE_SUFFIX}`; +} + export function deriveRestrictedPassword( ownerPassword: string, dbName: string, @@ -30,9 +34,7 @@ export async function ensureRestrictedRole( assertSafeRole(opts.dbName); const role = companionRoleName(opts.dbName); if (role === null) { - throw new Error( - `companion role name exceeds Postgres identifier limit (${PG_IDENT_MAX}): ${opts.dbName}${COMPANION_ROLE_SUFFIX}`, - ); + throw new Error(companionRoleLimitMessage(opts.dbName)); } assertSafeRole(role); const password = deriveRestrictedPassword(opts.ownerPassword, opts.dbName); diff --git a/packages/preview-env/src/env-keys.ts b/packages/preview-env/src/env-keys.ts index c98386b4..9c181711 100644 --- a/packages/preview-env/src/env-keys.ts +++ b/packages/preview-env/src/env-keys.ts @@ -87,6 +87,30 @@ export type PreviewEnvIssue = priorKey: string; }; +export type PreviewEnvProviderIssue = { + code: "env_requires_provider"; + key: CanonicalEnvKey; + home: DbProvider; +}; + +export function previewEnvIssueMessage( + path: string, + issue: PreviewEnvIssue | PreviewEnvProviderIssue, +): string { + switch (issue.code) { + case "unknown_env_key": + return `unknown key: ${path}.${issue.key}`; + case "empty_env_target": + return `${path}.${issue.key} is required`; + case "invalid_env_target": + return `${path}.${issue.key} is invalid`; + case "env_target_collision": + return `${path}: target collision: ${issue.target}`; + case "env_requires_provider": + return `${path}.${issue.key} requires db.provider ${issue.home}`; + } +} + export function parsePreviewEnvMap( raw: Record | undefined, ): diff --git a/packages/preview-env/src/health.test.ts b/packages/preview-env/src/health.test.ts index 3354158b..d414deb7 100644 --- a/packages/preview-env/src/health.test.ts +++ b/packages/preview-env/src/health.test.ts @@ -1,23 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { - DEFAULT_HEALTH, - parseDurationMs, - resolveHealthSpec, -} from "./health.ts"; - -describe("parseDurationMs", () => { - test("parses second durations", () => { - expect(parseDurationMs("2s")).toBe(2000); - expect(parseDurationMs("120s")).toBe(120_000); - }); - - test("rejects non-second forms and non-positive", () => { - expect(parseDurationMs("2")).toBeNull(); - expect(parseDurationMs("2ms")).toBeNull(); - expect(parseDurationMs("")).toBeNull(); - expect(parseDurationMs("0s")).toBeNull(); - }); -}); +import { DEFAULT_HEALTH, resolveHealthSpec } from "./health.ts"; describe("resolveHealthSpec", () => { test("defaults when omitted", () => { @@ -46,6 +28,25 @@ describe("resolveHealthSpec", () => { }); }); + test("accepts the shared duration grammar beyond seconds", () => { + expect( + resolveHealthSpec({ + path: "/health", + interval: "30m", + timeout: "2h", + expect: 200, + }), + ).toEqual({ + ok: true, + value: { + path: "/health", + intervalMs: 30 * 60_000, + timeoutMs: 2 * 3_600_000, + expectStatus: 200, + }, + }); + }); + test("rejects bad path, duration, and expect", () => { expect( resolveHealthSpec({ diff --git a/packages/preview-env/src/health.ts b/packages/preview-env/src/health.ts index ceea4593..988a1656 100644 --- a/packages/preview-env/src/health.ts +++ b/packages/preview-env/src/health.ts @@ -1,3 +1,5 @@ +import { parseDurationMs } from "./governance.ts"; + export type HealthSpec = { path: string; intervalMs: number; @@ -25,12 +27,17 @@ export type HealthIssue = | { code: "invalid_health_expect" } | { code: "invalid_health_path" }; -export function parseDurationMs(raw: string): number | null { - const match = /^(\d+)s$/.exec(raw.trim()); - if (!match) return null; - const seconds = Number(match[1]); - if (seconds <= 0) return null; - return seconds * 1000; +export function healthIssueMessage(issue: HealthIssue): string { + switch (issue.code) { + case "invalid_health_path": + return "health.path must start with /"; + case "invalid_health_interval": + return "health.interval is invalid (expected e.g. 2s, 30m, 2h, 7d)"; + case "invalid_health_timeout": + return "health.timeout is invalid (expected e.g. 2s, 30m, 2h, 7d)"; + case "invalid_health_expect": + return "health.expect must be a number between 100 and 599"; + } } export function resolveHealthSpec( diff --git a/packages/preview-env/src/index.test.ts b/packages/preview-env/src/index.test.ts index 88a72b08..be0f36ac 100644 --- a/packages/preview-env/src/index.test.ts +++ b/packages/preview-env/src/index.test.ts @@ -11,7 +11,10 @@ import { import { OWNER_ENV_KEYS, PREVIEW_ENV_KEYS, + healthIssueMessage, parsePreviewEnvForProvider, + previewContainerName, + previewEnvIssueMessage, } from "./index.ts"; describe("env key partitions", () => { @@ -211,3 +214,55 @@ describe("parsePreviewEnvForProvider", () => { ); }); }); + +describe("previewEnvIssueMessage", () => { + test("covers every issue code under one path", () => { + expect(previewEnvIssueMessage("preview.env", { code: "unknown_env_key", key: "REDIS_URL" })).toBe( + "unknown key: preview.env.REDIS_URL", + ); + expect(previewEnvIssueMessage("preview.env", { code: "empty_env_target", key: "PGHOST" })).toBe( + "preview.env.PGHOST is required", + ); + expect(previewEnvIssueMessage("preview.env", { code: "invalid_env_target", key: "PGHOST" })).toBe( + "preview.env.PGHOST is invalid", + ); + expect( + previewEnvIssueMessage("preview.env", { + code: "env_target_collision", + key: "PGPORT", + target: "DATABASE_HOST", + priorKey: "PGHOST", + }), + ).toBe("preview.env: target collision: DATABASE_HOST"); + expect( + previewEnvIssueMessage("preview.env", { + code: "env_requires_provider", + key: "PGHOST", + home: "postgres", + }), + ).toBe("preview.env.PGHOST requires db.provider postgres"); + }); +}); + +describe("healthIssueMessage", () => { + test("names each health field", () => { + expect(healthIssueMessage({ code: "invalid_health_path" })).toBe( + "health.path must start with /", + ); + expect(healthIssueMessage({ code: "invalid_health_interval" })).toContain( + "health.interval", + ); + expect(healthIssueMessage({ code: "invalid_health_timeout" })).toContain( + "health.timeout", + ); + expect(healthIssueMessage({ code: "invalid_health_expect" })).toContain( + "health.expect", + ); + }); +}); + +describe("previewContainerName", () => { + test("builds the canonical app container name", () => { + expect(previewContainerName("widgets", 7)).toBe("sprout-widgets-pr-7"); + }); +}); diff --git a/packages/preview-env/src/index.ts b/packages/preview-env/src/index.ts index 3f24227a..1ef734bd 100644 --- a/packages/preview-env/src/index.ts +++ b/packages/preview-env/src/index.ts @@ -1,9 +1,9 @@ import type { DbProvider } from "./db.ts"; import { envProviderMismatch, parsePreviewEnvMap } from "./env-keys.ts"; import type { - CanonicalEnvKey, PreviewEnvIssue, PreviewEnvMap, + PreviewEnvProviderIssue, } from "./env-keys.ts"; export { @@ -14,7 +14,11 @@ export { type PreviewAuthSpec, } from "./auth.ts"; -export { OWNER_ENV_KEYS, PREVIEW_ENV_KEYS } from "./env-keys.ts"; +export { + OWNER_ENV_KEYS, + PREVIEW_ENV_KEYS, + previewEnvIssueMessage, +} from "./env-keys.ts"; export type { CanonicalEnvKey, @@ -24,19 +28,13 @@ export type { PreviewEnvMap, } from "./env-keys.ts"; -type EnvProviderIssue = { - code: "env_requires_provider"; - key: CanonicalEnvKey; - home: DbProvider; -}; - /** Parse plus provider-scope check: the single entry both CLI and server call. */ export function parsePreviewEnvForProvider( raw: Record | undefined, provider: DbProvider, ): | { ok: true; value: PreviewEnvMap | undefined } - | { ok: false; issue: PreviewEnvIssue | EnvProviderIssue } { + | { ok: false; issue: PreviewEnvIssue | PreviewEnvProviderIssue } { const parsed = parsePreviewEnvMap(raw); if (!parsed.ok) return parsed; const mismatch = envProviderMismatch(parsed.value, provider); @@ -58,6 +56,7 @@ export { export { DEFAULT_HEALTH, + healthIssueMessage, resolveHealthSpec, type HealthIssue, type HealthRequest, @@ -84,6 +83,7 @@ export { dataVolumeName, parseDataVolumeName, parseSqliteVolumeName, + previewContainerName, sqliteVolumeName, type DataVolumeRef, } from "./naming.ts"; @@ -108,7 +108,6 @@ export { export { parsePreviewVolumes, previewVolumeIssueMessage, - type PreviewVolumeIssue, } from "./volumes.ts"; export { diff --git a/packages/preview-env/src/naming.ts b/packages/preview-env/src/naming.ts index afacf8be..dbf44d00 100644 --- a/packages/preview-env/src/naming.ts +++ b/packages/preview-env/src/naming.ts @@ -1,3 +1,9 @@ +/** App container per preview: the single name both the gateway and the + * acceptance harness address. */ +export function previewContainerName(slug: string, prId: number): string { + return `sprout-${slug}-pr-${prId}`; +} + /** -sqlite suffix stays outside the container and Postgres catalogs. */ export function sqliteVolumeName(slug: string, prId: number): string { return `sprout-${slug}-pr-${prId}-sqlite`; diff --git a/packages/preview-env/src/volumes.test.ts b/packages/preview-env/src/volumes.test.ts index 62d96d44..8f78c687 100644 --- a/packages/preview-env/src/volumes.test.ts +++ b/packages/preview-env/src/volumes.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test"; -import type { PreviewVolumeIssue } from "./index.ts"; import { parsePreviewVolumes, previewVolumeIssueMessage, + type PreviewVolumeIssue, } from "./volumes.ts"; describe("parsePreviewVolumes", () => {