▍ humdrum codex / custard v0.3.0
license AGPL-3.0

Expose the deploying commit to deploy commands as {{commit}} / {{short}}

488a5a27eec84a3ac29f97519be349b25f6155fb
humdrum-tiv <45084903+humdrum-tiv@users.noreply.github.com> · 2026-09-01 16:53

parent 4c0bfe36

Expose the deploying commit to deploy commands as {{commit}} / {{short}}

preview builds an export made with `git archive`, which carries no .git, so a
build running in there cannot discover which commit it is. Anything that stamps
itself with a version resolves fine on the developer's machine and ships blank
in the artifact — a failure that is invisible from either side alone, since both
the local build and the deployed page look internally consistent.

custard already knows the commit, so it now hands it over. `expand` replaces
{{commit}} and {{short}} alongside the existing {{url}}, and preview runs its
deploy command through it too, where previously only promote was expanded. url
is empty during preview, so {{url}} there collapses to nothing rather than being
left as a literal.

Found while deploying a static site that prints its own sha in the footer: the
page claimed "unstamped" in production while the local build showed the real
value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

2 files changed

internal/cli/verbs.go +22 −3
@@ -58,8 +58,9 @@
 	if cfg.Deploy.Preview == "" {
 		fail("no `deploy.preview` command in .custard.yaml")
 	}
-	warnVercelUnlinked(dir, cfg.Deploy.Preview)
-	out := step("deploying preview", func() (string, error) { return sh(dir, cfg.Deploy.Preview) })
+	previewCmd := expand(cfg.Deploy.Preview, "", commit)
+	warnVercelUnlinked(dir, previewCmd)
+	out := step("deploying preview", func() (string, error) { return sh(dir, previewCmd) })
 	url := deployURL(out)
 	if url == "" {
 		fail("deploy succeeded but no URL found in output")
@@ -87,7 +88,7 @@ 	cmd := cfg.Deploy.Promote
 	if cmd == "" {
 		cmd = "vercel promote {{url}}"
 	}
-	cmd = strings.ReplaceAll(cmd, "{{url}}", url)
+	cmd = expand(cmd, url, commit)
 	fmt.Println(headStyle.Render("▍ promote") + dimStyle.Render("  "+url))
 	out := step("promoting to production", func() (string, error) { return sh(".", cmd) })
 	prodURL := deployURL(out)
@@ -272,6 +273,24 @@ 	if len(c) >= 8 {
 		return c[:8]
 	}
 	return c
+}
+
+// expand substitutes the template variables available to deploy commands.
+//
+// {{commit}} and {{short}} matter because the build runs against a `git archive`
+// export, which carries no .git — a command in there cannot discover its own
+// commit, so custard has to hand it over. Without this, a build that stamps its
+// own version silently ships a blank one: it resolves fine on the developer's
+// machine and comes out empty in the artifact.
+//
+// url is empty for preview, where no URL exists yet; {{url}} then expands to
+// nothing rather than being left as a literal.
+func expand(cmd, url, commit string) string {
+	return strings.NewReplacer(
+		"{{url}}", url,
+		"{{commit}}", commit,
+		"{{short}}", shortCommit(commit),
+	).Replace(cmd)
 }
 
 // deployCommit resolves the commit to build and deploy: the newest non-empty
internal/cli/verbs_test.go +54 −0
@@ -52,3 +52,57 @@ 			}
 		})
 	}
 }
+
+func TestExpand(t *testing.T) {
+	const sha = "96017987fe1103d2c9b1"
+	cases := []struct {
+		name          string
+		cmd, url, com string
+		want          string
+	}{
+		{
+			name: "url substituted for promote",
+			cmd:  "vercel promote {{url}}",
+			url:  "https://x.vercel.app",
+			com:  sha,
+			want: "vercel promote https://x.vercel.app",
+		},
+		{
+			name: "commit and short both available",
+			cmd:  "SHA={{short}} FULL={{commit}} build",
+			com:  sha,
+			want: "SHA=96017987 FULL=" + sha + " build",
+		},
+		{
+			name: "repeated placeholder replaced everywhere",
+			cmd:  "a {{short}} b {{short}}",
+			com:  sha,
+			want: "a 96017987 b 96017987",
+		},
+		{
+			name: "preview has no url yet, so it expands to nothing",
+			cmd:  "deploy {{url}}",
+			com:  sha,
+			want: "deploy ",
+		},
+		{
+			name: "short commit is left whole when under 8 chars",
+			cmd:  "{{short}}",
+			com:  "abc",
+			want: "abc",
+		},
+		{
+			name: "command without placeholders is untouched",
+			cmd:  "vercel deploy --prebuilt --yes",
+			com:  sha,
+			want: "vercel deploy --prebuilt --yes",
+		},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			if got := expand(c.cmd, c.url, c.com); got != c.want {
+				t.Errorf("expand() = %q, want %q", got, c.want)
+			}
+		})
+	}
+}