feat: migrate builds plugin to go and track per-build records

Adds typed JSON build records under data/builds/<app>/<build-id>.{json,log} keyed on a stable base36 ULID-style DOKKU_BUILD_ID generated for every deploy. The new commands surface that history (builds:list, builds:info, builds:prune) and an operator-configurable retention via builds:set retention. The existing builds:cancel and builds:output now key on the build-id (with safe handling for already-finalized and abandoned records), and the per-build log file replaces journalctl as the durable source of truth for builds:output.
This commit is contained in:
Jose Diaz-Gonzalez
2026-04-30 01:18:03 -04:00
parent 3b3bc3dd39
commit 67e4cf04ab
30 changed files with 2690 additions and 196 deletions

View File

@@ -0,0 +1,60 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
)
const (
helpHeader = `Usage: dokku builds[:COMMAND]
Manage running and historical builds
Additional commands:`
helpContent = `
builds:cancel <app>, Cancel a running build for an app
builds:info <app> <build-id> [--format json|stdout], Show details for a single build
builds:list [<app>] [--format json] [--kind build|deploy] [--status <status>], List builds
builds:output <app> [<build-id>|current], Show build output
builds:prune <app> [--all-apps], Prune build records to retention
builds:report [<app>] [<flag>], Display a build report for one or more apps
builds:set [--global|<app>] <key> [<value>], Set or clear a builds property`
)
func main() {
flag.Usage = usage
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "builds", "builds:help":
usage()
case "help":
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: "ps",
Args: []string{"-o", "command=", strconv.Itoa(os.Getppid())},
})
if err == nil && strings.Contains(result.StdoutContents(), "--all") {
fmt.Println(helpContent)
} else {
fmt.Print("\n builds, Manage running and historical builds\n")
}
default:
dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT"))
if err != nil {
fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable")
dokkuNotImplementExitCode = 10
}
os.Exit(dokkuNotImplementExitCode)
}
}
func usage() {
common.CommandUsage(helpHeader, helpContent)
}

View File

@@ -0,0 +1,77 @@
package main
import (
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/builds"
"github.com/dokku/dokku/plugins/common"
flag "github.com/spf13/pflag"
)
func main() {
parts := strings.Split(os.Args[0], "/")
subcommand := parts[len(parts)-1]
var err error
switch subcommand {
case "cancel":
args := flag.NewFlagSet("builds:cancel", flag.ExitOnError)
args.Parse(os.Args[2:])
err = builds.CommandCancel(args.Arg(0))
case "info":
args := flag.NewFlagSet("builds:info", flag.ExitOnError)
format := args.String("format", "stdout", "format: [ stdout | json ]")
args.Parse(os.Args[2:])
err = builds.CommandInfo(args.Arg(0), args.Arg(1), *format)
case "list":
args := flag.NewFlagSet("builds:list", flag.ExitOnError)
format := args.String("format", "stdout", "format: [ stdout | json ]")
kind := args.String("kind", "", "filter by kind: [ build | deploy ]")
status := args.String("status", "", "filter by status: [ running | succeeded | failed | canceled | abandoned ]")
args.Parse(os.Args[2:])
err = builds.CommandList(args.Arg(0), *format, *kind, *status)
case "output":
args := flag.NewFlagSet("builds:output", flag.ExitOnError)
args.Parse(os.Args[2:])
err = builds.CommandOutput(args.Arg(0), args.Arg(1))
case "prune":
args := flag.NewFlagSet("builds:prune", flag.ExitOnError)
allApps := args.Bool("all-apps", false, "--all-apps: prune every app")
args.Parse(os.Args[2:])
err = builds.CommandPrune(args.Arg(0), *allApps)
case "report":
args := flag.NewFlagSet("builds:report", flag.ExitOnError)
format := args.String("format", "stdout", "format: [ stdout | json ]")
reportArgs, flagErr := common.ParseReportArgs("builds", os.Args[2:])
if flagErr == nil {
args.Parse(reportArgs.OSArgs)
appName := args.Arg(0)
if reportArgs.IsGlobal {
appName = "--global"
}
err = builds.CommandReport(appName, *format, reportArgs.InfoFlag)
}
case "set":
args := flag.NewFlagSet("builds:set", flag.ExitOnError)
global := args.Bool("global", false, "--global: set a global property")
args.Parse(os.Args[2:])
appName := args.Arg(0)
property := args.Arg(1)
value := args.Arg(2)
if *global {
appName = "--global"
property = args.Arg(0)
value = args.Arg(1)
}
err = builds.CommandSet(appName, property, value)
default:
err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand)
}
if err != nil {
common.LogFailWithError(err)
}
}

View File

@@ -0,0 +1,49 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/builds"
"github.com/dokku/dokku/plugins/common"
)
func main() {
parts := strings.Split(os.Args[0], "/")
trigger := parts[len(parts)-1]
flag.Parse()
var err error
switch trigger {
case "builds-generate-id":
err = builds.TriggerBuildsGenerateID()
case "builds-record-finalize":
appName := flag.Arg(0)
buildID := flag.Arg(1)
exitStr := flag.Arg(2)
err = builds.TriggerBuildsRecordFinalize(appName, buildID, exitStr)
case "builds-record-start":
appName := flag.Arg(0)
buildID := flag.Arg(1)
pid := flag.Arg(2)
source := flag.Arg(3)
err = builds.TriggerBuildsRecordStart(appName, buildID, pid, source)
case "install":
err = builds.TriggerInstall()
case "post-app-rename-setup":
oldAppName := flag.Arg(0)
newAppName := flag.Arg(1)
err = builds.TriggerPostAppRenameSetup(oldAppName, newAppName)
case "post-delete":
appName := flag.Arg(0)
err = builds.TriggerPostDelete(appName)
default:
err = fmt.Errorf("Invalid plugin trigger call: %s", trigger)
}
if err != nil {
common.LogFailWithError(err)
}
}