// 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) } // releaseEnvWrapper sources /app/env (if present) before running the // release recipe, so config vars set via `skiff config` reach the release // phase the same way they reach the running app. $1 is the release's own // checkout, passed positionally rather than string-interpolated so there's // no manual shell-quoting surface. const releaseEnvWrapper = `if [ -r /app/env ]; then set -a; . /app/env; set +a; fi; exec make -B -C "$1" release` // 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 is bmake's compatibility/ // execution-mode flag, not an unconditional-rebuild flag; what actually // guarantees the recipe always runs is `.PHONY: release` in the app's own // Makefile. 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, "sh", "-c", releaseEnvWrapper, "sh", releaseDirInJail) }