feat: add support for specifying buildpacks via app.json

The order of detection is:

- buildpacks:set
- app.json
- .buildpacks
- auto-detected from codebase

Closes #7102
This commit is contained in:
Jose Diaz-Gonzalez
2025-11-23 01:39:14 -05:00
parent 996d882310
commit 5a787af3ae
6 changed files with 71 additions and 10 deletions

View File

@@ -26,6 +26,9 @@ var (
// AppJSON is a struct that represents an app.json file as understood by Dokku
type AppJSON struct {
// Buildpacks is a list of buildpacks to use for the app
Buildpacks []Buildpack `json:"buildpacks"`
// Cron is a list of cron tasks to execute
Cron []CronTask `json:"cron"`
@@ -51,6 +54,12 @@ type AppJSON struct {
} `json:"scripts"`
}
// Buildpack is a struct that represents a single buildpack
type Buildpack struct {
// URL is the URL of the buildpack
URL string `json:"url"`
}
// CronTask is a struct that represents a single cron task from an app.json file
type CronTask struct {
// Command is the command to execute

View File

@@ -8,9 +8,38 @@ import (
"regexp"
"strings"
appjson "github.com/dokku/dokku/plugins/app-json"
"github.com/dokku/dokku/plugins/common"
)
// getBuildpacks returns the buildpacks for a given app
// the order of application is:
// 1. .buildpacks: if it exists, use it
// 2. app.json: if it exists and has buildpacks, use them
// 3. buildpack properties: if they exist, use them
// 4. buildpack detection
func getBuildpacks(appName string) ([]string, error) {
buildpacks, err := common.PropertyListGet("buildpacks", appName, "buildpacks")
if err != nil {
return buildpacks, err
}
appJSON, err := appjson.GetAppJSON(appName)
if err != nil {
return buildpacks, err
}
if len(appJSON.Buildpacks) == 0 {
return buildpacks, nil
}
for _, b := range appJSON.Buildpacks {
buildpacks = append(buildpacks, b.URL)
}
return buildpacks, nil
}
func rewriteBuildpacksFile(sourceWorkDir string) error {
buildpacksPath := filepath.Join(sourceWorkDir, ".buildpacks")
if !common.FileExists(buildpacksPath) {
@@ -27,6 +56,10 @@ func rewriteBuildpacksFile(sourceWorkDir string) error {
continue
}
if strings.HasPrefix(buildpack, "#") {
continue
}
buildpack, err = validBuildpackURL(buildpack)
if err != nil {
return fmt.Errorf("Unable to parse .buildpacks file, line %d: %s", i, err)

View File

@@ -3,6 +3,7 @@ module github.com/dokku/dokku/plugins/buildpacks
go 1.25.5
require (
github.com/dokku/dokku/plugins/app-json v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
github.com/spf13/pflag v1.0.10
)
@@ -12,6 +13,7 @@ require (
github.com/fatih/color v1.19.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -21,9 +23,13 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.10 // indirect
github.com/ryanuber/columnize v2.1.2+incompatible // indirect
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
)
replace github.com/dokku/dokku/plugins/app-json => ../app-json
replace github.com/dokku/dokku/plugins/common => ../common

View File

@@ -10,6 +10,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
@@ -36,6 +38,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a h1:a6TNDN9CgG+cYjaeN8l2mc4kSz2iMiCDQxPEyltUV/I=
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
@@ -53,3 +57,5 @@ golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=

View File

@@ -74,7 +74,7 @@ func TriggerPostDelete(appName string) error {
// TriggerPostExtract writes a .buildpacks file into the app
func TriggerPostExtract(appName string, sourceWorkDir string) error {
buildpacks, err := common.PropertyListGet("buildpacks", appName, "buildpacks")
buildpacks, err := getBuildpacks(appName)
if err != nil {
return nil
}
@@ -91,13 +91,20 @@ func TriggerPostExtract(appName string, sourceWorkDir string) error {
w := bufio.NewWriter(file)
for _, buildpack := range buildpacks {
buildpack, err = validBuildpackURL(buildpack)
if err != nil {
return err
}
fmt.Fprintln(w, buildpack)
}
if err = w.Flush(); err != nil {
return fmt.Errorf("Error writing .buildpacks file: %s", err.Error())
}
file.Chmod(0600)
if err = file.Chmod(0600); err != nil {
return fmt.Errorf("Error setting .buildpacks file permissions: %s", err.Error())
}
return nil
}

View File

@@ -5,14 +5,14 @@ go 1.25.5
require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/cert-manager/cert-manager v1.20.2
github.com/dokku/dokku/plugins/app-json v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/common v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/config v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/cron v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/docker-options v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/logs v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/nginx-vhosts v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/registry v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/app-json v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/config v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/cron v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/docker-options v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/logs v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/nginx-vhosts v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/registry v0.0.0-00010101000000-000000000000
github.com/fatih/color v1.19.0
github.com/fluxcd/pkg/kustomize v1.29.0
github.com/go-openapi/jsonpointer v0.23.0