fix: remove buildpacks from dockerfile test app and add tests for app.json buildpacks

The buildpacks entry in tests/apps/dockerfile/app.json caused
the post-extract trigger to write a .buildpacks file during
Dockerfile-based deploys, breaking builder detection. Remove
the entry and add Go unit tests for getBuildpacks/validBuildpackURL,
a bats integration test for deploying with app.json buildpacks,
and documentation for the new feature.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-18 05:43:44 -04:00
parent 44dbbe6a34
commit b0b11bcf26
8 changed files with 214 additions and 6 deletions

View File

@@ -5,6 +5,25 @@
> [!IMPORTANT]
> While the `app.json` format used by Dokku is based on the one [supported by Heroku](https://devcenter.heroku.com/articles/app-json-schema), not all Heroku functionality is supported by Dokku.
## Buildpacks
```json
{
"buildpacks": [
{
"url": "heroku/python"
},
{
"url": "https://github.com/heroku/heroku-buildpack-nodejs.git"
}
]
}
```
(list, optional) A list of buildpacks to use when deploying the app. Each entry is an object with a `url` property. Buildpacks specified via the `buildpacks:add` or `buildpacks:set` commands take precedence over those specified in `app.json`. Buildpack URLs may use the shorthand format (e.g., `heroku/python`) which will be expanded to the full GitHub URL.
- `url`: (string, required) The URL or shorthand reference of the buildpack.
## Cron
```json

View File

@@ -130,6 +130,24 @@ This will use the latest commit on the `master` branch of the specified buildpac
dokku buildpacks:set node-js-app https://github.com/heroku/heroku-buildpack-nodejs#v87
```
### Specifying buildpacks via app.json
Buildpacks can also be specified in the `app.json` file at the root of your repository:
```json
{
"buildpacks": [
{
"url": "heroku/python"
}
]
}
```
Buildpacks configured via `buildpacks:add` or `buildpacks:set` commands take precedence over those specified in `app.json`. If no buildpacks are configured via commands and `app.json` contains a `buildpacks` entry, those buildpacks will be used.
Shorthand buildpack references (e.g., `heroku/python`) are expanded to full GitHub URLs automatically.
### Displaying buildpack reports for an app
You can get a report about the app's buildpacks status using the `buildpacks:report` command:

View File

@@ -0,0 +1,141 @@
package buildpacks
import (
"os"
"path/filepath"
"testing"
. "github.com/onsi/gomega"
)
func setupTestEnvironment(t *testing.T) string {
t.Helper()
tmpDir, err := os.MkdirTemp("", "buildpacks-test")
if err != nil {
t.Fatal(err)
}
os.Setenv("DOKKU_LIB_ROOT", tmpDir)
os.Setenv("PLUGIN_PATH", "/var/lib/dokku/plugins")
os.Setenv("PLUGIN_ENABLED_PATH", "/var/lib/dokku/plugins/enabled")
return tmpDir
}
func teardownTestEnvironment(tmpDir string) {
os.RemoveAll(tmpDir)
}
func TestValidBuildpackURL(t *testing.T) {
RegisterTestingT(t)
url, err := validBuildpackURL("")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Must specify a buildpack"))
url, err = validBuildpackURL("heroku/nodejs")
Expect(err).NotTo(HaveOccurred())
Expect(url).To(Equal("https://github.com/heroku/heroku-buildpack-nodejs.git"))
url, err = validBuildpackURL("heroku/python")
Expect(err).NotTo(HaveOccurred())
Expect(url).To(Equal("https://github.com/heroku/heroku-buildpack-python.git"))
url, err = validBuildpackURL("heroku-community/apt")
Expect(err).NotTo(HaveOccurred())
Expect(url).To(Equal("https://github.com/heroku/heroku-buildpack-apt.git"))
url, err = validBuildpackURL("https://github.com/heroku/heroku-buildpack-nodejs.git")
Expect(err).NotTo(HaveOccurred())
Expect(url).To(Equal("https://github.com/heroku/heroku-buildpack-nodejs.git"))
url, err = validBuildpackURL("https://github.com/heroku/heroku-buildpack-nodejs")
Expect(err).NotTo(HaveOccurred())
Expect(url).To(Equal("https://github.com/heroku/heroku-buildpack-nodejs"))
url, err = validBuildpackURL("git://github.com/heroku/heroku-buildpack-nodejs.git")
Expect(err).NotTo(HaveOccurred())
Expect(url).To(Equal("git://github.com/heroku/heroku-buildpack-nodejs.git"))
_, err = validBuildpackURL("nodejs")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Invalid buildpack specified"))
_, err = validBuildpackURL("/nodejs")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Invalid buildpack specified"))
}
func TestGetBuildpacksEmpty(t *testing.T) {
RegisterTestingT(t)
tmpDir := setupTestEnvironment(t)
defer teardownTestEnvironment(tmpDir)
buildpacks, err := getBuildpacks("test-app")
Expect(err).NotTo(HaveOccurred())
Expect(buildpacks).To(BeEmpty())
}
func TestGetBuildpacksFromProperties(t *testing.T) {
RegisterTestingT(t)
tmpDir := setupTestEnvironment(t)
defer teardownTestEnvironment(tmpDir)
propDir := filepath.Join(tmpDir, "config", "buildpacks", "test-app")
Expect(os.MkdirAll(propDir, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(propDir, "buildpacks"), []byte("https://github.com/heroku/heroku-buildpack-nodejs.git\nhttps://github.com/heroku/heroku-buildpack-python.git\n"), 0644)).To(Succeed())
buildpacks, err := getBuildpacks("test-app")
Expect(err).NotTo(HaveOccurred())
Expect(buildpacks).To(HaveLen(2))
Expect(buildpacks[0]).To(Equal("https://github.com/heroku/heroku-buildpack-nodejs.git"))
Expect(buildpacks[1]).To(Equal("https://github.com/heroku/heroku-buildpack-python.git"))
}
func TestGetBuildpacksFromAppJSON(t *testing.T) {
RegisterTestingT(t)
tmpDir := setupTestEnvironment(t)
defer teardownTestEnvironment(tmpDir)
appJSONDir := filepath.Join(tmpDir, "data", "app-json", "test-app")
Expect(os.MkdirAll(appJSONDir, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(appJSONDir, "app.json"), []byte(`{"buildpacks": [{"url": "https://github.com/heroku/heroku-buildpack-nodejs.git"}]}`), 0644)).To(Succeed())
buildpacks, err := getBuildpacks("test-app")
Expect(err).NotTo(HaveOccurred())
Expect(buildpacks).To(HaveLen(1))
Expect(buildpacks[0]).To(Equal("https://github.com/heroku/heroku-buildpack-nodejs.git"))
}
func TestGetBuildpacksPropertiesOverrideAppJSON(t *testing.T) {
RegisterTestingT(t)
tmpDir := setupTestEnvironment(t)
defer teardownTestEnvironment(tmpDir)
propDir := filepath.Join(tmpDir, "config", "buildpacks", "test-app")
Expect(os.MkdirAll(propDir, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(propDir, "buildpacks"), []byte("https://github.com/heroku/heroku-buildpack-ruby.git\n"), 0644)).To(Succeed())
appJSONDir := filepath.Join(tmpDir, "data", "app-json", "test-app")
Expect(os.MkdirAll(appJSONDir, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(appJSONDir, "app.json"), []byte(`{"buildpacks": [{"url": "https://github.com/heroku/heroku-buildpack-nodejs.git"}]}`), 0644)).To(Succeed())
buildpacks, err := getBuildpacks("test-app")
Expect(err).NotTo(HaveOccurred())
Expect(buildpacks).To(HaveLen(1))
Expect(buildpacks[0]).To(Equal("https://github.com/heroku/heroku-buildpack-ruby.git"))
}
func TestGetBuildpacksAppJSONEmptyArray(t *testing.T) {
RegisterTestingT(t)
tmpDir := setupTestEnvironment(t)
defer teardownTestEnvironment(tmpDir)
appJSONDir := filepath.Join(tmpDir, "data", "app-json", "test-app")
Expect(os.MkdirAll(appJSONDir, 0755)).To(Succeed())
Expect(os.WriteFile(filepath.Join(appJSONDir, "app.json"), []byte(`{"buildpacks": []}`), 0644)).To(Succeed())
buildpacks, err := getBuildpacks("test-app")
Expect(err).NotTo(HaveOccurred())
Expect(buildpacks).To(BeEmpty())
}

View File

@@ -11,6 +11,7 @@ require (
require (
github.com/alexellis/go-execute/v2 v2.2.1 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/google/go-cmp v0.7.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
@@ -18,15 +19,19 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/melbahja/goph v1.5.0 // indirect
github.com/onsi/gomega v1.39.1 // indirect
github.com/otiai10/copy v1.14.1 // indirect
github.com/otiai10/mint v1.6.3 // indirect
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
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
)

View File

@@ -55,6 +55,7 @@ golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=

View File

@@ -149,6 +149,7 @@ ci-go-coverage:
@$(MAKE) ci-go-coverage-plugin PLUGIN_NAME=common
@$(MAKE) ci-go-coverage-plugin PLUGIN_NAME=config
@$(MAKE) ci-go-coverage-plugin PLUGIN_NAME=network
@$(MAKE) ci-go-coverage-plugin PLUGIN_NAME=buildpacks
ci-go-coverage-plugin:
mkdir -p test-results/coverage
@@ -177,9 +178,10 @@ go-tests:
@$(MAKE) go-test-plugin PLUGIN_NAME=common
@$(MAKE) go-test-plugin PLUGIN_NAME=config
@$(MAKE) go-test-plugin PLUGIN_NAME=network
@$(MAKE) go-test-plugin PLUGIN_NAME=buildpacks
go-test-plugin:
cd plugins/$(PLUGIN_NAME) && go get github.com/onsi/gomega && DOKKU_ROOT=/home/dokku go test -v -p 1 -race -mod=readonly || exit $$?
cd plugins/$(PLUGIN_NAME) && go get github.com/onsi/gomega && DOKKU_ROOT=/home/dokku DOKKU_LIB_ROOT=/var/lib/dokku go test -v -p 1 -race -mod=readonly || exit $$?
go-test-plugin-in-docker:
@echo running go unit tests...

View File

@@ -3,11 +3,6 @@
"dokku-postgres",
"dokku-redis"
],
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-nodejs"
}
],
"description": "Used in dokku's test suite",
"env": {
"SECRET_TOKEN": {

View File

@@ -336,6 +336,33 @@ teardown() {
assert_output_contains "heroku-buildpack-python"
}
@test "(buildpacks) deploy with buildpacks from app.json" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP template_buildpacks_appjson
echo "output: $output"
echo "status: $status"
assert_success
assert_output_contains "heroku-buildpack-python"
}
template_buildpacks_appjson() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
echo "injecting app.json with buildpacks -> $APP_REPO_DIR/app.json"
cat <<EOF >"$APP_REPO_DIR/app.json"
{
"buildpacks": [
{
"url": "heroku/python"
}
]
}
EOF
echo "injecting requirements.txt -> $APP_REPO_DIR/requirements.txt"
touch "$APP_REPO_DIR/requirements.txt"
}
template_buildpacks_cleanup() {
local APP="$1"
local APP_REPO_DIR="$2"