summaryrefslogtreecommitdiff
path: root/internal/release/release.go
blob: fe48aa4059102e19cdabb2ee270a0cb2a4f7d327 (plain)
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
// 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")
}