Merge pull request #8106 from dokku/8007-jsonc-app-json

Support inline comments for app.json
This commit is contained in:
Jose Diaz-Gonzalez
2025-11-20 02:37:01 -05:00
committed by GitHub
17 changed files with 78 additions and 36 deletions

View File

@@ -1,5 +1,9 @@
# 0.36.0 Migration Guide
## Changes
- The `app.json` file format is now parsed as JSONC and supports inline comments.
## Removals
- Support for the `DOKKU_DOCKERFILE_CACHE_BUILD` environment variable has been removed. Use the `docker-options` plugin to set build arguments for your app instead.

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/tailscale/hujson"
"k8s.io/utils/ptr"
)
@@ -262,8 +263,14 @@ func ReadAppJSON(path string) (AppJSON, error) {
return AppJSON{}, nil
}
ast, err := hujson.Parse(b)
if err != nil {
return AppJSON{}, fmt.Errorf("Cannot parse app.json as jsonc: %v", err)
}
ast.Standardize()
var appJSON AppJSON
if err = json.Unmarshal(b, &appJSON); err != nil {
if err = json.Unmarshal(ast.Pack(), &appJSON); err != nil {
return AppJSON{}, fmt.Errorf("Cannot parse app.json: %v", err)
}

View File

@@ -133,11 +133,7 @@ func cleanupDeploymentContainer(containerID string, phase string) error {
}
func executeScript(appName string, image string, imageTag string, phase string) error {
phaseName := phase
if phase == "heroku.postdeploy" {
phaseName = "first deploy postdeploy"
}
common.LogInfo1(fmt.Sprintf("Checking for %s task", phaseName))
common.LogInfo1(fmt.Sprintf("Checking for %s task", phase))
command := ""
phaseSource := ""
if phase == "release" {
@@ -152,14 +148,14 @@ func executeScript(appName string, image string, imageTag string, phase string)
}
if command == "" {
common.LogVerbose(fmt.Sprintf("No %s task found, skipping", phaseName))
common.LogVerbose(fmt.Sprintf("No %s task found, skipping", phase))
return nil
}
if phase == "predeploy" {
common.LogVerbose(fmt.Sprintf("Executing %s task from %s: %s", phaseName, phaseSource, command))
common.LogVerbose(fmt.Sprintf("Executing %s task from %s: %s", phase, phaseSource, command))
} else {
common.LogVerbose(fmt.Sprintf("Executing %s task from %s in ephemeral container: %s", phaseName, phaseSource, command))
common.LogVerbose(fmt.Sprintf("Executing %s task from %s in ephemeral container: %s", phase, phaseSource, command))
}
isHerokuishImage := common.IsImageHerokuishBased(image, appName)
@@ -271,15 +267,15 @@ func executeScript(appName string, image string, imageTag string, phase string)
defer cleanupDeploymentContainer(containerID, phase)
if !waitForExecution(containerID) {
common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phaseName, containerID[0:9]))
common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phase, containerID[0:9]))
common.LogVerboseQuietContainerLogs(containerID)
common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phaseName, containerID[0:9]))
return fmt.Errorf("Execution of %s task failed: %s", phaseName, command)
common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phase, containerID[0:9]))
return fmt.Errorf("Execution of %s task failed: %s", phase, command)
}
common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phaseName, containerID[0:9]))
common.LogInfo2Quiet(fmt.Sprintf("Start of %s %s task (%s) output", appName, phase, containerID[0:9]))
common.LogVerboseQuietContainerLogs(containerID)
common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phaseName, containerID[0:9]))
common.LogInfo2Quiet(fmt.Sprintf("End of %s %s task (%s) output", appName, phase, containerID[0:9]))
if phase != "predeploy" {
return nil

View File

@@ -6,6 +6,7 @@ require (
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/spf13/pflag v1.0.10
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a
k8s.io/utils v0.0.0-20240102154912-e7106e64919e
)
@@ -23,6 +24,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/ryanuber/columnize v2.1.2+incompatible // indirect
github.com/stretchr/testify v1.8.4 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect

View File

@@ -39,8 +39,11 @@ github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=

View File

@@ -1,9 +1,9 @@
package appjson
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
@@ -42,18 +42,17 @@ func TriggerAppJSONGetContent(appName string) error {
return nil
}
b, err := os.ReadFile(getProcessSpecificAppJSONPath(appName))
appJSON, err := ReadAppJSON(getProcessSpecificAppJSONPath(appName))
if err != nil {
return fmt.Errorf("Cannot read app.json file: %v", err)
return err
}
content := strings.TrimSpace(string(b))
if content == "" {
fmt.Print("{}")
return nil
content, err := json.Marshal(appJSON)
if err != nil {
return err
}
fmt.Print(content)
fmt.Print(string(content))
return nil
}

View File

@@ -26,6 +26,7 @@ require (
github.com/otiai10/mint v1.6.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect

View File

@@ -51,8 +51,11 @@ github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=

View File

@@ -26,7 +26,7 @@ require (
github.com/pkg/sftp v1.13.5 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/ryanuber/columnize v2.1.2+incompatible // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sys v0.37.0 // indirect
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect

View File

@@ -52,6 +52,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=

View File

@@ -139,6 +139,7 @@ require (
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/tailscale/hujson v0.0.0-20250605163823-992244df8c5a // indirect
github.com/traefik/paerser v0.2.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect

View File

@@ -341,6 +341,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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=
github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ=
github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24=
github.com/traefik/traefik/v2 v2.11.30 h1:Jkb5H8aI9lKQY/DDqwK3JP7iNUEnRfNLWXsr3azFf8w=

View File

@@ -0,0 +1,24 @@
{
"healthchecks": {
"web": [
{
"attempts": 2,
"content": "python/http.server",
"name": "check-1",
"path": "/",
"timeout": 5,
"type": "startup",
"wait": 2
}
]
},
"scripts": {
"dokku": {
// this is a comment for the postdeploy script
"postdeploy": "touch /app/postdeploy.test",
// this is a comment for the predeploy script
"predeploy": "touch /app/predeploy.test"
},
"postdeploy": "touch /app/heroku-postdeploy.test"
}
}

View File

@@ -14,7 +14,9 @@
},
"scripts": {
"dokku": {
// this is a comment for the postdeploy script
"postdeploy": "touch /app/postdeploy.test",
// this is a comment for the predeploy script
"predeploy": "touch /app/predeploy.test"
}
}

View File

@@ -50,7 +50,7 @@ teardown() {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP add_failing_postdeploy
echo "output: $output"
echo "status: $status"
assert_output_contains "Execution of first deploy postdeploy task failed"
assert_output_contains "Execution of heroku.postdeploy task failed"
assert_failure
}

View File

@@ -47,7 +47,12 @@ teardown() {
}
@test "(app-json) app.json scripts postdeploy" {
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP add_postdeploy_command
run /bin/bash -c "dokku app-json:set $TEST_APP appjson-path app-heroku-postdeploy.json"
echo "output: $output"
echo "status: $status"
assert_success
run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP
echo "output: $output"
echo "status: $status"
assert_success

View File

@@ -561,15 +561,6 @@ add_release_command() {
echo "release: touch /app/release.test" >>"$APP_REPO_DIR/Procfile"
}
add_postdeploy_command() {
local APP="$1"
local APP_REPO_DIR="$2"
[[ -z "$APP" ]] && local APP="$TEST_APP"
touch "$APP_REPO_DIR/app.json"
contents=$(jq '.scripts.postdeploy = "touch /app/heroku-postdeploy.test"' "$APP_REPO_DIR/app.json")
echo "${contents}" >"$APP_REPO_DIR/app.json"
}
move_dockerfile_into_place() {
local APP="$1"
local APP_REPO_DIR="$2"