Merge pull request #33 from railwayapp/jr/step-inputs-and-deploy-step

Step inputs and deploy step
This commit is contained in:
Jake Runzer
2025-02-24 11:29:23 -05:00
committed by GitHub
71 changed files with 3318 additions and 2035 deletions
+120 -115
View File
@@ -8,6 +8,7 @@ import (
"slices"
"strings"
"github.com/charmbracelet/log"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/util/system"
specs "github.com/opencontainers/image-spec/specs-go/v1"
@@ -17,7 +18,6 @@ import (
type BuildGraph struct {
graph *graph.Graph
BaseState *llb.State
CacheStore *BuildKitCacheStore
Plan *plan.BuildPlan
Platform *specs.Platform
@@ -32,7 +32,7 @@ type BuildGraphOutput struct {
GraphEnv BuildEnvironment
}
func NewBuildGraph(plan *plan.BuildPlan, baseState *llb.State, localState *llb.State, cacheStore *BuildKitCacheStore, secretsHash string, platform *specs.Platform) (*BuildGraph, error) {
func NewBuildGraph(plan *plan.BuildPlan, localState *llb.State, cacheStore *BuildKitCacheStore, secretsHash string, platform *specs.Platform) (*BuildGraph, error) {
var secretsFile *llb.State
if secretsHash != "" {
st := llb.Scratch().File(llb.Mkfile("/secrets-hash", 0644, []byte(secretsHash)), llb.WithCustomName("[railpack] secrets hash"))
@@ -42,7 +42,6 @@ func NewBuildGraph(plan *plan.BuildPlan, baseState *llb.State, localState *llb.S
g := &BuildGraph{
graph: graph.NewGraph(),
BaseState: baseState,
CacheStore: cacheStore,
Plan: plan,
Platform: platform,
@@ -67,23 +66,124 @@ func NewBuildGraph(plan *plan.BuildPlan, baseState *llb.State, localState *llb.S
// Add dependencies to each node
for _, node := range g.graph.GetNodes() {
llbNode := node.(*StepNode)
for _, depName := range llbNode.Step.DependsOn {
if depNode, exists := g.graph.GetNode(depName); exists {
for _, input := range llbNode.Step.Inputs {
// This input does not reference another step
if input.Step == "" {
continue
}
if depNode, exists := g.graph.GetNode(input.Step); exists {
// Create edges between the current node and the dependency node
parents := llbNode.GetParents()
parents = append(parents, depNode)
llbNode.SetParents(parents)
children := depNode.GetChildren()
children = append(children, node)
children = append(children, llbNode)
depNode.SetChildren(children)
}
}
}
g.graph.ComputeTransitiveDependencies()
// g.graph.PrintGraph()
return g, nil
}
func (g *BuildGraph) GetStateForInput(input plan.Input, baseState llb.State) llb.State {
var state llb.State
if input.Image != "" {
state = llb.Image(input.Image, llb.Platform(*g.Platform))
} else if input.Local {
state = *g.LocalState
} else if input.Step != "" {
if node, exists := g.graph.GetNode(input.Step); exists {
nodeState := node.(*StepNode).State
if nodeState == nil {
return baseState
}
state = *nodeState
}
} else {
state = baseState
}
return state
}
func (g *BuildGraph) GetFullStateFromInputs(inputs []plan.Input) llb.State {
if len(inputs) == 0 {
return llb.Scratch()
}
if len(inputs[0].Include)+len(inputs[0].Exclude) > 0 {
panic("first input must not have include or exclude paths")
}
// Get the base state from the first input
state := g.GetStateForInput(inputs[0], llb.Scratch())
if len(inputs) == 1 {
return state
}
// Copy from subsequent inputs into the base state
for _, input := range inputs[1:] {
inputState := g.GetStateForInput(input, llb.Scratch())
// Copy the specified paths (or everything) from this input into our base state
if len(input.Include) > 0 {
for _, include := range input.Include {
if input.Local {
// For local context, always copy into /app
destPath := filepath.Join("/app", filepath.Base(include))
state = state.File(llb.Copy(inputState, include, destPath, &llb.CopyInfo{
CopyDirContentsOnly: true,
CreateDestPath: true,
FollowSymlinks: true,
AllowWildcard: true,
AllowEmptyWildcard: true,
ExcludePatterns: input.Exclude,
}))
} else {
// For other states, handle paths based on whether they're absolute or relative
srcPath := include
var destPath string
switch {
case include == "." || include == "/app" || include == "/app/":
// Copy entire /app directory
srcPath = "/app"
destPath = "/app"
case filepath.IsAbs(include):
// Preserve absolute paths exactly
destPath = include
default:
// Relative paths are relative to /app
srcPath = filepath.Join("/app", include)
destPath = filepath.Join("/app", include)
}
state = state.File(llb.Copy(inputState, srcPath, destPath, &llb.CopyInfo{
CopyDirContentsOnly: true,
CreateDestPath: true,
FollowSymlinks: true,
AllowWildcard: true,
AllowEmptyWildcard: true,
ExcludePatterns: input.Exclude,
}))
}
}
} else {
log.Warnf("input %s has no include or exclude paths. This is probably a mistake.", input.Step)
}
}
return state
}
// GenerateLLB generates the LLB state for the build graph
func (g *BuildGraph) GenerateLLB() (*BuildGraphOutput, error) {
// Get processing order using topological sort
@@ -100,74 +200,22 @@ func (g *BuildGraph) GenerateLLB() (*BuildGraphOutput, error) {
}
}
// Find all leaf nodes and get their states
var leafNodes []*StepNode
graphEnv := NewGraphEnvironment()
// Process deploy state
deployState := g.GetFullStateFromInputs(g.Plan.Deploy.Inputs)
for _, node := range g.graph.GetNodes() {
llbNode := node.(*StepNode)
if len(llbNode.GetChildren()) == 0 && llbNode.State != nil {
leafNodes = append(leafNodes, llbNode)
graphEnv.Merge(llbNode.OutputEnv)
graphEnv := NewGraphEnvironment()
for _, input := range g.Plan.Deploy.Inputs {
if node, exists := g.graph.GetNode(input.Step); exists {
graphEnv.Merge(node.(*StepNode).OutputEnv)
}
}
// If no leaf states, return base state
if len(leafNodes) == 0 {
return &BuildGraphOutput{
State: g.BaseState,
GraphEnv: graphEnv,
}, nil
}
// If only one leaf state, return it
if len(leafNodes) == 1 {
return &BuildGraphOutput{
State: leafNodes[0].State,
GraphEnv: graphEnv,
}, nil
}
result := g.mergeNodes(leafNodes)
return &BuildGraphOutput{
State: &result,
State: &deployState,
GraphEnv: graphEnv,
}, nil
}
// mergeNodes merges the states of the given nodes into a single state
// This essentially creates a scratch file system and then copies the contents of each node's state into it
func (g *BuildGraph) mergeNodes(nodes []*StepNode) llb.State {
if len(nodes) == 1 {
return *nodes[0].State
}
// Sort nodes by step name for deterministic ordering
sortedNodes := slices.Clone(nodes)
slices.SortFunc(sortedNodes, func(a, b *StepNode) int {
return strings.Compare(a.Step.Name, b.Step.Name)
})
stateNames := []string{}
states := []llb.State{}
for _, node := range sortedNodes {
stateNames = append(stateNames, node.Step.Name)
states = append(states, *node.State)
}
result := llb.Scratch()
for i, state := range states {
result = result.File(llb.Copy(state, "/", "/", &llb.CopyInfo{
CreateDestPath: true,
FollowSymlinks: true,
AllowWildcard: true,
}), llb.WithCustomNamef("copy from %s", stateNames[i]))
}
return result
}
// processNode processes a node and its parents to determine the state to build upon
func (g *BuildGraph) processNode(node *StepNode) error {
// If already processed, we're done
@@ -196,7 +244,7 @@ func (g *BuildGraph) processNode(node *StepNode) error {
}
// Determine the state to build upon
var currentState *llb.State
// var currentState llb.State
currentGraphEnv := NewGraphEnvironment()
// Merge the output envs of all the parent nodes
@@ -205,29 +253,10 @@ func (g *BuildGraph) processNode(node *StepNode) error {
currentGraphEnv.Merge(parentNode.OutputEnv)
}
if len(node.GetParents()) == 0 {
currentState = g.BaseState
} else {
parentNodes := make([]*StepNode, len(node.GetParents()))
for i, parent := range node.GetParents() {
parentNode := parent.(*StepNode)
if parentNode.State == nil {
return fmt.Errorf("parent %s of %s has nil state",
parentNode.Step.Name, node.Step.Name)
}
parentNodes[i] = parentNode
}
merged := g.mergeNodes(parentNodes)
currentState = &merged
}
node.InputEnv = currentGraphEnv
// Convert this node's step to LLB
stepState, err := g.convertNodeToLLB(node, currentState)
stepState, err := g.convertNodeToLLB(node)
if err != nil {
return err
}
@@ -239,11 +268,8 @@ func (g *BuildGraph) processNode(node *StepNode) error {
}
// convertNodeToLLB converts a step node to an LLB state
func (g *BuildGraph) convertNodeToLLB(node *StepNode, baseState *llb.State) (*llb.State, error) {
state := *baseState
state = state.Dir("/app")
state, err := g.getNodeStartingState(state, node)
func (g *BuildGraph) convertNodeToLLB(node *StepNode) (*llb.State, error) {
state, err := g.getNodeStartingState(node)
if err != nil {
return nil, err
}
@@ -259,28 +285,14 @@ func (g *BuildGraph) convertNodeToLLB(node *StepNode, baseState *llb.State) (*ll
}
}
if len(node.Step.Outputs) > 0 {
newState := *baseState
// Copy the outputs directly to the new state
for _, output := range node.Step.Outputs {
newState = newState.File(llb.Copy(state, output, output, &llb.CopyInfo{
CreateDestPath: true,
AllowWildcard: true,
AllowEmptyWildcard: true,
FollowSymlinks: true,
}), llb.WithCustomNamef("copying %s", output))
}
state = newState
}
return &state, nil
}
// Adds the input environment to the base state of the node
// This includes things like the environment variables and accumulated paths
func (g *BuildGraph) getNodeStartingState(baseState llb.State, node *StepNode) (llb.State, error) {
func (g *BuildGraph) getNodeStartingState(node *StepNode) (llb.State, error) {
state := g.GetFullStateFromInputs(node.Step.Inputs).Dir("/app")
envVars := make(map[string]string)
// Collect all environment variables first
@@ -293,13 +305,6 @@ func (g *BuildGraph) getNodeStartingState(baseState llb.State, node *StepNode) (
node.OutputEnv.AddEnvVar(k, v)
}
var state llb.State
if node.Step.StartingImage != "" {
state = llb.Image(node.Step.StartingImage, llb.Platform(*g.Platform)).Dir("/app")
} else {
state = baseState
}
for _, k := range slices.Sorted(maps.Keys(envVars)) {
state = state.AddEnv(k, envVars[k])
}
-3
View File
@@ -3,7 +3,6 @@ package build_llb
import (
"fmt"
"github.com/charmbracelet/log"
"github.com/moby/buildkit/client/llb"
"github.com/railwayapp/railpack/core/plan"
)
@@ -33,7 +32,6 @@ func (c *BuildKitCacheStore) GetCache(key string, planCache *plan.Cache) BuildKi
}
if cache, ok := c.CacheMap[cacheKey]; ok {
log.Debugf("Cache %s already exists", cacheKey)
return cache
}
@@ -44,7 +42,6 @@ func (c *BuildKitCacheStore) GetCache(key string, planCache *plan.Cache) BuildKi
planCache: planCache,
cacheState: &cacheState,
}
log.Debugf("Creating new cache %s", cacheKey)
c.CacheMap[cacheKey] = cache
+9 -59
View File
@@ -27,8 +27,6 @@ const (
func ConvertPlanToLLB(plan *p.BuildPlan, opts ConvertPlanOptions) (*llb.State, *Image, error) {
platform := opts.BuildPlatform.ToPlatform()
state := getBaseState(plan, platform)
localState := llb.Local("context",
llb.SharedKeyHint("local"),
llb.SessionID(opts.SessionID),
@@ -37,7 +35,7 @@ func ConvertPlanToLLB(plan *p.BuildPlan, opts ConvertPlanOptions) (*llb.State, *
)
cacheStore := build_llb.NewBuildKitCacheStore(opts.CacheKey)
graph, err := build_llb.NewBuildGraph(plan, &state, &localState, cacheStore, opts.SecretsHash, &platform)
graph, err := build_llb.NewBuildGraph(plan, &localState, cacheStore, opts.SecretsHash, &platform)
if err != nil {
return nil, nil, err
}
@@ -47,13 +45,11 @@ func ConvertPlanToLLB(plan *p.BuildPlan, opts ConvertPlanOptions) (*llb.State, *
return nil, nil, err
}
state = *graphOutput.State
state = getStartState(state, localState, plan, platform)
state := getStartState(*graphOutput.State)
imageEnv := getImageEnv(graphOutput, plan)
startCommand := plan.Start.Command
if plan.Start.Command == "" {
startCommand := plan.Deploy.StartCmd
if startCommand == "" {
startCommand = "/bin/bash"
}
@@ -79,58 +75,22 @@ func ConvertPlanToLLB(plan *p.BuildPlan, opts ConvertPlanOptions) (*llb.State, *
return &state, &image, nil
}
func getStartState(buildState llb.State, localState llb.State, plan *p.BuildPlan, platform specs.Platform) llb.State {
// If there is no custom start image, we just copy over the outputs from the local state
if plan.Start.BaseImage == "" {
startState := buildState.Dir(WorkingDir)
// TODO: This is confusing and needs to change
// If baseImage is provided, we are using `outputs` to copy files from the local state
// If not provided, we are using `outputs` to copy files from the build state
// Confusing and not good
for _, path := range plan.Start.Outputs {
startState = startState.File(llb.Copy(localState, path, path, &llb.CopyInfo{
CreateDestPath: true,
FollowSymlinks: true,
CopyDirContentsOnly: true,
}))
}
return startState
}
startState := llb.Image(plan.Start.BaseImage, llb.Platform(platform)).Dir(WorkingDir)
// This is all the user code + any modifications made by the providers
mergedState := buildState.File(llb.Copy(localState, ".", ".", &llb.CopyInfo{
CreateDestPath: true,
FollowSymlinks: true,
CopyDirContentsOnly: true,
}))
// Copy over necessary files to the start image
for _, path := range plan.Start.Outputs {
startState = startState.File(llb.Copy(mergedState, path, path, &llb.CopyInfo{
CreateDestPath: true,
FollowSymlinks: true,
CopyDirContentsOnly: true,
}))
}
func getStartState(buildState llb.State) llb.State {
startState := buildState.Dir(WorkingDir)
return startState
}
func getImageEnv(graphOutput *build_llb.BuildGraphOutput, plan *p.BuildPlan) []string {
paths := []string{}
paths = append(paths, plan.Deploy.Paths...)
paths = append(paths, graphOutput.GraphEnv.PathList...)
paths = append(paths, plan.Start.Paths...)
paths = append(paths, system.DefaultPathEnvUnix)
slices.Sort(paths)
pathString := strings.Join(paths, ":")
envMap := make(map[string]string, len(graphOutput.GraphEnv.EnvVars)+len(plan.Start.Variables)+1)
envMap := make(map[string]string, len(graphOutput.GraphEnv.EnvVars)+len(plan.Deploy.Variables)+1)
maps.Copy(envMap, graphOutput.GraphEnv.EnvVars)
maps.Copy(envMap, plan.Start.Variables)
maps.Copy(envMap, plan.Deploy.Variables)
envMap["PATH"] = pathString
@@ -142,13 +102,3 @@ func getImageEnv(graphOutput *build_llb.BuildGraphOutput, plan *p.BuildPlan) []s
return envVars
}
func getBaseState(plan *p.BuildPlan, platform specs.Platform) llb.State {
state := llb.Image(plan.BaseImage,
llb.Platform(platform),
)
state = state.AddEnv("DEBIAN_FRONTEND", "noninteractive").Dir(WorkingDir)
return state
}
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
@@ -10,13 +9,39 @@
"type": "locked"
}
},
"start": {
"cmd": "neofetch $HELLO",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "packages:runtime"
},
{
"include": [
"."
],
"step": "build"
}
],
"startCommand": "neofetch $HELLO"
},
"steps": [
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y neofetch'",
"customName": "install apt packages: neofetch"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:runtime"
},
{
"assets": {
"mise.toml": "[mise.toml]"
@@ -26,13 +51,13 @@
"apt-lists"
],
"commands": [
{
"path": "/mise/shims"
},
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y neofetch'",
"customName": "install apt packages: neofetch"
},
{
"path": "/mise/shims"
},
{
"customName": "create mise config",
"name": "mise.toml",
@@ -43,16 +68,12 @@
"customName": "install mise packages: python"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -61,66 +82,6 @@
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"commands": [
{
"path": "/root/.local/bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [
"*"
],
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
{
"dependsOn": [
"packages:mise",
"setup",
"packages:apt:python-system-deps"
],
"name": "install",
"secrets": []
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y pkg-config'",
"customName": "install apt packages: pkg-config"
}
],
"name": "packages:apt:python-system-deps",
"secrets": []
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y neofetch'",
"customName": "install apt packages: neofetch"
}
],
"name": "packages:apt:config",
"secrets": []
},
{
"commands": [
{
@@ -128,9 +89,10 @@
"customName": "neofetch"
}
],
"dependsOn": [
"packages:mise",
"packages:apt:config"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "build",
"secrets": [
@@ -1,20 +1,37 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
"type": "locked"
},
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
},
"go-build": {
"directory": "/root/.cache/go-build",
"type": "shared"
}
},
"start": {
"baseImage": "alpine:latest",
"cmd": "./out",
"outputs": [
"out"
"deploy": {
"inputs": [
{
"step": "packages:runtime"
},
{
"include": [
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"variables": {
"GIN_MODE": "release"
}
"startCommand": "./out"
},
"steps": [
{
@@ -35,16 +52,12 @@
"customName": "install mise packages: go"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -58,6 +71,9 @@
"go-build"
],
"commands": [
{
"path": "/go/bin"
},
{
"dest": "go.mod",
"src": "go.mod"
@@ -70,15 +86,19 @@
"cmd": "go mod download"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": [
"*"
],
"variables": {
"CGO_ENABLED": "0"
"CGO_ENABLED": "0",
"GOBIN": "/go/bin",
"GOPATH": "/go"
}
},
{
@@ -91,17 +111,36 @@
"src": "."
},
{
"cmd": "go build -o out ./cmd/server"
"cmd": "go build -ldflags=\"-w -s\" -o out ./cmd/server"
}
],
"dependsOn": [
"packages:mise",
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y tzdata'",
"customName": "install apt packages: tzdata"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:runtime"
}
]
}
@@ -1,17 +1,37 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
"type": "locked"
},
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
},
"go-build": {
"directory": "/root/.cache/go-build",
"type": "shared"
}
},
"start": {
"baseImage": "alpine:latest",
"cmd": "./out",
"outputs": [
"out"
]
"deploy": {
"inputs": [
{
"step": "packages:runtime"
},
{
"include": [
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "./out"
},
"steps": [
{
@@ -32,16 +52,12 @@
"customName": "install mise packages: go"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -55,6 +71,9 @@
"go-build"
],
"commands": [
{
"path": "/go/bin"
},
{
"dest": "go.mod",
"src": "go.mod"
@@ -67,15 +86,19 @@
"cmd": "go mod download"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": [
"*"
],
"variables": {
"CGO_ENABLED": "0"
"CGO_ENABLED": "0",
"GOBIN": "/go/bin",
"GOPATH": "/go"
}
},
{
@@ -88,17 +111,36 @@
"src": "."
},
{
"cmd": "go build -o out"
"cmd": "go build -ldflags=\"-w -s\" -o out"
}
],
"dependsOn": [
"packages:mise",
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y tzdata'",
"customName": "install apt packages: tzdata"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:runtime"
}
]
}
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"node-modules": {
"directory": "/app/node_modules/.cache",
@@ -10,11 +9,48 @@
"type": "shared"
}
},
"start": {
"cmd": "npm run start",
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise",
"mise.toml"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
"."
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "npm run start",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -47,17 +83,12 @@
"customName": "install mise packages: node"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise",
"/app/mise.toml"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -67,16 +98,27 @@
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"dest": "package.json",
"src": "package.json"
},
{
"cmd": "npm install"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "setup",
"secrets": [],
"name": "install",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -92,19 +134,18 @@
],
"commands": [
{
"dest": "package.json",
"src": "package.json"
},
{
"cmd": "npm install"
"cmd": "npm prune --omit=dev"
}
],
"dependsOn": [
"packages:mise",
"setup"
"inputs": [
{
"step": "install"
}
],
"name": "install",
"secrets": []
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
@@ -119,8 +160,10 @@
"cmd": "npm run build"
}
],
"dependsOn": [
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"bun-install": {
"directory": "/root/.bun/install/cache",
@@ -10,11 +9,47 @@
"type": "shared"
}
},
"start": {
"cmd": "bun index.ts",
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
"."
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "bun index.ts",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -40,19 +75,15 @@
},
{
"cmd": "sh -c 'mise trust -a \u0026\u0026 mise install'",
"customName": "install mise packages: bun"
"customName": "install mise packages: bun, go"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -62,16 +93,31 @@
}
},
{
"caches": [
"bun-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "bun.lockb",
"src": "bun.lockb"
},
{
"cmd": "bun install --frozen-lockfile"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "setup",
"secrets": [],
"name": "install",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -87,23 +133,19 @@
],
"commands": [
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "bun.lockb",
"src": "bun.lockb"
},
{
"cmd": "bun install --frozen-lockfile"
"cmd": "sh -c 'rm -rf node_modules \u0026\u0026 bun install --production'",
"customName": "rm -rf node_modules \u0026\u0026 bun install --production"
}
],
"dependsOn": [
"packages:mise",
"setup"
"inputs": [
{
"step": "install"
}
],
"name": "install",
"secrets": []
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
@@ -118,8 +160,10 @@
"cmd": "bun run build"
}
],
"dependsOn": [
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"node-modules": {
"directory": "/app/node_modules/.cache",
@@ -10,11 +9,48 @@
"type": "shared"
}
},
"start": {
"cmd": "pnpm run start",
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
".",
"/root/.cache"
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "pnpm run start",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -43,16 +79,12 @@
"customName": "install mise packages: node, pnpm"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -63,27 +95,13 @@
}
},
{
"caches": [
"pnpm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [],
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"commands": [
},
{
"dest": "package.json",
"src": "package.json"
@@ -93,20 +111,7 @@
},
{
"cmd": "corepack prepare --activate"
}
],
"dependsOn": [
"packages:mise",
"setup"
],
"name": "corepack",
"secrets": []
},
{
"caches": [
"pnpm-install"
],
"commands": [
},
{
"dest": "package.json",
"src": "package.json"
@@ -119,13 +124,39 @@
"cmd": "pnpm install --frozen-lockfile --prod=false"
}
],
"dependsOn": [
"packages:mise",
"setup",
"corepack"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": []
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"caches": [
"pnpm-install"
],
"commands": [
{
"cmd": "pnpm prune --prod"
}
],
"inputs": [
{
"step": "install"
}
],
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
@@ -140,8 +171,10 @@
"cmd": "pnpm run build"
}
],
"dependsOn": [
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"next-": {
"directory": "/app/.next/cache",
@@ -14,11 +13,47 @@
"type": "shared"
}
},
"start": {
"cmd": "npm run start",
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
"."
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "npm run start",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -47,16 +82,12 @@
"customName": "install mise packages: node"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -66,16 +97,31 @@
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "package-lock.json",
"src": "package-lock.json"
},
{
"cmd": "npm ci"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "setup",
"secrets": [],
"name": "install",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -91,23 +137,18 @@
],
"commands": [
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "package-lock.json",
"src": "package-lock.json"
},
{
"cmd": "npm ci"
"cmd": "npm prune --omit=dev"
}
],
"dependsOn": [
"packages:mise",
"setup"
"inputs": [
{
"step": "install"
}
],
"name": "install",
"secrets": []
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
@@ -123,8 +164,10 @@
"cmd": "npm run build"
}
],
"dependsOn": [
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"node-modules": {
"directory": "/app/node_modules/.cache",
@@ -10,11 +9,47 @@
"type": "shared"
}
},
"start": {
"cmd": "node index.js",
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
"."
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "node index.js",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -43,16 +78,12 @@
"customName": "install mise packages: node"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -61,31 +92,14 @@
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"commands": [
{
"path": "/app/node_modules/.bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [],
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"dest": "package.json",
"src": "package.json"
@@ -106,19 +120,48 @@
"cmd": "npm ci"
}
],
"dependsOn": [
"packages:mise",
"setup"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": []
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"cmd": "npm prune --omit=dev"
}
],
"inputs": [
{
"step": "install"
}
],
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
"node-modules"
],
"dependsOn": [
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"node-modules": {
"directory": "/app/node_modules/.cache",
@@ -10,11 +9,47 @@
"type": "shared"
}
},
"start": {
"cmd": "npm run start",
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
"."
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "npm run start",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -43,16 +78,12 @@
"customName": "install mise packages: node"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -62,16 +93,31 @@
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "package-lock.json",
"src": "package-lock.json"
},
{
"cmd": "npm ci"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "setup",
"secrets": [],
"name": "install",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -87,30 +133,27 @@
],
"commands": [
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "package-lock.json",
"src": "package-lock.json"
},
{
"cmd": "npm ci"
"cmd": "npm prune --omit=dev"
}
],
"dependsOn": [
"packages:mise",
"setup"
"inputs": [
{
"step": "install"
}
],
"name": "install",
"secrets": []
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
"node-modules"
],
"dependsOn": [
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,16 +1,55 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"node-modules": {
"directory": "/app/node_modules/.cache",
"type": "shared"
},
"pnpm-install": {
"directory": "/root/.local/share/pnpm/store/v3",
"type": "shared"
}
},
"start": {
"cmd": "node index.js",
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
"."
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "node index.js",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -39,16 +78,12 @@
"customName": "install mise packages: node, pnpm"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -58,16 +93,43 @@
}
},
{
"caches": [
"pnpm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "packages/pkg-a/package.json",
"src": "packages/pkg-a/package.json"
},
{
"dest": "packages/pkg-b/package.json",
"src": "packages/pkg-b/package.json"
},
{
"dest": "pnpm-workspace.yaml",
"src": "pnpm-workspace.yaml"
},
{
"dest": "pnpm-lock.yaml",
"src": "pnpm-lock.yaml"
},
{
"cmd": "pnpm install --frozen-lockfile --prod=false"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "setup",
"secrets": [],
"name": "install",
"variables": {
"CI": "true",
"NODE_ENV": "production",
@@ -77,12 +139,33 @@
"YARN_PRODUCTION": "false"
}
},
{
"caches": [
"pnpm-install"
],
"commands": [
{
"cmd": "pnpm prune --prod"
}
],
"inputs": [
{
"step": "install"
}
],
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
"node-modules"
],
"dependsOn": [
"setup"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"next-apps-docs": {
"directory": "/app/apps/docs/.next/cache",
@@ -18,9 +17,46 @@
"type": "shared"
}
},
"start": {
"outputs": [
"."
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"exclude": [
"node_modules"
],
"include": [
".",
"/root/.cache"
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune"
},
{
"include": [
"."
],
"local": true
}
],
"variables": {
"CI": "true",
@@ -50,16 +86,12 @@
"customName": "install mise packages: node"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -70,27 +102,13 @@
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [],
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"commands": [
},
{
"dest": "package.json",
"src": "package.json"
@@ -100,20 +118,7 @@
},
{
"cmd": "corepack prepare --activate"
}
],
"dependsOn": [
"packages:mise",
"setup"
],
"name": "corepack",
"secrets": []
},
{
"caches": [
"npm-install"
],
"commands": [
},
{
"dest": "package.json",
"src": "package.json"
@@ -150,13 +155,39 @@
"cmd": "npm ci"
}
],
"dependsOn": [
"packages:mise",
"setup",
"corepack"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": []
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"cmd": "npm prune --omit=dev"
}
],
"inputs": [
{
"step": "install"
}
],
"name": "prune",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
@@ -173,8 +204,10 @@
"cmd": "npm run build"
}
],
"dependsOn": [
"install"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
@@ -1,14 +1,5 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
"type": "locked"
},
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
},
"composer": {
"directory": "/opt/cache/composer",
"type": "shared"
@@ -22,164 +13,52 @@
"type": "shared"
}
},
"start": {
"cmd": "bash /start-nginx.sh",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "install:composer"
},
{
"exclude": [
"node_modules"
],
"include": [
"."
],
"step": "build"
},
{
"include": [
"/app/node_modules"
],
"step": "prune:node"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "bash /start-nginx.sh",
"variables": {
"IS_LARAVEL": "true"
}
},
"steps": [
{
"name": "packages:image",
"secrets": [],
"startingImage": "php:8.2.27-fpm"
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y git nginx unzip zip'",
"customName": "install apt packages: git nginx unzip zip"
}
],
"dependsOn": [
"packages:image"
],
"name": "packages:apt:nginx",
"secrets": []
},
{
"caches": [
"composer"
],
"commands": [
"inputs": [
{
"dest": "/usr/bin/composer",
"image": "composer:latest",
"src": "/usr/bin/composer"
},
{
"dest": ".",
"src": "."
},
{
"cmd": "composer install --ignore-platform-reqs"
"image": "php:8.2.27-fpm"
}
],
"dependsOn": [
"packages:apt:nginx"
],
"name": "install",
"secrets": [
"*"
],
"variables": {
"COMPOSER_CACHE_DIR": "/opt/cache/composer"
}
},
{
"assets": {
"mise.toml": "[mise.toml]"
},
"commands": [
{
"path": "/mise/shims"
},
{
"customName": "create mise config",
"name": "mise.toml",
"path": "/etc/mise/config.toml"
},
{
"cmd": "sh -c 'mise trust -a \u0026\u0026 mise install'",
"customName": "install mise packages: node"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
"MISE_DATA_DIR": "/mise",
"MISE_INSTALLS_DIR": "/mise/installs",
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"commands": [
{
"path": "/app/node_modules/.bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup:node",
"secrets": [],
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "package-lock.json",
"src": "package-lock.json"
},
{
"cmd": "npm ci"
}
],
"dependsOn": [
"packages:mise",
"setup:node"
],
"name": "install:node",
"secrets": []
},
{
"caches": [
"node-modules"
],
"commands": [
{
"dest": ".",
"src": "."
},
{
"cmd": "npm run build"
}
],
"dependsOn": [
"install:node"
],
"name": "build:node",
"outputs": [
"/app"
],
"name": "packages:image",
"secrets": [
"*"
]
@@ -211,16 +90,166 @@
"path": "/start-nginx.sh"
}
],
"dependsOn": [
"packages:apt:nginx"
"inputs": [
{
"step": "packages:image"
}
],
"name": "nginx:setup",
"name": "nginx",
"secrets": [
"*"
]
},
{
"caches": [
"composer"
],
"commands": [
{
"dest": "/usr/bin/composer",
"image": "composer:latest",
"src": "/usr/bin/composer"
},
{
"dest": ".",
"src": "."
},
{
"cmd": "composer install --ignore-platform-reqs"
}
],
"inputs": [
{
"step": "nginx"
}
],
"name": "install:composer",
"secrets": [
"*"
],
"variables": {
"IS_LARAVEL": "true"
"COMPOSER_CACHE_DIR": "/opt/cache/composer"
}
},
{
"assets": {
"mise.toml": "[mise.toml]"
},
"commands": [
{
"path": "/mise/shims"
},
{
"customName": "create mise config",
"name": "mise.toml",
"path": "/etc/mise/config.toml"
},
{
"cmd": "sh -c 'mise trust -a \u0026\u0026 mise install'",
"customName": "install mise packages: node"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
"MISE_DATA_DIR": "/mise",
"MISE_INSTALLS_DIR": "/mise/installs",
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"path": "/app/node_modules/.bin"
},
{
"dest": "package.json",
"src": "package.json"
},
{
"dest": "package-lock.json",
"src": "package-lock.json"
},
{
"cmd": "npm ci"
}
],
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install:node",
"variables": {
"CI": "true",
"NODE_ENV": "production",
"NPM_CONFIG_FUND": "false",
"NPM_CONFIG_PRODUCTION": "false",
"NPM_CONFIG_UPDATE_NOTIFIER": "false",
"YARN_PRODUCTION": "false"
}
},
{
"caches": [
"npm-install"
],
"commands": [
{
"cmd": "npm prune --omit=dev"
}
],
"inputs": [
{
"step": "install:node"
}
],
"name": "prune:node",
"variables": {
"NPM_CONFIG_PRODUCTION": "true"
}
},
{
"caches": [
"node-modules"
],
"commands": [
{
"dest": ".",
"src": "."
},
{
"cmd": "npm run build"
}
],
"inputs": [
{
"step": "install:composer"
},
{
"include": [
".",
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "install:node"
}
],
"name": "build",
"secrets": [
"*"
]
}
]
}
@@ -1,76 +1,41 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
"type": "locked"
},
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
},
"composer": {
"directory": "/opt/cache/composer",
"type": "shared"
}
},
"start": {
"cmd": "bash /start-nginx.sh",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "bash /start-nginx.sh"
},
"steps": [
{
"name": "packages:image",
"secrets": [],
"startingImage": "php:8.2.27-fpm"
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y git nginx unzip zip'",
"customName": "install apt packages: git nginx unzip zip"
}
],
"dependsOn": [
"packages:image"
],
"name": "packages:apt:nginx",
"secrets": []
},
{
"caches": [
"composer"
],
"commands": [
"inputs": [
{
"dest": "/usr/bin/composer",
"image": "composer:latest",
"src": "/usr/bin/composer"
},
{
"dest": ".",
"src": "."
},
{
"cmd": "composer install --ignore-platform-reqs"
"image": "php:8.2.27-fpm"
}
],
"dependsOn": [
"packages:apt:nginx"
],
"name": "install",
"name": "packages:image",
"secrets": [
"*"
],
"variables": {
"COMPOSER_CACHE_DIR": "/opt/cache/composer"
}
]
},
{
"assets": {
@@ -99,15 +64,64 @@
"path": "/start-nginx.sh"
}
],
"dependsOn": [
"packages:apt:nginx"
"inputs": [
{
"step": "packages:image"
}
],
"name": "nginx:setup",
"name": "nginx",
"secrets": [
"*"
]
},
{
"caches": [
"composer"
],
"commands": [
{
"dest": "/usr/bin/composer",
"image": "composer:latest",
"src": "/usr/bin/composer"
},
{
"dest": ".",
"src": "."
},
{
"cmd": "composer install --ignore-platform-reqs"
}
],
"inputs": [
{
"step": "nginx"
}
],
"name": "install:composer",
"secrets": [
"*"
],
"variables": {
"COMPOSER_CACHE_DIR": "/opt/cache/composer"
}
},
{
"inputs": [
{
"step": "install:composer"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"name": "packages:mise",
"secrets": [
"*"
@@ -1,43 +1,35 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
"type": "locked"
},
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
}
},
"start": {
"cmd": "bash /start-nginx.sh",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "bash /start-nginx.sh"
},
"steps": [
{
"name": "packages:image",
"secrets": [],
"startingImage": "php:8.4.3-fpm"
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y git nginx unzip zip'",
"customName": "install apt packages: git nginx unzip zip"
}
],
"dependsOn": [
"packages:image"
"inputs": [
{
"image": "php:8.4.3-fpm"
}
],
"name": "packages:apt:nginx",
"secrets": []
"name": "packages:image",
"secrets": [
"*"
]
},
{
"assets": {
@@ -66,15 +58,44 @@
"path": "/start-nginx.sh"
}
],
"dependsOn": [
"packages:apt:nginx"
"inputs": [
{
"step": "packages:image"
}
],
"name": "nginx:setup",
"name": "nginx",
"secrets": [
"*"
]
},
{
"inputs": [
{
"step": "nginx"
}
],
"name": "install:composer",
"secrets": [
"*"
]
},
{
"inputs": [
{
"step": "install:composer"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"name": "packages:mise",
"secrets": [
"*"
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
@@ -10,18 +9,58 @@
"type": "locked"
}
},
"start": {
"cmd": "python main.py",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "packages:python-runtime-deps"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"include": [
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "python main.py",
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
"steps": [
{
"assets": {
"mise.toml": "[mise.toml]"
},
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libpq-dev python3-dev'",
"customName": "install apt packages: libpq-dev python3-dev"
},
{
"path": "/mise/shims"
},
@@ -35,16 +74,12 @@
"customName": "install mise packages: pipx, python"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -55,19 +90,34 @@
},
{
"commands": [
{
"cmd": "pipx install pdm"
},
{
"path": "/root/.local/bin"
},
{
"dest": ".",
"src": "."
},
{
"cmd": "python --version"
},
{
"cmd": "pdm install --check --prod --no-editable"
},
{
"path": "/app/.venv/bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [
"*"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PDM_CHECK_UPDATE": "false",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
@@ -77,39 +127,15 @@
}
},
{
"commands": [
"inputs": [
{
"cmd": "pipx install pdm"
},
{
"dest": "pyproject.toml",
"src": "pyproject.toml"
},
{
"dest": "pdm.lock",
"src": "pdm.lock"
},
{
"dest": ".",
"src": "."
},
{
"cmd": "pdm install --check --prod --no-editable"
},
{
"path": "/app/.venv/bin"
"step": "install"
}
],
"dependsOn": [
"packages:mise",
"setup",
"packages:apt:python-system-deps"
],
"name": "install",
"secrets": [],
"variables": {
"PDM_CHECK_UPDATE": "false"
}
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
@@ -118,12 +144,16 @@
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y pkg-config'",
"customName": "install apt packages: pkg-config"
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y '",
"customName": "install apt packages: "
}
],
"name": "packages:apt:python-system-deps",
"secrets": []
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:python-runtime-deps"
}
]
}
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
@@ -14,18 +13,59 @@
"type": "shared"
}
},
"start": {
"cmd": "python main.py",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "packages:python-runtime-deps"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"include": [
"/opt/python-packages",
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "python main.py",
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
"steps": [
{
"assets": {
"mise.toml": "[mise.toml]"
},
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libpq-dev python3-dev'",
"customName": "install apt packages: libpq-dev python3-dev"
},
{
"path": "/mise/shims"
},
@@ -39,16 +79,12 @@
"customName": "install mise packages: python"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -57,29 +93,6 @@
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"commands": [
{
"path": "/root/.local/bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [
"*"
],
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
{
"caches": [
"pip"
@@ -90,16 +103,36 @@
"src": "requirements.txt"
},
{
"cmd": "pip install -r requirements.txt"
"cmd": "pip install --target=/opt/python-packages -r requirements.txt"
}
],
"dependsOn": [
"packages:mise",
"setup",
"packages:apt:python-system-deps"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": []
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONPATH": "/opt/python-packages",
"PYTHONUNBUFFERED": "1"
}
},
{
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
@@ -108,12 +141,16 @@
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y pkg-config'",
"customName": "install apt packages: pkg-config"
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y '",
"customName": "install apt packages: "
}
],
"name": "packages:apt:python-system-deps",
"secrets": []
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:python-runtime-deps"
}
]
}
@@ -0,0 +1,176 @@
{
"caches": {
"apt": {
"directory": "/var/cache/apt",
"type": "locked"
},
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
}
},
"deploy": {
"inputs": [
{
"step": "packages:python-runtime-deps"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"include": [
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "python main.py",
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
"steps": [
{
"assets": {
"mise.toml": "[mise.toml]"
},
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libpq-dev python3-dev'",
"customName": "install apt packages: libpq-dev python3-dev"
},
{
"path": "/mise/shims"
},
{
"customName": "create mise config",
"name": "mise.toml",
"path": "/etc/mise/config.toml"
},
{
"cmd": "sh -c 'mise trust -a \u0026\u0026 mise install'",
"customName": "install mise packages: pipx, python"
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
"MISE_DATA_DIR": "/mise",
"MISE_INSTALLS_DIR": "/mise/installs",
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"commands": [
{
"cmd": "pipx install pipenv"
},
{
"path": "/root/.local/bin"
},
{
"dest": "Pipfile",
"src": "Pipfile"
},
{
"dest": "Pipfile.lock",
"src": "Pipfile.lock"
},
{
"cmd": "pipenv install --deploy --ignore-pipfile"
},
{
"path": "/app/.venv/bin"
},
{
"dest": "Pipfile",
"src": "Pipfile"
},
{
"dest": "Pipfile.lock",
"src": "Pipfile.lock"
},
{
"cmd": "pipenv install --deploy --ignore-pipfile"
},
{
"path": "/app/.venv/bin"
}
],
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"variables": {
"PIPENV_CHECK_UPDATE": "false",
"PIPENV_IGNORE_VIRTUALENVS": "1",
"PIPENV_VENV_IN_PROJECT": "1",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
{
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y '",
"customName": "install apt packages: "
}
],
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:python-runtime-deps"
}
]
}
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
@@ -10,18 +9,59 @@
"type": "locked"
}
},
"start": {
"cmd": "python main.py",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "packages:python-runtime-deps"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"include": [
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "python main.py",
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1",
"VIRTUAL_ENV": "/app/.venv"
}
},
"steps": [
{
"assets": {
"mise.toml": "[mise.toml]"
},
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libpq-dev python3-dev'",
"customName": "install apt packages: libpq-dev python3-dev"
},
{
"path": "/mise/shims"
},
@@ -35,16 +75,12 @@
"customName": "install mise packages: pipx, python"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -53,36 +89,16 @@
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"commands": [
{
"path": "/root/.local/bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [
"*"
],
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
{
"commands": [
{
"cmd": "pipx install poetry"
},
{
"cmd": "poetry config virtualenvs.create false"
"path": "/root/.local/bin"
},
{
"cmd": "poetry config virtualenvs.in-project true"
},
{
"dest": "pyproject.toml",
@@ -93,16 +109,41 @@
"src": "poetry.lock"
},
{
"cmd": "poetry install --no-interaction --no-ansi --no-root"
"cmd": "poetry install --no-interaction --no-ansi --only main --no-root"
},
{
"dest": ".",
"src": "."
},
{
"path": "/app/.venv/bin"
}
],
"dependsOn": [
"packages:mise",
"setup",
"packages:apt:python-system-deps"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": []
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
{
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
@@ -111,12 +152,16 @@
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y pkg-config'",
"customName": "install apt packages: pkg-config"
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y '",
"customName": "install apt packages: "
}
],
"name": "packages:apt:python-system-deps",
"secrets": []
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:python-runtime-deps"
}
]
}
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
@@ -14,18 +13,60 @@
"type": "shared"
}
},
"start": {
"cmd": "python main.py",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "packages:python-runtime-deps"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise",
".python-version"
],
"step": "packages:mise"
},
{
"include": [
"/opt/python-packages",
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "python main.py",
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
"steps": [
{
"assets": {
"mise.toml": "[mise.toml]"
},
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libpq-dev python3-dev'",
"customName": "install apt packages: libpq-dev python3-dev"
},
{
"path": "/mise/shims"
},
@@ -43,17 +84,12 @@
"customName": "install mise packages: python"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise",
"/app/.python-version"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -62,29 +98,6 @@
"MISE_SHIMS_DIR": "/mise/shims"
}
},
{
"commands": [
{
"path": "/root/.local/bin"
}
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [
"*"
],
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
{
"caches": [
"pip"
@@ -95,16 +108,36 @@
"src": "requirements.txt"
},
{
"cmd": "pip install -r requirements.txt"
"cmd": "pip install --target=/opt/python-packages -r requirements.txt"
}
],
"dependsOn": [
"packages:mise",
"setup",
"packages:apt:python-system-deps"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": []
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONPATH": "/opt/python-packages",
"PYTHONUNBUFFERED": "1"
}
},
{
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
@@ -113,12 +146,16 @@
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y ffmpeg gcc pkg-config poppler-utils'",
"customName": "install apt packages: ffmpeg gcc pkg-config poppler-utils"
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y ffmpeg poppler-utils'",
"customName": "install apt packages: ffmpeg poppler-utils"
}
],
"name": "packages:apt:python-system-deps",
"secrets": []
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:python-runtime-deps"
}
]
}
@@ -1,5 +1,4 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
@@ -8,20 +7,65 @@
"apt-lists": {
"directory": "/var/lib/apt/lists",
"type": "locked"
},
"uv": {
"directory": "/opt/uv-cache",
"type": "shared"
}
},
"start": {
"cmd": "gunicorn --bind 0.0.0.0:3333 main:app",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"step": "packages:python-runtime-deps"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise",
".python-version"
],
"step": "packages:mise"
},
{
"include": [
"."
],
"step": "build"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "gunicorn --bind 0.0.0.0:3333 main:app",
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
"steps": [
{
"assets": {
"mise.toml": "[mise.toml]"
},
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y libpq-dev python3-dev'",
"customName": "install apt packages: libpq-dev python3-dev"
},
{
"path": "/mise/shims"
},
@@ -39,17 +83,12 @@
"customName": "install mise packages: pipx, python"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise",
"/app/.python-version"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -59,33 +98,16 @@
}
},
{
"commands": [
{
"path": "/root/.local/bin"
}
"caches": [
"uv"
],
"dependsOn": [
"packages:mise"
],
"name": "setup",
"secrets": [
"*"
],
"variables": {
"PIP_CACHE_DIR": "/opt/pip-cache",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1"
}
},
{
"commands": [
{
"cmd": "pipx install uv"
},
{
"path": "/root/.local/bin"
},
{
"dest": "pyproject.toml",
"src": "pyproject.toml"
@@ -95,32 +117,52 @@
"src": "uv.lock"
},
{
"cmd": "uv sync --frozen --no-install-project --no-install-workspace --no-dev"
"cmd": "uv sync --locked --no-dev --no-install-project"
},
{
"dest": ".",
"src": "."
},
{
"cmd": "uv sync --frozen --no-dev"
"cmd": "uv sync --locked --no-dev --no-editable"
},
{
"path": "/app/.venv/bin"
}
],
"dependsOn": [
"packages:mise",
"setup",
"packages:apt:python-system-deps"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "install",
"secrets": [],
"secrets": [
"*"
],
"variables": {
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PYTHONDONTWRITEBYTECODE": "1",
"PYTHONFAULTHANDLER": "1",
"PYTHONHASHSEED": "random",
"PYTHONUNBUFFERED": "1",
"UV_CACHE_DIR": "/opt/uv-cache",
"UV_COMPILE_BYTECODE": "1",
"UV_LINK_MODE": "copy"
"UV_LINK_MODE": "copy",
"UV_PYTHON_DOWNLOADS": "never"
}
},
{
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
"apt",
@@ -128,12 +170,16 @@
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y pkg-config'",
"customName": "install apt packages: pkg-config"
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y '",
"customName": "install apt packages: "
}
],
"name": "packages:apt:python-system-deps",
"secrets": []
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
}
],
"name": "packages:python-runtime-deps"
}
]
}
@@ -1,15 +1,42 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"."
],
"step": "defaultsToUsing"
},
{
"include": [
"."
],
"step": "doesNotUseSecrets"
},
{
"include": [
"."
],
"step": "usesSecrets"
}
],
"startCommand": "./run.sh"
},
"secrets": [
"MY_SECRET",
"MY_OTHER_SECRET",
"HELLO_WORLD"
],
"start": {
"cmd": "./run.sh"
},
"steps": [
{
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"name": "packages:mise",
"secrets": [
"*"
@@ -26,13 +53,12 @@
"customName": "./run.sh"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "defaultsToUsing",
"outputs": [
"/app"
],
"secrets": [
"*"
],
@@ -51,14 +77,12 @@
"customName": "./run.sh true"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "doesNotUseSecrets",
"outputs": [
"/app"
],
"secrets": []
"name": "doesNotUseSecrets"
},
{
"commands": [
@@ -71,13 +95,12 @@
"customName": "./run.sh"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "usesSecrets",
"outputs": [
"/app"
],
"secrets": [
"MY_SECRET",
"MY_OTHER_SECRET"
@@ -1,10 +1,33 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"start": {
"cmd": "caddy run --config Caddyfile --adapter caddyfile 2\u003e\u00261",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"include": [
"."
],
"step": "setup"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "caddy run --config Caddyfile --adapter caddyfile 2\u003e\u00261"
},
"steps": [
{
@@ -25,16 +48,12 @@
"customName": "install mise packages: caddy"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -56,8 +75,10 @@
"cmd": "caddy fmt --overwrite Caddyfile"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "setup",
"secrets": [
@@ -1,10 +1,33 @@
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"start": {
"cmd": "caddy run --config Caddyfile --adapter caddyfile 2\u003e\u00261",
"outputs": [
"."
]
"deploy": {
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-runtime-base:latest"
},
{
"include": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
],
"step": "packages:mise"
},
{
"include": [
"."
],
"step": "setup"
},
{
"include": [
"."
],
"local": true
}
],
"startCommand": "caddy run --config Caddyfile --adapter caddyfile 2\u003e\u00261"
},
"steps": [
{
@@ -25,16 +48,12 @@
"customName": "install mise packages: caddy"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -56,8 +75,10 @@
"cmd": "caddy fmt --overwrite Caddyfile"
}
],
"dependsOn": [
"packages:mise"
"inputs": [
{
"step": "packages:mise"
}
],
"name": "setup",
"secrets": [
+16 -23
View File
@@ -6,30 +6,22 @@ import (
"github.com/railwayapp/railpack/core/utils"
)
type DeployConfig struct {
AptPackages []string `json:"aptPackages,omitempty" jsonschema:"description=List of apt packages to include at runtime"`
Inputs []plan.Input `json:"inputs,omitempty" jsonschema:"description=The inputs for the deploy step"`
StartCmd string `json:"startCommand,omitempty" jsonschema:"description=The command to run in the container"`
Variables map[string]string `json:"variables,omitempty" jsonschema:"description=The variables available to this step. The key is the name of the variable that is referenced in a variable command"`
Paths []string `json:"paths,omitempty" jsonschema:"description=The paths to prepend to the $PATH environment variable"`
}
type Config struct {
// The base image to use for the build
BaseImage string `json:"baseImage,omitempty" jsonschema:"description=The base image to use for the build"`
// List of providers to use
Providers *[]string `json:"providers,omitempty" jsonschema:"description=List of providers to use"`
// Map of step names to step definitions
Steps map[string]*plan.Step `json:"steps,omitempty" jsonschema:"description=Map of step names to step definitions"`
// Start configuration
Start plan.Start `json:"start,omitempty" jsonschema:"description=Start configuration"`
// Map of package name to package version
Packages map[string]string `json:"packages,omitempty" jsonschema:"description=Map of package name to package version"`
// List of apt packages to install
AptPackages []string `json:"aptPackages,omitempty" jsonschema:"description=List of apt packages to install"`
// Map of cache name to cache definitions. The cache key can be referenced in an exec command.
Caches map[string]*plan.Cache `json:"caches,omitempty" jsonschema:"description=Map of cache name to cache definitions. The cache key can be referenced in an exec command"`
// Secrets that should be made available to commands that have useSecrets set to true
Secrets []string `json:"secrets,omitempty" jsonschema:"description=Secrets that should be made available to commands that have useSecrets set to true"`
Provider *string `json:"provider" jsonschema:"description=The provider to use"`
BuildAptPackages []string `json:"buildAptPackages,omitempty" jsonschema:"description=List of apt packages to install during the build step"`
Steps map[string]*plan.Step `json:"steps,omitempty" jsonschema:"description=Map of step names to step definitions"`
Deploy *DeployConfig `json:"deploy,omitempty" jsonschema:"description=Deploy configuration"`
Packages map[string]string `json:"packages,omitempty" jsonschema:"description=Map of package name to package version"`
Caches map[string]*plan.Cache `json:"caches,omitempty" jsonschema:"description=Map of cache name to cache definitions. The cache key can be referenced in an exec command"`
Secrets []string `json:"secrets,omitempty" jsonschema:"description=Secrets that should be made available to commands that have useSecrets set to true"`
}
func EmptyConfig() *Config {
@@ -37,6 +29,7 @@ func EmptyConfig() *Config {
Steps: make(map[string]*plan.Step),
Packages: make(map[string]string),
Caches: make(map[string]*plan.Cache),
Deploy: &DeployConfig{},
}
}
+27 -28
View File
@@ -13,8 +13,13 @@ func TestEmptyConfig(t *testing.T) {
require.NotNil(t, config)
require.Empty(t, config.Caches)
require.Empty(t, config.Packages)
require.Empty(t, config.AptPackages)
require.Empty(t, config.BuildAptPackages)
require.Empty(t, config.Steps)
require.Nil(t, config.Provider)
require.NotNil(t, config.Deploy)
require.Nil(t, config.Deploy.Inputs)
require.Nil(t, config.Deploy.Paths)
}
func TestMergeConfigSmall(t *testing.T) {
@@ -37,7 +42,7 @@ func TestMergeConfigSmall(t *testing.T) {
"caches": ["pip"]
}
},
"start": {
"deploy": {
"variables": {
"SHARED": "one",
"HELLO": "world"
@@ -58,7 +63,7 @@ func TestMergeConfigSmall(t *testing.T) {
}
}
},
"start": {
"deploy": {
"variables": {
"SHARED": "two",
"FOO": "bar"
@@ -87,7 +92,7 @@ func TestMergeConfigSmall(t *testing.T) {
"caches": ["pip"]
}
},
"start": {
"deploy": {
"variables": {
"HELLO": "world",
"SHARED": "two",
@@ -131,13 +136,11 @@ func TestMergeConfig(t *testing.T) {
"steps": {
"install": {
"name": "install",
"useSecrets": true,
"outputs": ["node_modules/", "package-lock.json"],
"secrets": ["*"],
"assets": {
"package.json": "content1",
"requirements.txt": "content2"
},
"startingImage": "node:16",
"commands": [
{"type": "exec", "cmd": "npm install", "caches": ["npm"], "customName": "Install NPM deps"},
{"type": "path", "path": "/usr/local/bin"},
@@ -154,8 +157,8 @@ func TestMergeConfig(t *testing.T) {
]
}
},
"start": {
"command": "python app.py"
"deploy": {
"startCommand": "python app.py"
}
}`
@@ -179,25 +182,23 @@ func TestMergeConfig(t *testing.T) {
"steps": {
"install": {
"name": "install",
"useSecrets": true,
"outputs": ["dist/"],
"secrets": ["*"],
"assets": {
"package.json": "content3"
},
"startingImage": "node:18"
}
},
"build": {
"name": "build",
"useSecrets": false,
"secrets": [],
"commands": [
{"type": "exec", "cmd": "config 2 a"},
{"type": "exec", "cmd": "config 2 b"}
]
}
},
"start": {
"baseImage": "node:18",
"command": "node server.js",
"deploy": {
"aptPackages": ["curl"],
"startCommand": "node server.js",
"paths": ["/usr/local/bin", "/app/bin"]
}
}`
@@ -226,13 +227,11 @@ func TestMergeConfig(t *testing.T) {
"steps": {
"install": {
"name": "install",
"useSecrets": true,
"outputs": ["dist/"],
"secrets": ["*"],
"assets": {
"package.json": "content3",
"requirements.txt": "content2"
},
"startingImage": "node:18",
"commands": [
{"type": "exec", "cmd": "npm install", "caches": ["npm"], "customName": "Install NPM deps"},
{"type": "path", "path": "/usr/local/bin"},
@@ -243,16 +242,16 @@ func TestMergeConfig(t *testing.T) {
},
"build": {
"name": "build",
"useSecrets": false,
"secrets": [],
"commands": [
{"type": "exec", "cmd": "config 2 a"},
{"type": "exec", "cmd": "config 2 b"}
]
}
},
"start": {
"baseImage": "node:18",
"command": "node server.js",
"deploy": {
"aptPackages": ["curl"],
"startCommand": "node server.js",
"paths": ["/usr/local/bin", "/app/bin"]
}
}`
@@ -271,8 +270,8 @@ func TestMergeConfig(t *testing.T) {
func TestMergeConfigStart(t *testing.T) {
config1JSON := `{
"start": {
"command": "python app.py"
"deploy": {
"startCommand": "python app.py"
}
}`
@@ -286,8 +285,8 @@ func TestMergeConfigStart(t *testing.T) {
"packages": {
"node": "23"
},
"start": {
"command": "python app.py"
"deploy": {
"startCommand": "python app.py"
},
"steps": {},
"caches": {}
+27 -39
View File
@@ -33,7 +33,13 @@ type BuildResult struct {
}
func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuildPlanOptions) (*BuildResult, error) {
ctx, err := generate.NewGenerateContext(app, env)
// Get the full user config based on file config, env config, and options
config, err := GetConfig(app, env, options)
if err != nil {
return nil, err
}
ctx, err := generate.NewGenerateContext(app, env, config)
if err != nil {
return nil, err
}
@@ -45,12 +51,6 @@ func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuil
}
}
// Get the full user config based on file config, env config, and options
config, err := GetConfig(app, env, options)
if err != nil {
return nil, err
}
// Figure out what providers to use
providersToUse, detectedProviderNames := getProviders(ctx, config)
providerNames := make([]string, len(providersToUse))
@@ -82,11 +82,6 @@ func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuil
return nil, fmt.Errorf("failed to run procfile provider: %w", err)
}
// Update the context with the config
if err := ctx.ApplyConfig(config); err != nil {
return nil, fmt.Errorf("failed to apply config: %w", err)
}
buildPlan, resolvedPackages, err := ctx.Generate()
if err != nil {
return nil, fmt.Errorf("failed to generate build plan: %w", err)
@@ -153,24 +148,16 @@ func GenerateConfigFromEnvironment(app *app.App, env *app.Environment) *config.C
return config
}
if installCmdVar, _ := env.GetConfigVariable("INSTALL_CMD"); installCmdVar != "" {
installStep := config.GetOrCreateStep("install")
installStep.Commands = []plan.Command{plan.NewExecShellCommand(installCmdVar, plan.ExecOptions{CustomName: installCmdVar})}
installStep.DependsOn = append(installStep.DependsOn, "packages")
}
if buildCmdVar, _ := env.GetConfigVariable("BUILD_CMD"); buildCmdVar != "" {
buildStep := config.GetOrCreateStep("build")
buildStep.Commands = []plan.Command{
// We want to run the build command with all the files in the current directory
plan.NewCopyCommand("."),
plan.NewExecShellCommand(buildCmdVar, plan.ExecOptions{CustomName: buildCmdVar}),
}
buildStep.DependsOn = append(buildStep.DependsOn, "install")
}
if startCmdVar, _ := env.GetConfigVariable("START_CMD"); startCmdVar != "" {
config.Start.Command = startCmdVar
config.Deploy.StartCmd = startCmdVar
}
if envPackages, _ := env.GetConfigVariable("PACKAGES"); envPackages != "" {
@@ -181,8 +168,12 @@ func GenerateConfigFromEnvironment(app *app.App, env *app.Environment) *config.C
}
}
if envAptPackages, _ := env.GetConfigVariable("APT_PACKAGES"); envAptPackages != "" {
config.AptPackages = strings.Split(envAptPackages, " ")
if envAptPackages, _ := env.GetConfigVariable("BUILD_APT_PACKAGES"); envAptPackages != "" {
config.BuildAptPackages = strings.Split(envAptPackages, " ")
}
if envAptPackages, _ := env.GetConfigVariable("DEPLOY_APT_PACKAGES"); envAptPackages != "" {
config.Deploy.AptPackages = strings.Split(envAptPackages, " ")
}
for name := range env.Variables {
@@ -206,11 +197,10 @@ func GenerateConfigFromOptions(options *GenerateBuildPlanOptions) *config.Config
plan.NewCopyCommand("."),
plan.NewExecShellCommand(options.BuildCommand, plan.ExecOptions{CustomName: options.BuildCommand}),
}
buildStep.DependsOn = append(buildStep.DependsOn, "install")
}
if options.StartCommand != "" {
config.Start.Command = options.StartCommand
config.Deploy.StartCmd = options.StartCommand
}
return config
@@ -234,7 +224,7 @@ func getProviders(ctx *generate.GenerateContext, config *config.Config) ([]provi
detectedProviders = append(detectedProviders, provider.Name())
// If there are no providers manually specified in the config,
if config.Providers == nil {
if config.Provider == nil {
if err := provider.Initialize(ctx); err != nil {
log.Warnf("Failed to initialize provider `%s`: %s", provider.Name(), err.Error())
continue
@@ -246,20 +236,18 @@ func getProviders(ctx *generate.GenerateContext, config *config.Config) ([]provi
}
}
if config.Providers != nil {
for _, providerName := range *config.Providers {
provider := providers.GetProvider(providerName)
if provider == nil {
log.Warnf("Provider `%s` not found", providerName)
continue
}
if err := provider.Initialize(ctx); err != nil {
log.Warnf("Failed to initialize provider `%s`: %s", providerName, err.Error())
continue
}
providersToUse = append(providersToUse, provider)
if config.Provider != nil {
provider := providers.GetProvider(*config.Provider)
if provider == nil {
log.Warnf("Provider `%s` not found", *config.Provider)
return providersToUse, detectedProviders
}
if err := provider.Initialize(ctx); err != nil {
log.Warnf("Failed to initialize provider `%s`: %s", *config.Provider, err.Error())
return providersToUse, detectedProviders
}
providersToUse = append(providersToUse, provider)
}
return providersToUse, detectedProviders
+31 -52
View File
@@ -1,7 +1,6 @@
[TestGenerateContext - 1]
{
"baseImage": "ghcr.io/railwayapp/railpack-runtime-base:latest",
"caches": {
"apt": {
"directory": "/var/cache/apt",
@@ -12,33 +11,30 @@
"type": "locked"
}
},
"secrets": [
"RAILWAY_SECRET_1",
"RAILWAY_SECRET_2"
],
"start": {
"cmd": "npm run start",
"deploy": {
"inputs": [
{
"step": "build"
}
],
"startCommand": "echo hello",
"variables": {
"HELLO": "world"
}
},
"secrets": [
"RAILWAY_SECRET_1",
"RAILWAY_SECRET_2"
],
"steps": [
{
"assets": {
"mise.toml": "[tools]\n [tools.go]\n version = \"1.23.5\"\n [tools.node]\n version = \"20.18.2\"\n [tools.python]\n version = \"3.13.1\"\n"
},
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"path": "/mise/shims"
},
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y curl'",
"customName": "install apt packages: curl"
},
{
"customName": "create mise config",
"name": "mise.toml",
@@ -49,16 +45,12 @@
"customName": "install mise packages: go, node, python"
}
],
"name": "packages:mise",
"outputs": [
"/mise/shims",
"/mise/installs",
"/usr/local/bin/mise",
"/etc/mise/config.toml",
"/root/.local/state/mise"
"inputs": [
{
"image": "ghcr.io/railwayapp/railpack-builder-base:latest"
}
],
"secrets": [],
"startingImage": "ghcr.io/railwayapp/railpack-builder-base:latest",
"name": "packages:mise",
"variables": {
"MISE_CACHE_DIR": "/mise/cache",
"MISE_CONFIG_DIR": "/mise",
@@ -78,8 +70,12 @@
"customName": "install apt packages: git neofetch"
}
],
"name": "packages:apt:test",
"secrets": []
"inputs": [
{
"step": "packages:mise"
}
],
"name": "packages:test"
},
{
"commands": [
@@ -87,16 +83,12 @@
"cmd": "npm install"
}
],
"dependsOn": [
"packages:apt:test"
"inputs": [
{
"step": "packages:test"
}
],
"name": "install",
"outputs": [
"node_modules"
],
"secrets": [
"*"
]
"name": "install"
},
{
"commands": [
@@ -105,28 +97,15 @@
"customName": "echo building"
}
],
"dependsOn": [
"install",
"packages:apt:config"
"inputs": [
{
"step": "install"
}
],
"name": "build",
"secrets": [
"*"
]
},
{
"caches": [
"apt",
"apt-lists"
],
"commands": [
{
"cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y curl'",
"customName": "install apt packages: curl"
}
],
"name": "packages:apt:config",
"secrets": []
}
]
}
+8 -5
View File
@@ -4,20 +4,19 @@ import (
"fmt"
"github.com/railwayapp/railpack/core/plan"
"github.com/railwayapp/railpack/core/utils"
)
type AptStepBuilder struct {
DisplayName string
DependsOn []string
Packages []string
Inputs []plan.Input
}
func (c *GenerateContext) NewAptStepBuilder(name string) *AptStepBuilder {
step := &AptStepBuilder{
DisplayName: c.GetStepName(fmt.Sprintf("packages:apt:%s", name)),
DependsOn: []string{},
DisplayName: c.GetStepName(fmt.Sprintf("packages:%s", name)),
Packages: []string{},
Inputs: []plan.Input{},
}
c.Steps = append(c.Steps, step)
@@ -25,6 +24,10 @@ func (c *GenerateContext) NewAptStepBuilder(name string) *AptStepBuilder {
return step
}
func (b *AptStepBuilder) AddInput(input plan.Input) {
b.Inputs = append(b.Inputs, input)
}
func (b *AptStepBuilder) AddAptPackage(pkg string) {
b.Packages = append(b.Packages, pkg)
}
@@ -35,13 +38,13 @@ func (b *AptStepBuilder) Name() string {
func (b *AptStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) {
step := plan.NewStep(b.DisplayName)
step.DependsOn = utils.RemoveDuplicates(b.DependsOn)
step.AddCommands([]plan.Command{
options.NewAptInstallCommand(b.Packages),
})
step.Caches = options.Caches.GetAptCaches()
step.Inputs = b.Inputs
// Does not use any secrets
step.Secrets = []string{}
+23 -8
View File
@@ -3,16 +3,15 @@ package generate
import (
"maps"
"github.com/charmbracelet/log"
a "github.com/railwayapp/railpack/core/app"
"github.com/railwayapp/railpack/core/plan"
"github.com/railwayapp/railpack/core/utils"
)
type CommandStepBuilder struct {
DisplayName string
DependsOn []string
Commands []plan.Command
Outputs []string
Inputs []plan.Input
Assets map[string]string
Variables map[string]string
Caches []string
@@ -24,7 +23,7 @@ type CommandStepBuilder struct {
func (c *GenerateContext) NewCommandStep(name string) *CommandStepBuilder {
step := &CommandStepBuilder{
DisplayName: c.GetStepName(name),
DependsOn: []string{MisePackageStepName},
Inputs: []plan.Input{},
Commands: []plan.Command{},
Assets: map[string]string{},
Variables: map[string]string{},
@@ -34,13 +33,25 @@ func (c *GenerateContext) NewCommandStep(name string) *CommandStepBuilder {
env: c.Env,
}
// Remove any existing step with the same name
for i, existingStep := range c.Steps {
if existingStep.Name() == step.Name() {
c.Steps = append(c.Steps[:i], c.Steps[i+1:]...)
break
}
}
c.Steps = append(c.Steps, step)
return step
}
func (b *CommandStepBuilder) DependOn(name string) {
b.DependsOn = append(b.DependsOn, name)
func (b *CommandStepBuilder) AddInput(input plan.Input) {
b.Inputs = append(b.Inputs, input)
}
func (b *CommandStepBuilder) AddInputs(inputs []plan.Input) {
b.Inputs = append(b.Inputs, inputs...)
}
func (b *CommandStepBuilder) AddVariables(variables map[string]string) {
@@ -48,6 +59,11 @@ func (b *CommandStepBuilder) AddVariables(variables map[string]string) {
}
func (b *CommandStepBuilder) AddCache(name string) {
if name == "" {
log.Error("Cache name is empty")
return
}
b.Caches = append(b.Caches, name)
}
@@ -98,8 +114,7 @@ func (b *CommandStepBuilder) Name() string {
func (b *CommandStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) {
step := plan.NewStep(b.DisplayName)
step.DependsOn = utils.RemoveDuplicates(b.DependsOn)
step.Outputs = b.Outputs
step.Inputs = b.Inputs
step.Commands = b.Commands
step.Assets = b.Assets
step.Caches = b.Caches
+134 -138
View File
@@ -16,10 +16,6 @@ import (
"github.com/railwayapp/railpack/core/utils"
)
const (
DefaultBaseImage = "ghcr.io/railwayapp/railpack-runtime-base:latest"
)
type BuildStepOptions struct {
ResolvedPackages map[string]*resolver.ResolvedPackage
Caches *CacheContext
@@ -31,49 +27,53 @@ type StepBuilder interface {
}
type GenerateContext struct {
App *a.App
Env *a.Environment
App *a.App
Env *a.Environment
Config *config.Config
BaseImage string
Steps []StepBuilder
Start StartContext
Deploy *DeployBuilder
Caches *CacheContext
Secrets []string
SubContexts []string
Metadata *Metadata
Resolver *resolver.Resolver
miseStepBuilder *MiseStepBuilder
Metadata *Metadata
Resolver *resolver.Resolver
MiseStepBuilder *MiseStepBuilder
}
func NewGenerateContext(app *a.App, env *a.Environment) (*GenerateContext, error) {
func NewGenerateContext(app *a.App, env *a.Environment, config *config.Config) (*GenerateContext, error) {
resolver, err := resolver.NewResolver(mise.InstallDir)
if err != nil {
return nil, err
}
return &GenerateContext{
App: app,
Env: env,
BaseImage: DefaultBaseImage,
Steps: make([]StepBuilder, 0),
Start: *NewStartContext(),
Caches: NewCacheContext(),
Secrets: []string{},
Metadata: NewMetadata(),
Resolver: resolver,
}, nil
ctx := &GenerateContext{
App: app,
Env: env,
Config: config,
Steps: make([]StepBuilder, 0),
Deploy: NewDeployBuilder(),
Caches: NewCacheContext(),
Secrets: []string{},
Metadata: NewMetadata(),
Resolver: resolver,
}
// The default runtime image should include the runtime apt packages
ctx.Deploy.Inputs = append(ctx.Deploy.Inputs, ctx.DefaultRuntimeInput())
return ctx, nil
}
func (c *GenerateContext) GetMiseStepBuilder() *MiseStepBuilder {
if c.miseStepBuilder == nil {
c.miseStepBuilder = c.newMiseStepBuilder()
if c.MiseStepBuilder == nil {
c.MiseStepBuilder = c.newMiseStepBuilder()
}
return c.miseStepBuilder
return c.MiseStepBuilder
}
func (c *GenerateContext) EnterSubContext(subContext string) *GenerateContext {
@@ -109,16 +109,24 @@ func (c *GenerateContext) ResolvePackages() (map[string]*resolver.ResolvedPackag
// Generate a build plan from the context
func (c *GenerateContext) Generate() (*plan.BuildPlan, map[string]*resolver.ResolvedPackage, error) {
// Add all packages from the config to the mise step
miseStep := c.GetMiseStepBuilder()
for _, pkg := range slices.Sorted(maps.Keys(c.Config.Packages)) {
version := c.Config.Packages[pkg]
pkgRef := miseStep.Default(pkg, version)
miseStep.Version(pkgRef, version, "custom config")
}
c.applyConfig()
// Resolve all package versions into a fully qualified and valid version
resolvedPackages, err := c.ResolvePackages()
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve packages: %w", err)
}
// Generate the plan based on the context and resolved packages
// Create the actual build plan
buildPlan := plan.NewBuildPlan()
buildPlan.BaseImage = c.BaseImage
buildStepOptions := &BuildStepOptions{
ResolvedPackages: resolvedPackages,
@@ -136,122 +144,28 @@ func (c *GenerateContext) Generate() (*plan.BuildPlan, map[string]*resolver.Reso
}
buildPlan.Caches = c.Caches.Caches
buildPlan.Secrets = utils.RemoveDuplicates(c.Secrets)
buildPlan.Start.BaseImage = c.Start.BaseImage
buildPlan.Start.Command = c.Start.Command
buildPlan.Start.Outputs = utils.RemoveDuplicates(c.Start.outputs)
buildPlan.Start.Paths = utils.RemoveDuplicates(c.Start.paths)
buildPlan.Start.Variables = c.Start.variables
buildPlan.Deploy = c.Deploy.Build()
return buildPlan, resolvedPackages, nil
}
func (c *GenerateContext) ApplyConfig(config *config.Config) error {
// Mise package config
miseStep := c.GetMiseStepBuilder()
for _, pkg := range slices.Sorted(maps.Keys(config.Packages)) {
version := config.Packages[pkg]
pkgRef := miseStep.Default(pkg, version)
miseStep.Version(pkgRef, version, "custom config")
func (c *GenerateContext) DefaultRuntimeInput() plan.Input {
return c.DefaultRuntimeInputWithPackages([]string{})
}
func (c *GenerateContext) DefaultRuntimeInputWithPackages(additionalAptPackages []string) plan.Input {
aptPackages := append(c.Config.Deploy.AptPackages, additionalAptPackages...)
if len(aptPackages) == 0 {
return plan.NewImageInput(plan.RAILPACK_RUNTIME_IMAGE)
}
// Apt package config
aptStepName := ""
if len(config.AptPackages) > 0 {
aptStep := c.NewAptStepBuilder("config")
aptStepName = aptStep.Name()
aptStep.Packages = config.AptPackages
runtimeAptStep := c.NewAptStepBuilder("runtime")
runtimeAptStep.Packages = aptPackages
runtimeAptStep.AddInput(plan.NewImageInput(plan.RAILPACK_RUNTIME_IMAGE))
// We install the apt packages again in the mise step since they may be required for install mise packages
miseStep.SupportingAptPackages = append(miseStep.SupportingAptPackages, config.AptPackages...)
}
// Step config
for _, name := range slices.Sorted(maps.Keys(config.Steps)) {
configStep := config.Steps[name]
var commandStepBuilder *CommandStepBuilder
// We need to use the key as the step name and not `configStep.Name`
if existingStep := c.GetStepByName(name); existingStep != nil {
if csb, ok := (*existingStep).(*CommandStepBuilder); ok {
commandStepBuilder = csb
} else {
log.Warnf("Step `%s` exists, but it is not a command step. Skipping...", name)
continue
}
} else {
commandStepBuilder = c.NewCommandStep(name)
}
// Overwrite the step with values from the config if they exist
if len(configStep.DependsOn) > 0 {
commandStepBuilder.DependsOn = configStep.DependsOn
}
if aptStepName != "" {
commandStepBuilder.DependsOn = append(commandStepBuilder.DependsOn, aptStepName)
}
if configStep.Commands != nil {
commandStepBuilder.Commands = configStep.Commands
}
if configStep.Outputs != nil {
commandStepBuilder.Outputs = configStep.Outputs
}
for k, v := range configStep.Assets {
commandStepBuilder.Assets[k] = v
}
if configStep.Secrets != nil {
commandStepBuilder.Secrets = configStep.Secrets
}
// if configStep.UseSecrets != nil {
// commandStepBuilder.UseSecrets = *configStep.UseSecrets
// }
if len(configStep.Caches) > 0 {
commandStepBuilder.Caches = configStep.Caches
}
if configStep.Variables != nil {
commandStepBuilder.AddEnvVars(configStep.Variables)
}
}
// Cache config
for name, cache := range config.Caches {
c.Caches.SetCache(name, cache)
}
// Secret config
c.Secrets = append(c.Secrets, config.Secrets...)
// Start config
if config.Start.BaseImage != "" {
c.Start.BaseImage = config.Start.BaseImage
}
if config.Start.Command != "" {
c.Start.Command = config.Start.Command
}
if config.Start.Variables != nil {
c.Start.AddEnvVars(config.Start.Variables)
}
c.Start.AddPaths(config.Start.Paths)
if len(config.Start.Outputs) > 0 {
c.Start.outputs = config.Start.Outputs
}
return nil
return plan.NewStepInput(runtimeAptStep.Name())
}
func (o *BuildStepOptions) NewAptInstallCommand(pkgs []string) plan.Command {
@@ -262,3 +176,85 @@ func (o *BuildStepOptions) NewAptInstallCommand(pkgs []string) plan.Command {
CustomName: "install apt packages: " + strings.Join(pkgs, " "),
})
}
func (c *GenerateContext) applyConfig() {
miseStep := c.GetMiseStepBuilder()
// Apply the cache config to the context
maps.Copy(c.Caches.Caches, c.Config.Caches)
// Apply step config to the context
for _, name := range slices.Sorted(maps.Keys(c.Config.Steps)) {
configStep := c.Config.Steps[name]
var commandStepBuilder *CommandStepBuilder
if existingStep := c.GetStepByName(name); existingStep != nil {
if csb, ok := (*existingStep).(*CommandStepBuilder); ok {
commandStepBuilder = csb
} else {
log.Warnf("Step `%s` exists, but it is not a command step. Skipping...", name)
continue
}
} else {
// If no build step found, create a new one
// Run the build in the builder context and copy the /app contents to the final image
commandStepBuilder = c.NewCommandStep(name)
commandStepBuilder.AddInput(plan.NewStepInput(miseStep.Name()))
c.Deploy.Inputs = append(c.Deploy.Inputs, plan.NewStepInput(commandStepBuilder.Name(), plan.InputOptions{
Include: []string{"."},
}))
}
if configStep.Commands != nil {
commandStepBuilder.Commands = configStep.Commands
}
if configStep.Inputs != nil {
commandStepBuilder.Inputs = configStep.Inputs
}
for k, v := range configStep.Assets {
commandStepBuilder.Assets[k] = v
}
if configStep.Secrets != nil {
commandStepBuilder.Secrets = configStep.Secrets
}
if len(configStep.Caches) > 0 {
commandStepBuilder.Caches = configStep.Caches
}
if configStep.Variables != nil {
commandStepBuilder.AddEnvVars(configStep.Variables)
}
// Secret config
if configStep.Secrets != nil {
commandStepBuilder.Secrets = configStep.Secrets
}
}
if c.Config.Secrets != nil {
c.Secrets = c.Config.Secrets
}
// Update deploy from config
if c.Config.Deploy != nil {
if c.Config.Deploy.StartCmd != "" {
c.Deploy.StartCmd = c.Config.Deploy.StartCmd
}
if c.Config.Deploy.Inputs != nil {
c.Deploy.Inputs = c.Config.Deploy.Inputs
}
if c.Config.Deploy.Paths != nil {
c.Deploy.Paths = c.Config.Deploy.Paths
}
maps.Copy(c.Deploy.Variables, c.Config.Deploy.Variables)
}
}
+15 -27
View File
@@ -21,20 +21,23 @@ func (p *TestProvider) Plan(ctx *GenerateContext) error {
// apt
aptStep := ctx.NewAptStepBuilder("test")
aptStep.AddInput(plan.NewStepInput(mise.Name()))
aptStep.AddAptPackage("git")
aptStep.AddAptPackage("neofetch")
// commands
installStep := ctx.NewCommandStep("install")
installStep.AddCommand(plan.NewExecCommand("npm install", plan.ExecOptions{}))
installStep.Outputs = []string{"node_modules"}
installStep.DependsOn = []string{aptStep.Name()}
installStep.AddInput(plan.NewStepInput(aptStep.Name()))
installStep.Secrets = []string{}
buildStep := ctx.NewCommandStep("build")
buildStep.AddCommand(plan.NewExecCommand("npm run build", plan.ExecOptions{}))
buildStep.DependsOn = []string{installStep.Name()}
buildStep.AddInput(plan.NewStepInput(installStep.Name()))
ctx.Start.Command = "npm run start"
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(buildStep.Name()),
}
return nil
}
@@ -46,8 +49,9 @@ func CreateTestContext(t *testing.T, path string) *GenerateContext {
require.NoError(t, err)
env := app.NewEnvironment(nil)
config := config.EmptyConfig()
ctx, err := NewGenerateContext(userApp, env)
ctx, err := NewGenerateContext(userApp, env, config)
require.NoError(t, err)
return ctx
@@ -72,39 +76,23 @@ func TestGenerateContext(t *testing.T) {
}
},
"secrets": ["RAILWAY_SECRET_1", "RAILWAY_SECRET_2"],
"start": {
"deploy": {
"startCommand": "echo hello",
"variables": {
"HELLO": "world"
}
}
}`
var cfg config.Config
require.NoError(t, json.Unmarshal([]byte(configJSON), &cfg))
var config config.Config
require.NoError(t, json.Unmarshal([]byte(configJSON), &config))
// Apply the config to the context
require.NoError(t, ctx.ApplyConfig(&cfg))
// Resolve packages
resolvedPkgs, err := ctx.ResolvePackages()
require.NoError(t, err)
// Generate a plan
buildOpts := &BuildStepOptions{
ResolvedPackages: resolvedPkgs,
Caches: ctx.Caches,
}
// Build and verify each step
for _, builder := range ctx.Steps {
_, err := builder.Build(buildOpts)
require.NoError(t, err)
}
ctx.Config = &config
buildPlan, _, err := ctx.Generate()
require.NoError(t, err)
buildPlanJSON, err := json.Marshal(buildPlan)
buildPlanJSON, err := json.MarshalIndent(buildPlan, "", " ")
require.NoError(t, err)
var actualPlan map[string]interface{}
+30
View File
@@ -0,0 +1,30 @@
package generate
import "github.com/railwayapp/railpack/core/plan"
type DeployBuilder struct {
Inputs []plan.Input
StartCmd string
Variables map[string]string
Paths []string
AptPackages []string
}
func NewDeployBuilder() *DeployBuilder {
return &DeployBuilder{
Inputs: []plan.Input{},
StartCmd: "",
Variables: map[string]string{},
Paths: []string{},
AptPackages: []string{},
}
}
func (b *DeployBuilder) Build() plan.Deploy {
return plan.Deploy{
Inputs: b.Inputs,
StartCmd: b.StartCmd,
Variables: b.Variables,
Paths: b.Paths,
}
}
+12 -5
View File
@@ -9,8 +9,8 @@ type ImageStepBuilder struct {
DisplayName string
Resolver *resolver.Resolver
Packages []*resolver.PackageRef
Outputs []string
ResolveStepImage func(options *BuildStepOptions) string
AptPackages []string
}
func (c *GenerateContext) NewImageStep(name string, resolveStepImage func(options *BuildStepOptions) string) *ImageStepBuilder {
@@ -46,11 +46,18 @@ func (b *ImageStepBuilder) Name() string {
}
func (b *ImageStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) {
step := plan.NewStep(b.DisplayName)
image := b.ResolveStepImage(options)
step.StartingImage = b.ResolveStepImage(options)
step.Outputs = b.Outputs
step.Secrets = []string{}
step := plan.NewStep(b.DisplayName)
step.Inputs = []plan.Input{
plan.NewImageInput(image),
}
if len(b.AptPackages) > 0 {
step.Commands = []plan.Command{
options.NewAptInstallCommand(b.AptPackages),
}
}
return step, nil
}
+55 -47
View File
@@ -14,7 +14,6 @@ import (
const (
MisePackageStepName = "packages:mise"
BuilderBaseImage = "ghcr.io/railwayapp/railpack-builder-base:latest"
)
type MiseStepBuilder struct {
@@ -24,20 +23,22 @@ type MiseStepBuilder struct {
MisePackages []*resolver.PackageRef
SupportingMiseFiles []string
Assets map[string]string
Outputs []string
Inputs []plan.Input
Variables map[string]string
app *a.App
env *a.Environment
}
func (c *GenerateContext) newMiseStepBuilder() *MiseStepBuilder {
supportingAptPackages := c.Config.BuildAptPackages
step := &MiseStepBuilder{
DisplayName: MisePackageStepName,
Resolver: c.Resolver,
MisePackages: []*resolver.PackageRef{},
SupportingAptPackages: []string{},
SupportingAptPackages: append(supportingAptPackages, c.Config.BuildAptPackages...),
Assets: map[string]string{},
Outputs: []string{"/mise/shims", "/mise/installs", "/usr/local/bin/mise", "/etc/mise/config.toml", "/root/.local/state/mise"},
Inputs: []plan.Input{},
Variables: map[string]string{},
app: c.App,
env: c.Env,
@@ -52,6 +53,10 @@ func (b *MiseStepBuilder) AddSupportingAptPackage(name string) {
b.SupportingAptPackages = append(b.SupportingAptPackages, name)
}
func (b *MiseStepBuilder) AddInput(input plan.Input) {
b.Inputs = append(b.Inputs, input)
}
func (b *MiseStepBuilder) Default(name string, defaultVersion string) resolver.PackageRef {
for _, pkg := range b.MisePackages {
if pkg.Name == name {
@@ -72,15 +77,32 @@ func (b *MiseStepBuilder) Name() string {
return b.DisplayName
}
func (b *MiseStepBuilder) GetOutputPaths() []string {
supportingMiseConfigFiles := b.GetSupportingMiseConfigFiles(b.app.Source)
files := []string{"/mise/shims", "/mise/installs", "/usr/local/bin/mise", "/etc/mise/config.toml", "/root/.local/state/mise"}
files = append(files, supportingMiseConfigFiles...)
return files
}
func (b *MiseStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) {
step := plan.NewStep(b.DisplayName)
step.Inputs = []plan.Input{
plan.NewImageInput(plan.RAILPACK_BUILDER_IMAGE),
}
// Setup apt commands
if len(b.SupportingAptPackages) > 0 {
step.AddCommands([]plan.Command{
options.NewAptInstallCommand(b.SupportingAptPackages),
})
step.Caches = options.Caches.GetAptCaches()
}
if len(b.MisePackages) == 0 {
return step, nil
}
step.StartingImage = BuilderBaseImage
// Setup mise
step.AddCommands([]plan.Command{
plan.NewPathCommand("/mise/shims"),
@@ -104,54 +126,40 @@ func (b *MiseStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) {
step.AddCommands([]plan.Command{
plan.NewCopyCommand(file, "/app/"+file),
})
// We want to make sure the file is copied into the next step
b.Outputs = append(b.Outputs, "/app/"+file)
}
// Setup apt commands
if len(b.SupportingAptPackages) > 0 {
step.AddCommands([]plan.Command{
options.NewAptInstallCommand(b.SupportingAptPackages),
})
step.Caches = options.Caches.GetAptCaches()
}
// Setup mise commands
if len(b.MisePackages) > 0 {
packagesToInstall := make(map[string]string)
for _, pkg := range b.MisePackages {
resolved, ok := options.ResolvedPackages[pkg.Name]
if ok && resolved.ResolvedVersion != nil {
packagesToInstall[pkg.Name] = *resolved.ResolvedVersion
}
packagesToInstall := make(map[string]string)
for _, pkg := range b.MisePackages {
resolved, ok := options.ResolvedPackages[pkg.Name]
if ok && resolved.ResolvedVersion != nil {
packagesToInstall[pkg.Name] = *resolved.ResolvedVersion
}
miseToml, err := mise.GenerateMiseToml(packagesToInstall)
if err != nil {
return nil, fmt.Errorf("failed to generate mise.toml: %w", err)
}
b.Assets["mise.toml"] = miseToml
pkgNames := make([]string, 0, len(packagesToInstall))
for k := range packagesToInstall {
pkgNames = append(pkgNames, k)
}
sort.Strings(pkgNames)
step.AddCommands([]plan.Command{
plan.NewFileCommand("/etc/mise/config.toml", "mise.toml", plan.FileOptions{
CustomName: "create mise config",
}),
plan.NewExecCommand("sh -c 'mise trust -a && mise install'", plan.ExecOptions{
CustomName: "install mise packages: " + strings.Join(pkgNames, ", "),
}),
})
}
miseToml, err := mise.GenerateMiseToml(packagesToInstall)
if err != nil {
return nil, fmt.Errorf("failed to generate mise.toml: %w", err)
}
b.Assets["mise.toml"] = miseToml
pkgNames := make([]string, 0, len(packagesToInstall))
for k := range packagesToInstall {
pkgNames = append(pkgNames, k)
}
sort.Strings(pkgNames)
step.AddCommands([]plan.Command{
plan.NewFileCommand("/etc/mise/config.toml", "mise.toml", plan.FileOptions{
CustomName: "create mise config",
}),
plan.NewExecCommand("sh -c 'mise trust -a && mise install'", plan.ExecOptions{
CustomName: "install mise packages: " + strings.Join(pkgNames, ", "),
}),
})
step.Assets = b.Assets
step.Outputs = b.Outputs
step.Secrets = []string{}
return step, nil
-29
View File
@@ -1,29 +0,0 @@
package generate
type StartContext struct {
BaseImage string
Command string
outputs []string
paths []string
variables map[string]string
}
func NewStartContext() *StartContext {
return &StartContext{
variables: make(map[string]string),
}
}
func (s *StartContext) AddEnvVars(envVars map[string]string) {
for k, v := range envVars {
s.variables[k] = v
}
}
func (s *StartContext) AddPaths(paths []string) {
s.paths = append(s.paths, paths...)
}
func (s *StartContext) AddOutputs(outputs []string) {
s.outputs = append(s.outputs, outputs...)
}
+6
View File
@@ -60,6 +60,12 @@ func NewExecCommand(cmd string, options ...ExecOptions) Command {
}
func NewExecShellCommand(cmd string, options ...ExecOptions) Command {
if len(options) == 0 {
options = []ExecOptions{
{CustomName: cmd},
}
}
exec := NewExecCommand("sh -c '"+cmd+"'", options...)
return exec
}
+18 -17
View File
@@ -1,34 +1,35 @@
package plan
const (
RAILPACK_BUILDER_IMAGE = "ghcr.io/railwayapp/railpack-builder-base:latest"
RAILPACK_RUNTIME_IMAGE = "ghcr.io/railwayapp/railpack-runtime-base:latest"
)
type BuildPlan struct {
BaseImage string `json:"baseImage,omitempty"`
Steps []Step `json:"steps,omitempty"`
Start Start `json:"start,omitempty"`
Caches map[string]*Cache `json:"caches,omitempty"`
Secrets []string `json:"secrets,omitempty"`
Steps []Step `json:"steps,omitempty"`
Caches map[string]*Cache `json:"caches,omitempty"`
Secrets []string `json:"secrets,omitempty"`
Deploy Deploy `json:"deploy,omitempty"`
}
type Start struct {
// The image to use for the container runtime
BaseImage string `json:"baseImage,omitempty"`
type Deploy struct {
// The inputs for the deploy step
Inputs []Input `json:"inputs,omitempty"`
// The command to run in the container
Command string `json:"cmd,omitempty"`
StartCmd string `json:"startCommand,omitempty"`
// Outputs to be copied from the container to the host
Outputs []string `json:"outputs,omitempty"`
// $PATHs to be prefixed to the container's base $PATH
Paths []string `json:"paths,omitempty"`
// Environment variables to set in the container. These are not available at build time.
// The variables available to this step. The key is the name of the variable that is referenced in a variable command
Variables map[string]string `json:"variables,omitempty"`
// The paths to prepend to the $PATH environment variable
Paths []string `json:"paths,omitempty"`
}
func NewBuildPlan() *BuildPlan {
return &BuildPlan{
Steps: []Step{},
Start: Start{},
Deploy: Deploy{},
Caches: make(map[string]*Cache),
Secrets: []string{},
}
+59 -30
View File
@@ -6,35 +6,68 @@ import (
"github.com/invopop/jsonschema"
)
type Input struct {
Image string `json:"image,omitempty"`
Step string `json:"step,omitempty"`
Local bool `json:"local,omitempty"`
Include []string `json:"include,omitempty"`
Exclude []string `json:"exclude,omitempty"`
}
type InputOptions struct {
Include []string
Exclude []string
}
type Step struct {
// The name of the step
Name string `json:"name,omitempty" jsonschema:"description=The name of the step"`
// The steps that this step depends on. The step will only run after all the steps in DependsOn have run
DependsOn []string `json:"dependsOn,omitempty" jsonschema:"description=The steps that this step depends on. The step will only run after all the steps in DependsOn have run"`
// The commands to run in this step
Commands []Command `json:"commands,omitempty" jsonschema:"description=The commands to run in this step"`
// The secrets that this step uses
Secrets []string `json:"secrets" jsonschema:"description=The secrets that this step uses"`
// Paths that this step outputs. Only these paths will be available to the next step
Outputs []string `json:"outputs,omitempty" jsonschema:"description=Paths that this step outputs. Only these paths will be available to the next step"`
// The assets available to this step. The key is the name of the asset that is referenced in a file command
Assets map[string]string `json:"assets,omitempty" jsonschema:"description=The assets available to this step. The key is the name of the asset that is referenced in a file command"`
// The variables available to this step. The key is the name of the variable that is referenced in a variable command
Name string `json:"name,omitempty" jsonschema:"description=The name of the step"`
Inputs []Input `json:"inputs,omitempty" jsonschema:"description=The inputs for this step"`
Commands []Command `json:"commands,omitempty" jsonschema:"description=The commands to run in this step"`
Secrets []string `json:"secrets,omitempty" jsonschema:"description=The secrets that this step uses"`
Assets map[string]string `json:"assets,omitempty" jsonschema:"description=The assets available to this step. The key is the name of the asset that is referenced in a file command"`
Variables map[string]string `json:"variables,omitempty" jsonschema:"description=The variables available to this step. The key is the name of the variable that is referenced in a variable command"`
Caches []string `json:"caches,omitempty" jsonschema:"description=The caches available to all commands in this step. Each cache must refer to a cache at the top level of the plan"`
}
// The caches available to all commands in this step. Each cache must refer to a cache at the top level of the plan
Caches []string `json:"caches,omitempty" jsonschema:"description=The caches available to all commands in this step. Each cache must refer to a cache at the top level of the plan"`
func NewStepInput(stepName string, options ...InputOptions) Input {
input := Input{
Step: stepName,
}
// The base image that will be used for this step
// If empty (default), the base image will be the one from the previous step
// Only set this if you don't want to reuse any part of the file system from the previous step
StartingImage string `json:"startingImage,omitempty" jsonschema:"description=The base image that will be used for this step. If empty (default), the base image will be the one from the previous step. Only set this if you don't want to reuse any part of the file system from the previous step"`
if len(options) > 0 {
input.Include = options[0].Include
input.Exclude = options[0].Exclude
}
return input
}
func NewImageInput(image string, options ...InputOptions) Input {
input := Input{
Image: image,
}
if len(options) > 0 {
input.Include = options[0].Include
input.Exclude = options[0].Exclude
}
return input
}
func RuntimeImageInput() Input {
return NewImageInput("ghcr.io/railwayapp/railpack-runtime-base:latest")
}
func NewLocalInput(path string) Input {
return Input{
Local: true,
Include: []string{path},
}
}
func (i *Input) String() string {
bytes, _ := json.Marshal(i)
return string(bytes)
}
func NewStep(name string) *Step {
@@ -46,10 +79,6 @@ func NewStep(name string) *Step {
}
}
func (s *Step) DependOn(name string) {
s.DependsOn = append(s.DependsOn, name)
}
func (s *Step) AddCommands(commands []Command) {
if s.Commands == nil {
s.Commands = []Command{}
@@ -89,7 +118,7 @@ func (Step) JSONSchemaExtend(schema *jsonschema.Schema) {
var required []string
for _, prop := range schema.Required {
if prop != "name" {
required = append(required, "name")
required = append(required, prop)
}
}
schema.Required = required
+3 -3
View File
@@ -144,10 +144,10 @@ func FormatBuildResult(br *BuildResult, options ...PrintOptions) string {
}
}
if br.Plan.Start.Command != "" {
output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Start"))
if br.Plan.Deploy.StartCmd != "" {
output.WriteString(sectionHeaderStyle.MarginTop(1).Render("Deploy"))
output.WriteString("\n")
output.WriteString(fmt.Sprintf("%s %s", commandPrefixStyle.Render("$"), commandStyle.Render(br.Plan.Start.Command)))
output.WriteString(fmt.Sprintf("%s %s", commandPrefixStyle.Render("$"), commandStyle.Render(br.Plan.Deploy.StartCmd)))
}
if opts.Metadata && br.Metadata != nil && len(br.Metadata) > 0 {
+60 -62
View File
@@ -13,7 +13,7 @@ const (
DEFAULT_GO_VERSION = "1.23"
GO_BUILD_CACHE_KEY = "go-build"
GO_BINARY_NAME = "out"
START_IMAGE = "alpine:latest"
GO_PATH = "/go"
)
type GoProvider struct{}
@@ -31,35 +31,37 @@ func (p *GoProvider) Initialize(ctx *generate.GenerateContext) error {
}
func (p *GoProvider) Plan(ctx *generate.GenerateContext) error {
packages, err := p.Packages(ctx)
if err != nil {
return err
}
builder := p.GetBuilder(ctx)
p.InstallGoPackages(ctx, builder)
install, err := p.Install(ctx, packages)
if err != nil {
return err
}
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepInput(builder.Name()))
p.InstallGoDeps(ctx, install)
build, err := p.Build(ctx, packages, install)
if err != nil {
return err
}
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepInput(install.Name()))
p.Build(ctx, build)
if build != nil {
ctx.Start.Command = fmt.Sprintf("./%s", GO_BINARY_NAME)
ctx.Deploy.StartCmd = fmt.Sprintf("./%s", GO_BINARY_NAME)
if !p.hasCGOEnabled(ctx) {
ctx.Start.AddOutputs([]string{GO_BINARY_NAME})
if p.hasCGOEnabled(ctx) {
runtimeAptStep := ctx.NewAptStepBuilder("runtime")
runtimeAptStep.AddInput(ctx.DefaultRuntimeInputWithPackages([]string{"libc6", "tzdata"}))
ctx.Start.BaseImage = START_IMAGE
if startImage, _ := ctx.Env.GetConfigVariable("START_IMAGE"); startImage != "" {
ctx.Start.BaseImage = startImage
}
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(runtimeAptStep.Name()),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{"."},
}),
plan.NewLocalInput("."),
}
if p.isGin(ctx) {
ctx.Start.AddEnvVars(map[string]string{"GIN_MODE": "release"})
} else {
ctx.Deploy.Inputs = []plan.Input{
ctx.DefaultRuntimeInputWithPackages([]string{"tzdata"}),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{"."},
}),
plan.NewLocalInput("."),
}
}
@@ -68,52 +70,51 @@ func (p *GoProvider) Plan(ctx *generate.GenerateContext) error {
return nil
}
func (p *GoProvider) Build(ctx *generate.GenerateContext, packages *generate.MiseStepBuilder, install *generate.CommandStepBuilder) (*generate.CommandStepBuilder, error) {
func (p *GoProvider) Build(ctx *generate.GenerateContext, build *generate.CommandStepBuilder) {
var buildCmd string
flags := "-w -s"
baseBuildCmd := fmt.Sprintf("go build -ldflags=\"%s\" -o %s", flags, GO_BINARY_NAME)
if binName, _ := ctx.Env.GetConfigVariable("GO_BIN"); binName != "" {
// If there is a RAILPACK_GO_BIN variable, use that
buildCmd = fmt.Sprintf("go build -o %s ./cmd/%s", GO_BINARY_NAME, binName)
buildCmd = fmt.Sprintf("%s ./cmd/%s", baseBuildCmd, binName)
} else if p.isGoMod(ctx) && p.hasRootGoFiles(ctx) {
buildCmd = fmt.Sprintf("go build -o %s", GO_BINARY_NAME)
buildCmd = baseBuildCmd
} else if dirs, err := ctx.App.FindDirectories("cmd/*"); err == nil && len(dirs) > 0 {
// Try to find a command in the cmd directory
cmdName := filepath.Base(dirs[0])
buildCmd = fmt.Sprintf("go build -o %s ./cmd/%s", GO_BINARY_NAME, cmdName)
buildCmd = fmt.Sprintf("%s ./cmd/%s", baseBuildCmd, cmdName)
} else if p.isGoMod(ctx) {
buildCmd = fmt.Sprintf("go build -o %s", GO_BINARY_NAME)
buildCmd = baseBuildCmd
} else if ctx.App.HasMatch("main.go") {
buildCmd = fmt.Sprintf("go build -o %s main.go", GO_BINARY_NAME)
buildCmd = fmt.Sprintf("%s main.go", baseBuildCmd)
}
if buildCmd == "" {
return nil, nil
return
}
build := ctx.NewCommandStep("build")
build.AddCache(p.goBuildCache(ctx))
build.AddCommands([]plan.Command{
plan.NewCopyCommand("."),
plan.NewExecCommand(buildCmd),
})
if packages != nil {
build.DependsOn = append(build.DependsOn, packages.DisplayName)
}
if install != nil {
build.DependsOn = append(build.DependsOn, install.DisplayName)
}
return build, nil
}
func (p *GoProvider) Install(ctx *generate.GenerateContext, packages *generate.MiseStepBuilder) (*generate.CommandStepBuilder, error) {
func (p *GoProvider) InstallGoDeps(ctx *generate.GenerateContext, install *generate.CommandStepBuilder) {
install.AddEnvVars(map[string]string{
"GOPATH": GO_PATH,
"GOBIN": fmt.Sprintf("%s/bin", GO_PATH),
})
install.AddCommands([]plan.Command{
plan.NewPathCommand(fmt.Sprintf("%s/bin", GO_PATH)),
})
if !p.isGoMod(ctx) {
return nil, nil
return
}
install := ctx.NewCommandStep("install")
install.AddCache(p.goBuildCache(ctx))
install.AddCommand(plan.NewCopyCommand("go.mod"))
@@ -123,24 +124,13 @@ func (p *GoProvider) Install(ctx *generate.GenerateContext, packages *generate.M
install.AddCommand(plan.NewExecCommand("go mod download"))
// If CGO is enabled, we need to install the gcc packages
if p.hasCGOEnabled(ctx) {
aptStep := ctx.NewAptStepBuilder("cgo")
aptStep.Packages = []string{"gcc", "g++", "libc6-dev"}
install.DependsOn = append(install.DependsOn, aptStep.DisplayName)
} else {
if !p.hasCGOEnabled(ctx) {
install.AddEnvVars(map[string]string{"CGO_ENABLED": "0"})
}
install.DependsOn = []string{packages.DisplayName}
return install, nil
}
func (p *GoProvider) Packages(ctx *generate.GenerateContext) (*generate.MiseStepBuilder, error) {
packages := ctx.GetMiseStepBuilder()
goPkg := packages.Default("go", DEFAULT_GO_VERSION)
func (p *GoProvider) InstallGoPackages(ctx *generate.GenerateContext, miseStep *generate.MiseStepBuilder) {
goPkg := miseStep.Default("go", DEFAULT_GO_VERSION)
if goModContents, err := ctx.App.ReadFile("go.mod"); err == nil {
// Split content into lines and look for "go X.XX" line
@@ -149,7 +139,7 @@ func (p *GoProvider) Packages(ctx *generate.GenerateContext) (*generate.MiseStep
if strings.HasPrefix(strings.TrimSpace(line), "go ") {
// Extract version number
if goVersion := strings.TrimSpace(strings.TrimPrefix(line, "go")); goVersion != "" {
packages.Version(goPkg, goVersion, "go.mod")
miseStep.Version(goPkg, goVersion, "go.mod")
break
}
}
@@ -157,10 +147,18 @@ func (p *GoProvider) Packages(ctx *generate.GenerateContext) (*generate.MiseStep
}
if envVersion, varName := ctx.Env.GetConfigVariable("GO_VERSION"); envVersion != "" {
packages.Version(goPkg, envVersion, varName)
miseStep.Version(goPkg, envVersion, varName)
}
}
func (p *GoProvider) GetBuilder(ctx *generate.GenerateContext) *generate.MiseStepBuilder {
miseStep := ctx.GetMiseStepBuilder()
if p.hasCGOEnabled(ctx) {
miseStep.SupportingAptPackages = append(miseStep.SupportingAptPackages, "gcc", "g++", "libc6-dev")
}
return packages, nil
return miseStep
}
func (p *GoProvider) addMetadata(ctx *generate.GenerateContext) {
+91 -107
View File
@@ -2,6 +2,7 @@ package node
import (
"fmt"
"maps"
"path"
"strings"
@@ -18,8 +19,9 @@ const (
)
type NodeProvider struct {
packageJson *PackageJson
workspace *Workspace
packageJson *PackageJson
packageManager PackageManager
workspace *Workspace
}
func (p *NodeProvider) Name() string {
@@ -33,6 +35,8 @@ func (p *NodeProvider) Initialize(ctx *generate.GenerateContext) error {
}
p.packageJson = packageJson
p.packageManager = p.getPackageManager(ctx.App)
workspace, err := NewWorkspace(ctx.App)
if err != nil {
return err
@@ -51,56 +55,70 @@ func (p *NodeProvider) Plan(ctx *generate.GenerateContext) error {
return fmt.Errorf("package.json not loaded, did you call Initialize?")
}
packages, err := p.Packages(ctx, p.packageJson)
if err != nil {
return err
}
miseStep := ctx.GetMiseStepBuilder()
p.InstallMisePackages(ctx, miseStep)
install, err := p.Install(ctx, packages, p.packageJson)
if err != nil {
return err
}
// Install
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepInput(miseStep.Name()))
p.InstallNodeDeps(ctx, install)
if _, err := p.Build(ctx, install, p.packageJson); err != nil {
return err
}
// Prune
prune := ctx.NewCommandStep("prune")
prune.AddInput(plan.NewStepInput(install.Name()))
p.PruneNodeDeps(ctx, prune)
if err := p.start(ctx, p.packageJson); err != nil {
return err
}
return nil
}
func (p *NodeProvider) start(ctx *generate.GenerateContext, packageJson *PackageJson) error {
packageManager := p.getPackageManager(ctx.App)
if start := p.getScripts(packageJson, "start"); start != "" {
ctx.Start.Command = packageManager.RunCmd("start")
} else if main := packageJson.Main; main != "" {
ctx.Start.Command = packageManager.RunScriptCommand(main)
} else if files, err := ctx.App.FindFiles("{index.js,index.ts}"); err == nil && len(files) > 0 {
ctx.Start.Command = packageManager.RunScriptCommand(files[0])
}
ctx.Start.AddOutputs([]string{"."})
ctx.Start.AddEnvVars(p.GetNodeEnvVars(ctx))
return nil
}
func (p *NodeProvider) Build(ctx *generate.GenerateContext, install *generate.CommandStepBuilder, packageJson *PackageJson) (*generate.CommandStepBuilder, error) {
packageManager := p.getPackageManager(ctx.App)
// Build
build := ctx.NewCommandStep("build")
build.DependsOn = []string{install.DisplayName}
build.AddInput(plan.NewStepInput(install.Name()))
p.Build(ctx, build)
_, ok := packageJson.Scripts["build"]
// Deploy
ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
maps.Copy(ctx.Deploy.Variables, p.GetNodeEnvVars(ctx))
buildIncludeDirs := []string{"."}
if p.usesCorepack() {
buildIncludeDirs = append(buildIncludeDirs, "/root/.cache")
}
ctx.Deploy.Inputs = append(ctx.Deploy.Inputs, []plan.Input{
ctx.DefaultRuntimeInput(),
plan.NewStepInput(miseStep.Name(), plan.InputOptions{
Include: miseStep.GetOutputPaths(),
}),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: buildIncludeDirs,
Exclude: []string{"node_modules"},
}),
plan.NewStepInput(prune.Name(), plan.InputOptions{
Include: []string{"/app/node_modules"}, // we only wanted the pruned node_modules
}),
plan.NewLocalInput("."),
}...)
return nil
}
func (p *NodeProvider) GetStartCommand(ctx *generate.GenerateContext) string {
if start := p.getScripts(p.packageJson, "start"); start != "" {
return p.packageManager.RunCmd("start")
} else if main := p.packageJson.Main; main != "" {
return p.packageManager.RunScriptCommand(main)
} else if files, err := ctx.App.FindFiles("{index.js,index.ts}"); err == nil && len(files) > 0 {
return p.packageManager.RunScriptCommand(files[0])
}
return ""
}
func (p *NodeProvider) Build(ctx *generate.GenerateContext, build *generate.CommandStepBuilder) {
_, ok := p.packageJson.Scripts["build"]
if ok {
build.AddCommands([]plan.Command{
plan.NewCopyCommand("."),
plan.NewExecCommand(packageManager.RunCmd("build")),
plan.NewExecCommand(p.packageManager.RunCmd("build")),
})
}
// Generic node_modules cache
@@ -115,97 +133,63 @@ func (p *NodeProvider) Build(ctx *generate.GenerateContext, install *generate.Co
build.AddCache(ctx.Caches.AddCache(fmt.Sprintf("next-%s", nextApp), nextCacheDir))
}
}
return build, nil
}
func (p *NodeProvider) Install(ctx *generate.GenerateContext, packages *generate.MiseStepBuilder, packageJson *PackageJson) (*generate.CommandStepBuilder, error) {
lenDeps := len(packageJson.Dependencies) + len(packageJson.DevDependencies)
setup, err := p.Setup(ctx)
if err != nil {
return nil, err
func (p *NodeProvider) PruneNodeDeps(ctx *generate.GenerateContext, prune *generate.CommandStepBuilder) {
if ctx.Env.IsConfigVariableTruthy("NO_PRUNE") {
return
}
if lenDeps == 0 {
return setup, nil
}
prune.Variables["NPM_CONFIG_PRODUCTION"] = "true"
prune.Secrets = []string{}
p.packageManager.PruneDeps(ctx, prune)
}
var corepackStepName string
if p.usesCorepack(packageJson) {
corepackStep := ctx.NewCommandStep("corepack")
corepackStep.AddCommands([]plan.Command{
func (p *NodeProvider) InstallNodeDeps(ctx *generate.GenerateContext, install *generate.CommandStepBuilder) {
maps.Copy(install.Variables, p.GetNodeEnvVars(ctx))
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"NODE", "NPM", "BUN", "PNPM", "YARN", "CI"})
install.AddPaths([]string{"/app/node_modules/.bin"})
if p.usesCorepack() {
install.AddCommands([]plan.Command{
plan.NewCopyCommand("package.json"),
plan.NewExecCommand("corepack enable"),
plan.NewExecCommand("corepack prepare --activate"),
})
corepackStepName = corepackStep.DisplayName
corepackStep.DependsOn = append(corepackStep.DependsOn, setup.DisplayName)
corepackStep.Secrets = []string{} // Don't include any secrets in this step
}
pkgManager := p.getPackageManager(ctx.App)
install := ctx.NewCommandStep("install")
install.DependsOn = append(install.DependsOn, []string{packages.DisplayName, setup.DisplayName}...)
// We only want to invalidate the install step when these secrets change, not all of them
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"NODE", "NPM", "BUN", "PNPM", "YARN", "CI"})
pkgManager.installDependencies(ctx, packageJson, install)
if corepackStepName != "" {
install.DependsOn = append(install.DependsOn, corepackStepName)
}
return install, nil
p.packageManager.installDependencies(ctx, p.packageJson, install)
}
func (p *NodeProvider) Setup(ctx *generate.GenerateContext) (*generate.CommandStepBuilder, error) {
setup := ctx.NewCommandStep("setup")
setup.AddEnvVars(p.GetNodeEnvVars(ctx))
setup.AddPaths([]string{"/app/node_modules/.bin"})
setup.Secrets = []string{}
return setup, nil
}
func (p *NodeProvider) Packages(ctx *generate.GenerateContext, packageJson *PackageJson) (*generate.MiseStepBuilder, error) {
packageManager := p.getPackageManager(ctx.App)
ctx.Metadata.Set("packageManager", string(packageManager))
packages := ctx.GetMiseStepBuilder()
func (p *NodeProvider) InstallMisePackages(ctx *generate.GenerateContext, miseStep *generate.MiseStepBuilder) {
// Node
if packageManager.requiresNode(packageJson) {
node := packages.Default("node", DEFAULT_NODE_VERSION)
if p.packageManager.requiresNode(p.packageJson) {
node := miseStep.Default("node", DEFAULT_NODE_VERSION)
if envVersion, varName := ctx.Env.GetConfigVariable("NODE_VERSION"); envVersion != "" {
packages.Version(node, envVersion, varName)
miseStep.Version(node, envVersion, varName)
}
if packageJson.Engines != nil && packageJson.Engines["node"] != "" {
packages.Version(node, packageJson.Engines["node"], "package.json > engines > node")
if p.packageJson.Engines != nil && p.packageJson.Engines["node"] != "" {
miseStep.Version(node, p.packageJson.Engines["node"], "package.json > engines > node")
}
}
// Bun
if packageManager == PackageManagerBun {
bun := packages.Default("bun", DEFAULT_BUN_VERSION)
if p.packageManager == PackageManagerBun {
bun := miseStep.Default("bun", DEFAULT_BUN_VERSION)
if envVersion, varName := ctx.Env.GetConfigVariable("BUN_VERSION"); envVersion != "" {
packages.Version(bun, envVersion, varName)
miseStep.Version(bun, envVersion, varName)
}
}
packageManager.GetPackageManagerPackages(ctx, packages)
p.packageManager.GetPackageManagerPackages(ctx, miseStep)
if p.usesCorepack(packageJson) {
packages.Variables["MISE_NODE_COREPACK"] = "true"
if p.usesCorepack() {
miseStep.Variables["MISE_NODE_COREPACK"] = "true"
}
return packages, nil
}
func (p *NodeProvider) GetNodeEnvVars(ctx *generate.GenerateContext) map[string]string {
@@ -221,8 +205,8 @@ func (p *NodeProvider) GetNodeEnvVars(ctx *generate.GenerateContext) map[string]
return envVars
}
func (p *NodeProvider) usesCorepack(packageJson *PackageJson) bool {
return packageJson.PackageManager != nil
func (p *NodeProvider) usesCorepack() bool {
return p.packageJson.PackageManager != nil
}
func (p *NodeProvider) getPackageManager(app *app.App) PackageManager {
+4 -3
View File
@@ -102,11 +102,10 @@ func TestNodeCorepack(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := testingUtils.CreateGenerateContext(t, tt.path)
provider := NodeProvider{}
packageJson, err := provider.GetPackageJson(ctx.App)
err := provider.Initialize(ctx)
require.NoError(t, err)
usesCorepack := provider.usesCorepack(packageJson)
usesCorepack := provider.usesCorepack()
require.Equal(t, tt.wantCorepack, usesCorepack)
})
}
@@ -139,6 +138,8 @@ func TestGetNextApps(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := testingUtils.CreateGenerateContext(t, tt.path)
provider := NodeProvider{}
err := provider.Initialize(ctx)
require.NoError(t, err)
nextApps, err := provider.getNextApps(ctx)
require.NoError(t, err)
+41 -9
View File
@@ -64,12 +64,30 @@ func (p PackageManager) installDependencies(ctx *generate.GenerateContext, packa
p.InstallDeps(ctx, install)
}
// GetCache returns the cache for the package manager
func (p PackageManager) GetInstallCache(ctx *generate.GenerateContext) string {
switch p {
case PackageManagerNpm:
return ctx.Caches.AddCache("npm-install", "/root/.npm")
case PackageManagerPnpm:
return ctx.Caches.AddCache("pnpm-install", "/root/.local/share/pnpm/store/v3")
case PackageManagerBun:
return ctx.Caches.AddCache("bun-install", "/root/.bun/install/cache")
case PackageManagerYarn1:
return ctx.Caches.AddCacheWithType("yarn-install", "/usr/local/share/.cache/yarn", plan.CacheTypeLocked)
case PackageManagerYarn2:
return ctx.Caches.AddCache("yarn-install", "/usr/local/share/.cache/yarn")
default:
return ""
}
}
func (p PackageManager) InstallDeps(ctx *generate.GenerateContext, install *generate.CommandStepBuilder) {
install.AddCache(p.GetInstallCache(ctx))
switch p {
case PackageManagerNpm:
hasLockfile := ctx.App.HasMatch("package-lock.json")
install.AddCache(ctx.Caches.AddCache("npm-install", "/root/.npm"))
if hasLockfile {
install.AddCommand(plan.NewExecCommand("npm ci"))
} else {
@@ -77,17 +95,30 @@ func (p PackageManager) InstallDeps(ctx *generate.GenerateContext, install *gene
}
case PackageManagerPnpm:
install.AddCommand(plan.NewExecCommand("pnpm install --frozen-lockfile --prod=false"))
install.AddCache(ctx.Caches.AddCache("pnpm-install", "/root/.local/share/pnpm/store/v3"))
case PackageManagerBun:
install.AddCommand(plan.NewExecCommand("bun install --frozen-lockfile"))
// TODO: We should also cache this for the build step
install.AddCache(ctx.Caches.AddCache("bun-install", "/root/.bun/install/cache"))
case PackageManagerYarn1:
install.AddCommand(plan.NewExecCommand("yarn install --frozen-lockfile"))
install.AddCache(ctx.Caches.AddCacheWithType("yarn-install", "/usr/local/share/.cache/yarn", plan.CacheTypeLocked))
case PackageManagerYarn2:
install.AddCommand(plan.NewExecCommand("yarn install --check-cache"))
install.AddCache(ctx.Caches.AddCache("yarn-install", "/usr/local/share/.cache/yarn"))
}
}
func (p PackageManager) PruneDeps(ctx *generate.GenerateContext, prune *generate.CommandStepBuilder) {
prune.AddCache(p.GetInstallCache(ctx))
switch p {
case PackageManagerNpm:
prune.AddCommand(plan.NewExecCommand("npm prune --omit=dev"))
case PackageManagerPnpm:
prune.AddCommand(plan.NewExecCommand("pnpm prune --prod"))
case PackageManagerBun:
// Prune is not supported in Bun. https://github.com/oven-sh/bun/issues/3605
prune.AddCommand(plan.NewExecShellCommand("rm -rf node_modules && bun install --production"))
case PackageManagerYarn1:
prune.AddCommand(plan.NewExecCommand("yarn install --production=true"))
case PackageManagerYarn2:
prune.AddCommand(plan.NewExecCommand("yarn install --production=true"))
}
}
@@ -102,7 +133,6 @@ func (p PackageManager) SupportingInstallFiles(app *a.App) []string {
"**/bun.lockb",
"**/bun.lock",
"**/yarn.lock",
"**/node_modules",
"**/.pnp.*", // Yarn Plug'n'Play files
"**/.yarnrc.yml", // Yarn 2+ config
"**/.npmrc", // NPM config
@@ -158,8 +188,10 @@ func (p PackageManager) GetPackageManagerPackages(ctx *generate.GenerateContext,
}
}
// Check is Node is required for the package manager
// If the package manager is Bun, we avoid using Node unless it's specified in the package.json
func (p PackageManager) requiresNode(packageJson *PackageJson) bool {
if p != PackageManagerBun || packageJson == nil {
if p != PackageManagerBun || packageJson == nil || packageJson.PackageManager != nil {
return true
}
+92 -65
View File
@@ -32,65 +32,15 @@ func (p *PhpProvider) Initialize(ctx *generate.GenerateContext) error {
}
func (p *PhpProvider) Plan(ctx *generate.GenerateContext) error {
imageStep, err := p.phpImagePackage(ctx)
phpImageStep, err := p.phpImagePackage(ctx)
if err != nil {
return err
}
// Install nginx
nginxPackages := ctx.NewAptStepBuilder("nginx")
nginxPackages.Packages = []string{"nginx", "git", "zip", "unzip"}
nginxPackages.DependsOn = []string{imageStep.DisplayName}
// Install composer
if _, err := p.readComposerJson(ctx); err == nil {
install := ctx.NewCommandStep("install")
install.AddCache(ctx.Caches.AddCache("composer", "/opt/cache/composer"))
install.AddEnvVars(map[string]string{"COMPOSER_CACHE_DIR": "/opt/cache/composer"})
install.AddCommands([]plan.Command{
// Copy composer from the composer image
plan.CopyCommand{Image: "composer:latest", Src: "/usr/bin/composer", Dest: "/usr/bin/composer"},
plan.NewCopyCommand("."),
plan.NewExecCommand("composer install --ignore-platform-reqs"),
})
install.DependsOn = []string{nginxPackages.DisplayName}
}
// Install node
nodeProvider := node.NodeProvider{}
isNode := ctx.App.HasMatch("package.json")
if packageJson, err := nodeProvider.GetPackageJson(ctx.App); isNode && err == nil && packageJson != nil {
ctx.EnterSubContext("node")
nodePackages, err := nodeProvider.Packages(ctx, packageJson)
if err != nil {
return err
}
// nodePackages.DependsOn = []string{imageStep.DisplayName}
nodeInstall, err := nodeProvider.Install(ctx, nodePackages, packageJson)
if err != nil {
return err
}
nodeBuild, err := nodeProvider.Build(ctx, nodeInstall, packageJson)
if err != nil {
return err
}
// Only copy the changes to the app, not the entire rest of the file system
nodeBuild.Outputs = []string{"/app"}
ctx.ExitSubContext()
}
// Setup nginx
nginxSetup := ctx.NewCommandStep("nginx:setup")
nginxSetup.DependsOn = []string{nginxPackages.DisplayName}
nginxSetup.AddCommands([]plan.Command{
// Nginx
nginx := ctx.NewCommandStep("nginx")
nginx.AddInput(plan.NewStepInput(phpImageStep.Name()))
nginx.AddCommands([]plan.Command{
plan.NewFileCommand("/etc/nginx/railpack.conf", "nginx.conf", plan.FileOptions{CustomName: "create nginx config"}),
plan.NewExecCommand("nginx -t -c /etc/nginx/railpack.conf"),
plan.NewFileCommand("/etc/php-fpm.conf", "php-fpm.conf", plan.FileOptions{CustomName: "create php-fpm config"}),
@@ -100,22 +50,93 @@ func (p *PhpProvider) Plan(ctx *generate.GenerateContext) error {
}),
})
if p.usesLaravel(ctx) {
nginxSetup.AddEnvVars(map[string]string{"IS_LARAVEL": "true"})
}
nginxSetup.Assets["start-nginx.sh"] = startNginxScriptAsset
nginx.Assets["start-nginx.sh"] = startNginxScriptAsset
configFiles, err := p.getConfigFiles(ctx)
if err != nil {
return fmt.Errorf("failed to get config files: %w", err)
}
nginxSetup.Assets["nginx.conf"] = configFiles.NginxConf
nginxSetup.Assets["nginx.conf"] = configFiles.NginxConf
nginxSetup.Assets["php-fpm.conf"] = configFiles.PhpFpmConf
nginx.Assets["nginx.conf"] = configFiles.NginxConf
nginx.Assets["nginx.conf"] = configFiles.NginxConf
nginx.Assets["php-fpm.conf"] = configFiles.PhpFpmConf
ctx.Start.Command = "bash /start-nginx.sh"
ctx.Start.AddOutputs([]string{"."})
// Composer
composer := ctx.NewCommandStep("install:composer")
composer.AddInput(plan.NewStepInput(nginx.Name()))
if _, err := p.readComposerJson(ctx); err == nil {
composer.AddCache(ctx.Caches.AddCache("composer", "/opt/cache/composer"))
composer.AddEnvVars(map[string]string{"COMPOSER_CACHE_DIR": "/opt/cache/composer"})
composer.AddCommands([]plan.Command{
// Copy composer from the composer image
plan.CopyCommand{Image: "composer:latest", Src: "/usr/bin/composer", Dest: "/usr/bin/composer"},
plan.NewCopyCommand("."),
plan.NewExecCommand("composer install --ignore-platform-reqs"),
})
}
// Node (if necessary)
nodeProvider := node.NodeProvider{}
isNode, err := nodeProvider.Detect(ctx)
if err != nil {
return err
}
if isNode {
err = nodeProvider.Initialize(ctx)
if err != nil {
return err
}
miseStep := ctx.GetMiseStepBuilder()
nodeProvider.InstallMisePackages(ctx, miseStep)
install := ctx.NewCommandStep("install:node")
install.AddInput(plan.NewStepInput(miseStep.Name()))
nodeProvider.InstallNodeDeps(ctx, install)
prune := ctx.NewCommandStep("prune:node")
prune.AddInput(plan.NewStepInput(install.Name()))
nodeProvider.PruneNodeDeps(ctx, prune)
build := ctx.NewCommandStep("build")
build.Inputs = []plan.Input{
plan.NewStepInput(composer.Name()),
// Include the app and mise packages (node, pnpm, etc.)
plan.NewStepInput(install.Name(), plan.InputOptions{
Include: append([]string{"."}, miseStep.GetOutputPaths()...),
}),
}
nodeProvider.Build(ctx, build)
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(composer.Name()),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{"."},
Exclude: []string{"node_modules"},
}),
plan.NewStepInput(prune.Name(), plan.InputOptions{
Include: []string{"/app/node_modules"},
}),
plan.NewLocalInput("."),
}
} else {
// A manual build command will go here
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepInput(composer.Name()))
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(build.Name()),
plan.NewLocalInput("."),
}
}
ctx.Deploy.StartCmd = "bash /start-nginx.sh"
if p.usesLaravel(ctx) {
ctx.Deploy.Variables["IS_LARAVEL"] = "true"
}
return nil
}
@@ -168,6 +189,12 @@ func (p *PhpProvider) phpImagePackage(ctx *generate.GenerateContext) (*generate.
return getPhpImage(DEFAULT_PHP_VERSION)
})
imageStep.AptPackages = append(imageStep.AptPackages, "nginx", "git", "zip", "unzip")
// Include both build and runtime apt packages since we don't have a separate runtime image
imageStep.AptPackages = append(imageStep.AptPackages, ctx.Config.BuildAptPackages...)
imageStep.AptPackages = append(imageStep.AptPackages, ctx.Config.Deploy.AptPackages...)
php := imageStep.Default("php", DEFAULT_PHP_VERSION)
// Read composer.json to get the PHP version
+2 -2
View File
@@ -22,9 +22,9 @@ func (p *ProcfileProvider) Plan(ctx *generate.GenerateContext) (bool, error) {
workerCommand := parsedProcfile["worker"]
if webCommand != "" {
ctx.Start.Command = webCommand
ctx.Deploy.StartCmd = webCommand
} else if workerCommand != "" {
ctx.Start.Command = workerCommand
ctx.Deploy.StartCmd = workerCommand
}
return false, nil
+1 -1
View File
@@ -14,5 +14,5 @@ func TestProcfile(t *testing.T) {
_, err := provider.Plan(ctx)
require.NoError(t, err)
require.Equal(t, "gunicorn --bind 0.0.0.0:3333 main:app", ctx.Start.Command)
require.Equal(t, "gunicorn --bind 0.0.0.0:3333 main:app", ctx.Deploy.StartCmd)
}
+299 -58
View File
@@ -1,6 +1,8 @@
package python
import (
"fmt"
"maps"
"regexp"
"strings"
@@ -12,6 +14,9 @@ const (
DEFAULT_PYTHON_VERSION = "3.13.2"
UV_CACHE_DIR = "/opt/uv-cache"
PIP_CACHE_DIR = "/opt/pip-cache"
PACKAGES_DIR = "/opt/python-packages"
VENV_PATH = "/app/.venv"
LOCAL_BIN_PATH = "/root/.local/bin"
)
type PythonProvider struct{}
@@ -35,40 +40,294 @@ func (p *PythonProvider) Detect(ctx *generate.GenerateContext) (bool, error) {
}
func (p *PythonProvider) Plan(ctx *generate.GenerateContext) error {
if err := p.packages(ctx); err != nil {
return err
p.InstallMisePackages(ctx, ctx.GetMiseStepBuilder())
if p.hasRequirements(ctx) {
p.PlanPip(ctx)
} else if p.hasPyproject(ctx) && p.hasUv(ctx) {
p.PlanUv(ctx)
} else if p.hasPyproject(ctx) && p.hasPoetry(ctx) {
p.PlanPoetry(ctx)
} else if p.hasPyproject(ctx) && p.hasPdm(ctx) {
p.PlanPDM(ctx)
} else if p.hasPipfile(ctx) {
p.PlanPipenv(ctx)
}
if err := p.install(ctx); err != nil {
return err
}
// Install dependencies
// install := ctx.NewCommandStep("install")
// install.AddInput(plan.NewStepInput(miseStep.Name()))
// p.InstallPythonDeps(ctx, install)
if err := p.start(ctx); err != nil {
return err
}
// // Build step (if needed)
// build := ctx.NewCommandStep("build")
// build.AddInput(plan.NewStepInput(install.Name()))
// // Deploy configuration
// ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
// maps.Copy(ctx.Deploy.Variables, p.GetPythonEnvVars(ctx))
// ctx.Deploy.Inputs = append(ctx.Deploy.Inputs, []plan.Input{
// plan.NewImageInput(plan.RAILPACK_RUNTIME_IMAGE),
// plan.NewStepInput(miseStep.Name(), plan.InputOptions{
// Include: miseStep.GetOutputPaths(),
// }),
// plan.NewStepInput(build.Name(), plan.InputOptions{
// Include: []string{"."},
// }),
// plan.NewLocalInput("."),
// }...)
p.addMetadata(ctx)
return nil
}
func (p *PythonProvider) start(ctx *generate.GenerateContext) error {
ctx.Start.AddOutputs([]string{"."})
var startCommand string
func (p *PythonProvider) GetStartCommand(ctx *generate.GenerateContext) string {
if ctx.App.HasMatch("main.py") {
startCommand = "python main.py"
return "python main.py"
}
if startCommand != "" {
ctx.Start.Command = startCommand
}
return nil
return ""
}
func (p *PythonProvider) install(ctx *generate.GenerateContext) error {
func (p *PythonProvider) PlanUv(ctx *generate.GenerateContext) {
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepInput(p.GetBuilderDeps(ctx).Name()))
install.AddCache(ctx.Caches.AddCache("uv", UV_CACHE_DIR))
install.AddEnvVars(map[string]string{
"UV_COMPILE_BYTECODE": "1",
"UV_LINK_MODE": "copy",
"UV_CACHE_DIR": UV_CACHE_DIR,
"UV_PYTHON_DOWNLOADS": "never",
})
install.AddEnvVars(p.GetPythonEnvVars(ctx))
install.AddCommands([]plan.Command{
plan.NewExecCommand("pipx install uv"),
plan.NewPathCommand(LOCAL_BIN_PATH),
plan.NewCopyCommand("pyproject.toml"),
plan.NewCopyCommand("uv.lock"),
plan.NewExecCommand("uv sync --locked --no-dev --no-install-project"),
plan.NewCopyCommand("."),
plan.NewExecCommand("uv sync --locked --no-dev --no-editable"),
plan.NewPathCommand(VENV_PATH + "/bin"),
})
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepInput(install.Name()))
ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
maps.Copy(ctx.Deploy.Variables, p.GetPythonEnvVars(ctx))
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(p.GetImageWithRuntimeDeps(ctx).Name()),
plan.NewStepInput(ctx.GetMiseStepBuilder().Name(), plan.InputOptions{
Include: ctx.GetMiseStepBuilder().GetOutputPaths(),
}),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{"."},
}),
plan.NewLocalInput("."),
}
}
func (p *PythonProvider) PlanPipenv(ctx *generate.GenerateContext) {
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepInput(p.GetBuilderDeps(ctx).Name()))
install.AddEnvVars(p.GetPythonEnvVars(ctx))
install.AddEnvVars(map[string]string{
"PIPENV_CHECK_UPDATE": "false",
"PIPENV_VENV_IN_PROJECT": "1",
"PIPENV_IGNORE_VIRTUALENVS": "1",
})
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"PYTHON", "PIP"})
install.AddCommands([]plan.Command{
plan.NewExecCommand("pipx install pipenv"),
plan.NewPathCommand(LOCAL_BIN_PATH),
plan.NewCopyCommand("Pipfile"),
plan.NewCopyCommand("Pipfile.lock"),
plan.NewExecCommand("pipenv install --deploy --ignore-pipfile"),
plan.NewPathCommand(VENV_PATH + "/bin"),
})
if ctx.App.HasMatch("Pipfile.lock") {
install.AddCommands([]plan.Command{
plan.NewCopyCommand("Pipfile"),
plan.NewCopyCommand("Pipfile.lock"),
plan.NewExecCommand("pipenv install --deploy --ignore-pipfile"),
})
} else {
install.AddCommands([]plan.Command{
plan.NewCopyCommand("Pipfile"),
plan.NewExecCommand("pipenv install --skip-lock"),
})
}
install.AddCommands([]plan.Command{
plan.NewPathCommand(VENV_PATH + "/bin"),
})
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepInput(install.Name()))
ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
maps.Copy(ctx.Deploy.Variables, p.GetPythonEnvVars(ctx))
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(p.GetImageWithRuntimeDeps(ctx).Name()),
plan.NewStepInput(ctx.GetMiseStepBuilder().Name(), plan.InputOptions{
Include: ctx.GetMiseStepBuilder().GetOutputPaths(),
}),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{"."},
}),
plan.NewLocalInput("."),
}
}
func (p *PythonProvider) PlanPDM(ctx *generate.GenerateContext) {
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepInput(p.GetBuilderDeps(ctx).Name()))
install.AddEnvVars(p.GetPythonEnvVars(ctx))
install.AddEnvVars(map[string]string{
"PDM_CHECK_UPDATE": "false",
})
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"PYTHON", "PDM"})
install.AddCommands([]plan.Command{
plan.NewExecCommand("pipx install pdm"),
plan.NewPathCommand(LOCAL_BIN_PATH),
plan.NewCopyCommand("."),
plan.NewExecCommand("python --version"),
plan.NewExecCommand("pdm install --check --prod --no-editable"),
plan.NewPathCommand(VENV_PATH + "/bin"),
})
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepInput(install.Name()))
ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
maps.Copy(ctx.Deploy.Variables, p.GetPythonEnvVars(ctx))
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(p.GetImageWithRuntimeDeps(ctx).Name()),
plan.NewStepInput(ctx.GetMiseStepBuilder().Name(), plan.InputOptions{
Include: ctx.GetMiseStepBuilder().GetOutputPaths(),
}),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{"."},
}),
plan.NewLocalInput("."),
}
}
func (p *PythonProvider) PlanPoetry(ctx *generate.GenerateContext) {
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepInput(p.GetBuilderDeps(ctx).Name()))
install.AddEnvVars(p.GetPythonEnvVars(ctx))
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"PYTHON", "POETRY"})
install.AddCommands([]plan.Command{
plan.NewExecCommand("pipx install poetry"),
plan.NewPathCommand(LOCAL_BIN_PATH),
plan.NewExecCommand("poetry config virtualenvs.in-project true"),
plan.NewCopyCommand("pyproject.toml"),
plan.NewCopyCommand("poetry.lock"),
plan.NewExecCommand("poetry install --no-interaction --no-ansi --only main --no-root"),
plan.NewCopyCommand("."),
plan.NewPathCommand(VENV_PATH + "/bin"),
})
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepInput(install.Name()))
ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
maps.Copy(ctx.Deploy.Variables, p.GetPythonEnvVars(ctx))
maps.Copy(ctx.Deploy.Variables, map[string]string{
"VIRTUAL_ENV": VENV_PATH,
})
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(p.GetImageWithRuntimeDeps(ctx).Name()),
plan.NewStepInput(ctx.GetMiseStepBuilder().Name(), plan.InputOptions{
Include: ctx.GetMiseStepBuilder().GetOutputPaths(),
}),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{"."},
}),
plan.NewLocalInput("."),
}
}
func (p *PythonProvider) PlanPip(ctx *generate.GenerateContext) {
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepInput(p.GetBuilderDeps(ctx).Name()))
install.AddCache(ctx.Caches.AddCache("pip", PIP_CACHE_DIR))
install.AddCommands([]plan.Command{
plan.NewCopyCommand("requirements.txt"),
plan.NewExecCommand(fmt.Sprintf("pip install --target=%s -r requirements.txt", PACKAGES_DIR)),
})
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"PYTHON", "PIP", "PIPX"})
maps.Copy(install.Variables, p.GetPythonEnvVars(ctx))
maps.Copy(install.Variables, map[string]string{
"PIP_CACHE_DIR": PIP_CACHE_DIR,
"PYTHONPATH": PACKAGES_DIR,
})
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepInput(install.Name()))
ctx.Deploy.StartCmd = p.GetStartCommand(ctx)
maps.Copy(ctx.Deploy.Variables, p.GetPythonEnvVars(ctx))
ctx.Deploy.Inputs = []plan.Input{
plan.NewStepInput(p.GetImageWithRuntimeDeps(ctx).Name()),
plan.NewStepInput(ctx.GetMiseStepBuilder().Name(), plan.InputOptions{
Include: ctx.GetMiseStepBuilder().GetOutputPaths(),
}),
plan.NewStepInput(build.Name(), plan.InputOptions{
Include: []string{PACKAGES_DIR, "."},
}),
plan.NewLocalInput("."),
}
}
func (p *PythonProvider) GetImageWithRuntimeDeps(ctx *generate.GenerateContext) *generate.AptStepBuilder {
aptStep := ctx.NewAptStepBuilder("python-runtime-deps")
aptStep.Inputs = []plan.Input{
ctx.DefaultRuntimeInput(),
}
for dep, requiredPkgs := range pythonRuntimeDepRequirements {
if p.usesDep(ctx, dep) {
aptStep.Packages = append(aptStep.Packages, requiredPkgs...)
}
}
return aptStep
}
func (p *PythonProvider) GetBuilderDeps(ctx *generate.GenerateContext) *generate.MiseStepBuilder {
miseStep := ctx.GetMiseStepBuilder()
miseStep.SupportingAptPackages = append(miseStep.SupportingAptPackages, "python3-dev", "libpq-dev")
return miseStep
}
func (p *PythonProvider) InstallPythonDeps(ctx *generate.GenerateContext, install *generate.CommandStepBuilder) {
maps.Copy(install.Variables, p.GetPythonEnvVars(ctx))
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"PYTHON", "PIP", "PIPX", "PIPENV", "UV", "POETRY", "PDM"})
install.AddPaths([]string{"/root/.local/bin", "/app/.venv/bin"})
hasRequirements := p.hasRequirements(ctx)
hasPyproject := p.hasPyproject(ctx)
@@ -77,13 +336,6 @@ func (p *PythonProvider) install(ctx *generate.GenerateContext) error {
hasPdm := p.hasPdm(ctx)
hasUv := p.hasUv(ctx)
setup := ctx.NewCommandStep("setup")
setup.AddEnvVars(p.GetPythonEnvVars(ctx))
setup.AddPaths([]string{"/root/.local/bin"})
install := ctx.NewCommandStep("install")
install.DependsOn = append(install.DependsOn, setup.DisplayName)
if hasRequirements {
install.AddCache(ctx.Caches.AddCache("pip", PIP_CACHE_DIR))
install.AddCommands([]plan.Command{
@@ -99,8 +351,6 @@ func (p *PythonProvider) install(ctx *generate.GenerateContext) error {
plan.NewExecCommand("poetry install --no-interaction --no-ansi --no-root"),
})
} else if hasPyproject && hasPdm {
// TODO: Fix this. PDM is not working because the packages are installed into a venv
// that is not available to python at runtime
install.AddEnvVars(map[string]string{"PDM_CHECK_UPDATE": "false"})
install.AddCommands([]plan.Command{
plan.NewExecCommand("pipx install pdm"),
@@ -143,48 +393,39 @@ func (p *PythonProvider) install(ctx *generate.GenerateContext) error {
}
}
install.Secrets = []string{}
install.UseSecretsWithPrefixes([]string{"PYTHON", "PIP", "PIPX", "PIPENV", "UV", "POETRY", "PDM"})
// Handle system dependencies
aptStep := ctx.NewAptStepBuilder("python-system-deps")
aptStep.Packages = []string{"pkg-config"}
install.DependsOn = append(install.DependsOn, aptStep.DisplayName)
for dep, requiredPkgs := range pythonDepRequirements {
for dep, requiredPkgs := range pythonRuntimeDepRequirements {
if p.usesDep(ctx, dep) {
aptStep.Packages = append(aptStep.Packages, requiredPkgs...)
}
}
return nil
}
func (p *PythonProvider) packages(ctx *generate.GenerateContext) error {
packages := ctx.GetMiseStepBuilder()
python := packages.Default("python", DEFAULT_PYTHON_VERSION)
func (p *PythonProvider) InstallMisePackages(ctx *generate.GenerateContext, miseStep *generate.MiseStepBuilder) {
python := miseStep.Default("python", DEFAULT_PYTHON_VERSION)
if envVersion, varName := ctx.Env.GetConfigVariable("PYTHON_VERSION"); envVersion != "" {
packages.Version(python, envVersion, varName)
miseStep.Version(python, envVersion, varName)
}
if versionFile, err := ctx.App.ReadFile(".python-version"); err == nil {
packages.Version(python, string(versionFile), ".python-version")
miseStep.Version(python, string(versionFile), ".python-version")
}
if runtimeFile, err := ctx.App.ReadFile("runtime.txt"); err == nil {
packages.Version(python, string(runtimeFile), "runtime.txt")
miseStep.Version(python, string(runtimeFile), "runtime.txt")
}
if pipfileVersion := parseVersionFromPipfile(ctx); pipfileVersion != "" {
packages.Version(python, pipfileVersion, "Pipfile")
miseStep.Version(python, pipfileVersion, "Pipfile")
}
if p.hasPoetry(ctx) || p.hasUv(ctx) || p.hasPdm(ctx) {
packages.Default("pipx", "latest")
if p.hasPoetry(ctx) || p.hasUv(ctx) || p.hasPdm(ctx) || p.hasPipfile(ctx) {
miseStep.Default("pipx", "latest")
}
return nil
}
func (p *PythonProvider) GetPythonEnvVars(ctx *generate.GenerateContext) map[string]string {
@@ -195,7 +436,6 @@ func (p *PythonProvider) GetPythonEnvVars(ctx *generate.GenerateContext) map[str
"PYTHONDONTWRITEBYTECODE": "1",
"PIP_DISABLE_PIP_VERSION_CHECK": "1",
"PIP_DEFAULT_TIMEOUT": "100",
"PIP_CACHE_DIR": PIP_CACHE_DIR,
}
}
@@ -214,15 +454,16 @@ func (p *PythonProvider) addMetadata(ctx *generate.GenerateContext) {
pkgManager = "uv"
}
ctx.Metadata.Set("packageManager", pkgManager)
ctx.Metadata.SetBool("requirements", p.hasRequirements(ctx))
ctx.Metadata.SetBool("pyproject", p.hasPyproject(ctx))
ctx.Metadata.SetBool("pipfile", p.hasPipfile(ctx))
ctx.Metadata.Set("pythonPackageManager", pkgManager)
ctx.Metadata.SetBool("pythonHasRequirementsTxt", p.hasRequirements(ctx))
ctx.Metadata.SetBool("pythonHasPyproject", p.hasPyproject(ctx))
ctx.Metadata.SetBool("pythonHasPipfile", p.hasPipfile(ctx))
}
func (p *PythonProvider) usesDep(ctx *generate.GenerateContext, dep string) bool {
for _, file := range []string{"requirements.txt", "pyproject.toml", "Pipfile"} {
if contents, err := ctx.App.ReadFile(file); err == nil {
// TODO: Do something better than string comparison
if strings.Contains(strings.ToLower(contents), strings.ToLower(dep)) {
return true
}
@@ -272,8 +513,8 @@ func (p *PythonProvider) hasUv(ctx *generate.GenerateContext) bool {
}
// Mapping of python dependencies to required apt packages
var pythonDepRequirements = map[string][]string{
"pdf2image": {"poppler-utils", "gcc"},
"pydub": {"ffmpeg", "gcc"},
"pymovie": {"ffmpeg", "qt5-qmake", "qtbase5-dev", "qtbase5-dev-tools", "qttools5-dev-tools", "libqt5core5a", "python3-pyqt5", "gcc"},
var pythonRuntimeDepRequirements = map[string][]string{
"pdf2image": {"poppler-utils"},
"pydub": {"ffmpeg"},
"pymovie": {"ffmpeg", "qt5-qmake", "qtbase5-dev", "qtbase5-dev-tools", "qttools5-dev-tools", "libqt5core5a", "python3-pyqt5"},
}
+65 -59
View File
@@ -22,19 +22,27 @@ type StaticfileConfig struct {
RootDir string `yaml:"root"`
}
type StaticfileProvider struct{}
type StaticfileProvider struct {
RootDir string
}
func (p *StaticfileProvider) Name() string {
return "staticfile"
}
func (p *StaticfileProvider) Initialize(ctx *generate.GenerateContext) error {
rootDir, err := getRootDir(ctx)
if err != nil {
return err
}
p.RootDir = rootDir
return nil
}
func (p *StaticfileProvider) Detect(ctx *generate.GenerateContext) (bool, error) {
rootDir, err := p.getRootDir(ctx)
rootDir, err := getRootDir(ctx)
if rootDir != "" && err == nil {
return true, nil
}
@@ -43,90 +51,56 @@ func (p *StaticfileProvider) Detect(ctx *generate.GenerateContext) (bool, error)
}
func (p *StaticfileProvider) Plan(ctx *generate.GenerateContext) error {
p.Packages(ctx)
miseStep := ctx.GetMiseStepBuilder()
miseStep.Default("caddy", "latest")
rootDir, err := p.getRootDir(ctx)
setup := ctx.NewCommandStep("setup")
setup.AddInput(plan.NewStepInput(miseStep.Name()))
err := p.Setup(ctx, setup)
if err != nil {
return err
}
setupStep, err := p.SetupCaddy(ctx, rootDir)
if err != nil {
return err
ctx.Deploy.Inputs = []plan.Input{
ctx.DefaultRuntimeInput(),
plan.NewStepInput(miseStep.Name(), plan.InputOptions{
Include: miseStep.GetOutputPaths(),
}),
plan.NewStepInput(setup.Name(), plan.InputOptions{
Include: []string{"."},
}),
plan.NewLocalInput("."),
}
setupStep.DependsOn = append(setupStep.DependsOn, ctx.GetMiseStepBuilder().DisplayName)
ctx.Start.AddOutputs([]string{"."})
ctx.Start.Command = p.CaddyStartCommand(ctx, rootDir)
ctx.Deploy.StartCmd = p.CaddyStartCommand(ctx)
return nil
}
func (p *StaticfileProvider) CaddyStartCommand(ctx *generate.GenerateContext, rootDir string) string {
func (p *StaticfileProvider) CaddyStartCommand(ctx *generate.GenerateContext) string {
return "caddy run --config " + CaddyfilePath + " --adapter caddyfile 2>&1"
}
func (p *StaticfileProvider) SetupCaddy(ctx *generate.GenerateContext, rootDir string) (*generate.CommandStepBuilder, error) {
func (p *StaticfileProvider) Setup(ctx *generate.GenerateContext, setup *generate.CommandStepBuilder) error {
data := map[string]interface{}{
"STATIC_FILE_ROOT": rootDir,
"STATIC_FILE_ROOT": p.RootDir,
}
caddyfile, err := p.getCaddyfile(data)
if err != nil {
return nil, err
return err
}
setupStep := ctx.NewCommandStep("setup")
setupStep.AddCommands([]plan.Command{
setup.AddCommands([]plan.Command{
plan.NewFileCommand(CaddyfilePath, "Caddyfile"),
// format to validate the Caddyfile
plan.NewExecCommand("caddy fmt --overwrite Caddyfile"),
})
setupStep.Assets = map[string]string{
setup.Assets = map[string]string{
"Caddyfile": caddyfile,
}
return setupStep, nil
}
func (p *StaticfileProvider) Packages(ctx *generate.GenerateContext) {
miseStep := ctx.GetMiseStepBuilder()
miseStep.Default("caddy", "latest")
}
func (p *StaticfileProvider) getRootDir(ctx *generate.GenerateContext) (string, error) {
if rootDir, _ := ctx.Env.GetConfigVariable("STATIC_FILE_ROOT"); rootDir != "" {
return rootDir, nil
}
staticfileConfig, err := p.getStaticfileConfig(ctx)
if staticfileConfig != nil && err == nil {
return staticfileConfig.RootDir, nil
}
if ctx.App.HasMatch("public") {
return "public", nil
} else if ctx.App.HasMatch("index.html") {
return ".", nil
}
return "", fmt.Errorf("no static file root dir found")
}
func (p *StaticfileProvider) getStaticfileConfig(ctx *generate.GenerateContext) (*StaticfileConfig, error) {
if !ctx.App.HasMatch(StaticfileConfigName) {
return nil, nil
}
staticfileData := StaticfileConfig{}
if err := ctx.App.ReadYAML(StaticfileConfigName, &staticfileData); err != nil {
return nil, err
}
return &staticfileData, nil
return nil
}
func (p *StaticfileProvider) getCaddyfile(data map[string]interface{}) (string, error) {
@@ -142,3 +116,35 @@ func (p *StaticfileProvider) getCaddyfile(data map[string]interface{}) (string,
return buf.String(), nil
}
func getRootDir(ctx *generate.GenerateContext) (string, error) {
if rootDir, _ := ctx.Env.GetConfigVariable("STATIC_FILE_ROOT"); rootDir != "" {
return rootDir, nil
}
staticfileConfig, err := getStaticfileConfig(ctx)
if staticfileConfig != nil && err == nil {
return staticfileConfig.RootDir, nil
}
if ctx.App.HasMatch("public") {
return "public", nil
} else if ctx.App.HasMatch("index.html") {
return ".", nil
}
return "", fmt.Errorf("no static file root dir found")
}
func getStaticfileConfig(ctx *generate.GenerateContext) (*StaticfileConfig, error) {
if !ctx.App.HasMatch(StaticfileConfigName) {
return nil, nil
}
staticfileData := StaticfileConfig{}
if err := ctx.App.ReadYAML(StaticfileConfigName, &staticfileData); err != nil {
return nil, err
}
return &staticfileData, nil
}
+1 -2
View File
@@ -88,8 +88,7 @@ func TestGetRootDir(t *testing.T) {
ctx.Env.SetVariable(k, v)
}
provider := StaticfileProvider{}
got, err := provider.getRootDir(ctx)
got, err := getRootDir(ctx)
if tt.expectError {
require.Error(t, err)
+4 -1
View File
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/railwayapp/railpack/core/app"
"github.com/railwayapp/railpack/core/config"
"github.com/railwayapp/railpack/core/generate"
)
@@ -18,7 +19,9 @@ func CreateGenerateContext(t *testing.T, path string) *generate.GenerateContext
env := app.NewEnvironment(nil)
ctx, err := generate.NewGenerateContext(userApp, env)
config := config.EmptyConfig()
ctx, err := generate.NewGenerateContext(userApp, env, config)
if err != nil {
t.Fatalf("error creating generate context: %v", err)
}
+5 -4
View File
@@ -1,10 +1,10 @@
{
"$schema": "../../test/schema.json",
"providers": ["python"],
"provider": "python",
"packages": {
"python": "latest"
},
"aptPackages": ["neofetch"],
"buildAptPackages": ["neofetch"],
"steps": {
"build": {
"commands": ["neofetch"],
@@ -13,7 +13,8 @@
}
}
},
"start": {
"cmd": "neofetch $HELLO"
"deploy": {
"aptPackages": ["neofetch"],
"startCommand": "neofetch $HELLO"
}
}
+5 -3
View File
@@ -1,13 +1,15 @@
{
"$schema": "../../test/schema.json",
"providers": [],
"aptPackages": ["cowsay"],
"buildAptPackages": ["cowsay"],
"steps": {
"build": {
"commands": [{ "path": "/usr/games" }, "cowsay hello"]
}
},
"start": {
"cmd": "cowsay hello"
"deploy": {
"aptPackages": ["cowsay"],
"paths": ["/usr/games"],
"startCommand": "cowsay hello"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "../../test/schema.json",
"packages": {
"go": "1.23.5"
}
}
+22 -2
View File
@@ -1,15 +1,21 @@
{
"name": "22",
"name": "node-npm",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "22",
"name": "node-npm",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"dayjs": "^1.11.13"
},
"devDependencies": {
"typescript": "^5.6.3"
},
"engines": {
"node": "23.5.0"
}
},
"node_modules/dayjs": {
@@ -17,6 +23,20 @@
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}
+3
View File
@@ -15,5 +15,8 @@
"description": "",
"dependencies": {
"dayjs": "^1.11.13"
},
"devDependencies": {
"typescript": "^5.6.3"
}
}
+3
View File
@@ -1,3 +1,6 @@
import strawberry
import numpy as np
print(np.__version__)
print(strawberry)
print("Hello from Python-PDM")
+59 -34
View File
@@ -3,37 +3,73 @@
[metadata]
groups = ["default"]
strategy = ["cross_platform"]
strategy = []
lock_version = "4.5.0"
content_hash = "sha256:05981b9e064b616cee0146951466f6a19becc0b90d22d565d98088945e24e87b"
content_hash = "sha256:d74fecdd5593d25e82dc4a1d29e5b2f14bc4bfc9c18873b5db3dc6b655fa1622"
[[metadata.targets]]
requires_python = ">=3.8"
[[package]]
name = "astunparse"
version = "1.6.3"
summary = "An AST unparser for Python"
dependencies = [
"six<2.0,>=1.6.1",
"wheel<1.0,>=0.23.0",
]
files = [
{file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"},
{file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"},
]
requires_python = ">=3.11"
[[package]]
name = "graphql-core"
version = "3.2.5"
version = "3.2.6"
requires_python = "<4,>=3.6"
summary = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
dependencies = [
"typing-extensions<5,>=4; python_version < \"3.10\"",
]
files = [
{file = "graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a"},
{file = "graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5"},
{file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"},
{file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"},
]
[[package]]
name = "numpy"
version = "2.2.3"
requires_python = ">=3.10"
summary = "Fundamental package for array computing in Python"
files = [
{file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"},
{file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"},
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"},
{file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"},
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"},
{file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"},
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"},
{file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"},
{file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"},
{file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"},
{file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"},
{file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"},
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"},
{file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"},
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"},
{file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"},
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"},
{file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"},
{file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"},
{file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"},
{file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"},
{file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"},
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"},
{file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"},
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"},
{file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"},
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"},
{file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"},
{file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"},
{file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"},
{file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"},
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"},
{file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"},
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"},
{file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"},
{file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"},
{file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"},
{file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"},
]
[[package]]
@@ -61,18 +97,17 @@ files = [
[[package]]
name = "strawberry-graphql"
version = "0.255.0"
requires_python = "<4.0,>=3.8"
version = "0.260.2"
requires_python = "<4.0,>=3.9"
summary = "A library for creating GraphQL APIs"
dependencies = [
"astunparse<2.0.0,>=1.6.3; python_version < \"3.9\"",
"graphql-core<3.4.0,>=3.2.0",
"python-dateutil<3.0.0,>=2.7.0",
"typing-extensions>=4.5.0",
]
files = [
{file = "strawberry_graphql-0.255.0-py3-none-any.whl", hash = "sha256:890b454cd6ae56e5e63c2a80e1930bd1bd97dcebc91047d89913c2b008110b9f"},
{file = "strawberry_graphql-0.255.0.tar.gz", hash = "sha256:bbed6da18887d63d8306b4085cc56370a04e8b15eda87e954e0133674ea824a6"},
{file = "strawberry_graphql-0.260.2-py3-none-any.whl", hash = "sha256:b3eb30bb1318b3a2d4405c100618b36e082899309fea8550029ef530d56c1276"},
{file = "strawberry_graphql-0.260.2.tar.gz", hash = "sha256:c630a570c941a0c6b34398c7f9c87bcb5af18383557f1a1f45ed957a4ec7f4b7"},
]
[[package]]
@@ -84,13 +119,3 @@ files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "wheel"
version = "0.45.1"
requires_python = ">=3.8"
summary = "A built-package format for Python"
files = [
{file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"},
{file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"},
]
+4 -4
View File
@@ -1,5 +1,5 @@
[project]
dependencies = [
"strawberry-graphql>=0.192.0",
]
requires-python = ">=3.8"
name = "python-pdm"
version = "0.1.0"
dependencies = ["strawberry-graphql>=0.192.0", "numpy>=1.24.4"]
requires-python = ">=3.11"
+5
View File
@@ -0,0 +1,5 @@
[
{
"expectedOutput": "Hello from Python-PDM"
}
]
+1 -1
View File
@@ -7,4 +7,4 @@ print("numpy", np.__version__)
print("pandas", pd.__version__)
print("psycopg2", psycopg2.__version__)
print("Hello from Python")
print("Hello from UV")
+1 -1
View File
@@ -1,5 +1,5 @@
[
{
"expectedOutput": "Hello from Python"
"expectedOutput": "Hello from UV"
}
]
+13
View File
@@ -0,0 +1,13 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
cowsay = "*"
[dev-packages]
[requires]
python_version = "3.13"
python_full_version = "3.13.2"
+30
View File
@@ -0,0 +1,30 @@
{
"_meta": {
"hash": {
"sha256": "f855c7f410b3ae66d683bbb0c5940c1f456eabd69a517647f5b4a63804baa5e3"
},
"pipfile-spec": 6,
"requires": {
"python_full_version": "3.13.2",
"python_version": "3.13"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"cowsay": {
"hashes": [
"sha256:274b1e6fc1b966d53976333eb90ac94cb07a450a700b455af9fbdf882244b30a"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==6.1"
}
},
"develop": {}
}
+3
View File
@@ -0,0 +1,3 @@
import cowsay
cowsay.cow("Hello from Python-Pipfile")
+5
View File
@@ -0,0 +1,5 @@
[
{
"expectedOutput": "Hello from Python-Pipfile"
}
]
+5 -8
View File
@@ -9,26 +9,23 @@
"secrets": ["MY_SECRET", "MY_OTHER_SECRET"],
"variables": {
"NOT_SECRET": "not secret"
},
"outputs": ["/app"]
}
},
"defaultsToUsing": {
"commands": [{ "src": ".", "dest": "." }, "./run.sh"],
"variables": {
"NOT_SECRET": "not secret"
},
"outputs": ["/app"]
}
},
"doesNotUseSecrets": {
"commands": [{ "src": ".", "dest": "." }, "./run.sh true"],
"secrets": [],
"outputs": ["/app"]
"secrets": []
}
},
"start": {
"cmd": "./run.sh"
"deploy": {
"startCommand": "./run.sh"
}
}
+5
View File
@@ -0,0 +1,5 @@
[
{
"expectedOutput": "using config from file"
}
]
+1
View File
@@ -37,6 +37,7 @@ ENV MISE_INSTALL_PATH=/usr/local/bin/mise \
MISE_DATA_DIR=/mise \
MISE_CONFIG_DIR=/mise \
MISE_CACHE_DIR=/mise/cache \
DEBIAN_FRONTEND=noninteractive \
PATH=/mise/shims:$PATH
# Copy mise binary from builder