1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
// 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)
}
|