summaryrefslogtreecommitdiff
path: root/internal/release/release.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/release/release.go')
-rw-r--r--internal/release/release.go52
1 files changed, 52 insertions, 0 deletions
diff --git a/internal/release/release.go b/internal/release/release.go
new file mode 100644
index 0000000..fe48aa4
--- /dev/null
+++ b/internal/release/release.go
@@ -0,0 +1,52 @@
+// Package release runs an app's own release-phase Makefile target, if it
+// declares one - mirroring Heroku's Procfile `release:` line, but as a
+// `release` target in the same Makefile that builds the app. Skiff never
+// interprets what the target does (run migrations, warm a cache,
+// whatever); it only checks whether one is declared and gates the deploy
+// on its exit code.
+package release
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "strings"
+
+ "skiff/internal/sh"
+)
+
+// notDeclared is bmake's exact wording (verified against FreeBSD 15's
+// /usr/bin/make) when asked to build a target with no matching rule -
+// explicit, implicit, or file-on-disk. Skiff is FreeBSD-only, so this
+// doesn't need to also cover GNU make's differently-worded message.
+const notDeclared = "don't know how to make release"
+
+// Declared reports whether dir's Makefile defines a `release` target, by
+// asking bmake itself via a dry run rather than parsing Makefile syntax -
+// robust regardless of how the target is actually defined (directly, via
+// an include, a conditional, a pattern rule). dir is the release's
+// host-side checkout; this runs on the host, before the jail is involved.
+func Declared(ctx context.Context, dir string) (bool, error) {
+ out, err := sh.CombinedOutput(ctx, dir, "make", "-n", "release")
+ if err == nil {
+ return true, nil
+ }
+ if strings.Contains(out, notDeclared) {
+ return false, nil
+ }
+ return false, fmt.Errorf("make -n release: %w\n%s", err, out)
+}
+
+// Run executes the `release` Makefile target as a one-off inside app's
+// already-running jail (the jail holds the process's filesystem and any
+// state the target needs to reach - e.g. the app's own data.db).
+// releaseDirInJail must be the new release's own checkout, jail-relative -
+// not `current`, which still points at the previous release until cutover
+// happens after this returns successfully. -B forces the recipe to run
+// even if a stray file/directory named `release` already exists in the
+// checkout. A non-nil error means the deploy must abort before
+// restart/cutover.
+func Run(ctx context.Context, out io.Writer, app, releaseDirInJail string) error {
+ fmt.Fprintf(out, "==> release: make release (in %s)\n", releaseDirInJail)
+ return sh.Run(ctx, out, "", "jexec", app, "make", "-B", "-C", releaseDirInJail, "release")
+}