Files
pack/cli/common.go
T
Ed GaniukovandGitHub 01ecfada39 fix: exit 75 for transient failures (#690)
Every failure exited 1, so callers could only retry blindly:
deterministic failures (no provider detected, missing start command)
were run and logged three times by builderv3 before giving up.

Transient failures are now typed as mise.TemporaryError and map to exit
75 (EX_TEMPFAIL): transport errors, 429, 408, 5xx, and truncated
bodies. A 404 for a pinned mise version and a malformed archive stay
deterministic, since retrying them cannot succeed. The mise download
also had no response status check at all, so a 5xx body was written to
disk and later failed as "binary not found in archive".

Carrying that across the process boundary meant giving
GenerateBuildPlan an error return: a mise install failure was
previously swallowed into BuildResult{Success: false} and exited 1
along with every app-level failure. The error is non-nil only for
transient failures, so !Success stays on exit 1.

The download gets an http.Client timeout, since http.DefaultClient has
none and a stalled connection would hang the build forever. It
deliberately does not retry in-process: the caller already retries the
whole command, and retrying here would multiply that and delay the
signal.

Also writes --info-out even when the build result is unsuccessful, so
callers can read {"success": false, "logs": [...]} instead of scraping
stdout.
2026-08-12 14:46:23 +02:00

129 lines
3.5 KiB
Go

package cli
import (
"encoding/json"
"fmt"
"github.com/charmbracelet/log"
"github.com/railwayapp/railpack/core"
a "github.com/railwayapp/railpack/core/app"
"github.com/railwayapp/railpack/core/config"
"github.com/railwayapp/railpack/core/mise"
"github.com/railwayapp/railpack/core/plan"
"github.com/railwayapp/railpack/internal/utils"
"github.com/urfave/cli/v3"
)
var Version string // This will be set by main
const (
// the command failed for a reason that will not change on a retry
ExitCodeFailure = 1
// EX_TEMPFAIL: the command failed for a transient reason (e.g. a network error
// downloading mise) and is worth retrying. Platforms that run Railpack in a
// build pipeline key their retry logic on this.
ExitCodeTransient = 75
)
func exitCodeForError(err error) int {
if mise.IsTemporary(err) {
return ExitCodeTransient
}
return ExitCodeFailure
}
func commonPlanFlags() []cli.Flag {
return []cli.Flag{
&cli.StringSliceFlag{
Name: "env",
Aliases: []string{"e"},
Usage: "environment variables to set",
},
&cli.StringSliceFlag{
Name: "previous",
Usage: "versions of packages used for previous builds (e.g. 'package@version')",
},
&cli.StringFlag{
Name: "build-cmd",
Usage: "build command to use",
},
&cli.StringFlag{
Name: "start-cmd",
Usage: "start command to use",
},
&cli.StringFlag{
Name: "config-file",
Usage: "relative path to railpack config file (default: railpack.json)",
},
&cli.BoolFlag{
Name: "error-missing-start",
Usage: "error if no start command is found",
},
}
}
func GenerateBuildResultForCommand(cmd *cli.Command) (*core.BuildResult, *a.App, *a.Environment, error) {
directory := cmd.Args().First()
if directory == "" {
return nil, nil, nil, cli.Exit("directory argument is required", ExitCodeFailure)
}
app, err := a.NewApp(directory)
if err != nil {
return nil, nil, nil, fmt.Errorf("error creating app: %w", err)
}
log.Debugf("Building %s", app.Source)
envsArgs := cmd.StringSlice("env")
env, err := a.FromEnvs(envsArgs)
if err != nil {
return nil, nil, nil, fmt.Errorf("error creating env: %w", err)
}
// if --verbose is passed as a CLI global argument, enable verbose mise logging so the user don't have to understand
// the railpack build system deeply to get this debugging information.
if cmd.Bool("verbose") && env.GetVariable("MISE_VERBOSE") == "" {
env.SetVariable("MISE_VERBOSE", "1")
}
previousVersions := utils.ParsePackageWithVersion(cmd.StringSlice("previous"))
generateOptions := &core.GenerateBuildPlanOptions{
RailpackVersion: Version,
BuildCommand: cmd.String("build-cmd"),
StartCommand: cmd.String("start-cmd"),
PreviousVersions: previousVersions,
ConfigFilePath: cmd.String("config-file"),
ErrorMissingStartCommand: cmd.Bool("error-missing-start"),
}
buildResult, err := core.GenerateBuildPlan(app, env, generateOptions)
if err != nil {
return nil, nil, nil, err
}
return buildResult, app, env, nil
}
// add $schema link to resulting map JSON for improved IDE experience when manually editing
func addSchemaToPlanMap(p *plan.BuildPlan) (map[string]any, error) {
if p == nil {
return map[string]any{"$schema": config.SchemaUrl}, nil
}
planBytes, err := json.Marshal(p)
if err != nil {
return nil, err
}
var planMap map[string]any
if err := json.Unmarshal(planBytes, &planMap); err != nil {
return nil, err
}
planMap["$schema"] = config.SchemaUrl
return planMap, nil
}