From 9481ec692ee36e17a79a86ff11eb00f180833654 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 24 Jul 2026 16:18:04 +0100 Subject: [PATCH 1/2] fix(cli): render SMS/MFA config templates instead of emitting the raw text/template escape (CLI-1971) --- .../src/shared/init/project-init.templates.ts | 4 ++-- .../init/project-init.templates.unit.test.ts | 22 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/shared/init/project-init.templates.ts b/apps/cli/src/shared/init/project-init.templates.ts index f3a41b6db8..2c6a823579 100644 --- a/apps/cli/src/shared/init/project-init.templates.ts +++ b/apps/cli/src/shared/init/project-init.templates.ts @@ -260,7 +260,7 @@ enable_signup = false # If enabled, users need to confirm their phone number before signing in. enable_confirmations = false # Template for sending OTP to users -template = "Your code is {{ \`{{ .Code }}\` }}" +template = "Your code is {{ .Code }}" # Controls the minimum amount of time that must pass before sending another sms otp. max_frequency = "5s" @@ -308,7 +308,7 @@ verify_enabled = false enroll_enabled = false verify_enabled = false otp_length = 6 -template = "Your code is {{ \`{{ .Code }}\` }}" +template = "Your code is {{ .Code }}" max_frequency = "5s" # Configure MFA via WebAuthn diff --git a/apps/cli/src/shared/init/project-init.templates.unit.test.ts b/apps/cli/src/shared/init/project-init.templates.unit.test.ts index 7b58bf1aea..6fb0143d5b 100644 --- a/apps/cli/src/shared/init/project-init.templates.unit.test.ts +++ b/apps/cli/src/shared/init/project-init.templates.unit.test.ts @@ -21,9 +21,18 @@ function readGoTemplate(...segments: ReadonlyArray): string { return normalizeNewlines(readFileSync(join(goCliRoot, ...segments), "utf8")); } +// Go renders its config.toml scaffold through text/template (config.Eject), so an action +// containing a backtick raw string — {{ (backtick){{ .Code }}(backtick) }} in the template +// source — is rendered to the literal string it quotes: {{ .Code }} in the ejected file. +function resolveGoTemplateEscapes(template: string): string { + return template.replace(/\{\{\s*`([^`]*)`\s*\}\}/g, "$1"); +} + describe("project init templates", () => { - it("renders config.toml with the same content as the Go scaffold", () => { - const expected = readGoTemplate("pkg", "config", "templates", "config.toml") + it("renders config.toml with the same content as the Go CLI ejects", () => { + const expected = resolveGoTemplateEscapes( + readGoTemplate("pkg", "config", "templates", "config.toml"), + ) .replace("{{ .ProjectId }}", "demo-project") .replace("{{ .Experimental.OrioleDBVersion }}", "15.1.0.150") // supabase init always opts new projects into pg-delta; the Go template renders @@ -33,6 +42,15 @@ describe("project init templates", () => { expect(normalizeNewlines(renderProjectConfigTemplate("demo-project", true))).toBe(expected); }); + it("renders the SMS and MFA phone OTP templates as GoTrue templates, not raw Go escapes", () => { + const rendered = renderProjectConfigTemplate("demo-project", false); + const otpTemplateLines = rendered.split("\n").filter((line) => line.startsWith("template = ")); + expect(otpTemplateLines).toEqual([ + 'template = "Your code is {{ .Code }}"', + 'template = "Your code is {{ .Code }}"', + ]); + }); + it("enables pg-delta by default in the generated config", () => { const rendered = renderProjectConfigTemplate("demo-project", false); expect(rendered).toContain("[experimental.pgdelta]\nenabled = true"); From 3b76fc960f91156262050bd6cc46dab3944b14b0 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Fri, 24 Jul 2026 16:25:00 +0100 Subject: [PATCH 2/2] test(cli): guard the init config parity suite against unmodeled Go template constructs (CLI-1971) --- .../init/project-init.templates.unit.test.ts | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/cli/src/shared/init/project-init.templates.unit.test.ts b/apps/cli/src/shared/init/project-init.templates.unit.test.ts index 6fb0143d5b..650ef7ec88 100644 --- a/apps/cli/src/shared/init/project-init.templates.unit.test.ts +++ b/apps/cli/src/shared/init/project-init.templates.unit.test.ts @@ -24,22 +24,39 @@ function readGoTemplate(...segments: ReadonlyArray): string { // Go renders its config.toml scaffold through text/template (config.Eject), so an action // containing a backtick raw string — {{ (backtick){{ .Code }}(backtick) }} in the template // source — is rendered to the literal string it quotes: {{ .Code }} in the ejected file. +// This is the only text/template construct emulated here beyond the substituted fields; +// the "models every template action" test below fails loudly if the Go scaffold ever +// gains a construct this suite does not resolve. function resolveGoTemplateEscapes(template: string): string { return template.replace(/\{\{\s*`([^`]*)`\s*\}\}/g, "$1"); } -describe("project init templates", () => { - it("renders config.toml with the same content as the Go CLI ejects", () => { - const expected = resolveGoTemplateEscapes( - readGoTemplate("pkg", "config", "templates", "config.toml"), - ) +// Emulates what Go's config.Eject writes to disk for a fresh `supabase init` project. +function renderExpectedGoEject(): string { + return ( + resolveGoTemplateEscapes(readGoTemplate("pkg", "config", "templates", "config.toml")) .replace("{{ .ProjectId }}", "demo-project") .replace("{{ .Experimental.OrioleDBVersion }}", "15.1.0.150") // supabase init always opts new projects into pg-delta; the Go template renders // this from a flag set only on the init path (false when deriving defaults). - .replace("{{ .Experimental.PgDeltaInitEnabled }}", "true"); + .replace("{{ .Experimental.PgDeltaInitEnabled }}", "true") + ); +} + +describe("project init templates", () => { + it("renders config.toml with the same content as the Go CLI ejects", () => { + expect(normalizeNewlines(renderProjectConfigTemplate("demo-project", true))).toBe( + renderExpectedGoEject(), + ); + }); - expect(normalizeNewlines(renderProjectConfigTemplate("demo-project", true))).toBe(expected); + it("models every template action in the Go scaffold, so parity cannot silently drift", () => { + // After escape resolution and field substitution, the only {{ ... }} occurrences left + // must be the GoTrue OTP placeholders quoted by the backtick escapes. Anything else + // means the Go template gained a construct this suite does not emulate yet — update + // resolveGoTemplateEscapes/renderExpectedGoEject to match config.Eject before shipping. + const unresolvedActions = renderExpectedGoEject().match(/\{\{[^}]*\}\}/g) ?? []; + expect(new Set(unresolvedActions)).toEqual(new Set(["{{ .Code }}"])); }); it("renders the SMS and MFA phone OTP templates as GoTrue templates, not raw Go escapes", () => {