feat(buildkit): add RAILPACK_BUILT_AT environment variable (#669)

- Add `RAILPACK_BUILT_AT` to runtime image environment containing Unix epoch seconds
- Document runtime Railpack environment variables and precedence rules
- Add unit tests verifying environment generation and cache isolation for deploy variables

Generated-by: aiautocommit
This commit is contained in:
Michael Bianco
2026-08-10 14:19:00 -06:00
committed by GitHub
parent 2a6ff9c723
commit 7e7e3cbafe
3 changed files with 88 additions and 6 deletions
+5 -4
View File
@@ -5,7 +5,9 @@ import (
"fmt"
"maps"
"slices"
"strconv"
"strings"
"time"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/util/system"
@@ -32,9 +34,7 @@ type ConvertPlanOptions struct {
NoCache bool
}
const (
WorkingDir = "/app"
)
const WorkingDir = "/app"
func ConvertPlanToLLB(plan *p.BuildPlan, opts ConvertPlanOptions) (*llb.State, *Image, error) {
platform := opts.BuildPlatform
@@ -107,11 +107,12 @@ func getImageEnv(graphOutput *build_llb.BuildGraphOutput, plan *p.BuildPlan) []s
slices.Sort(paths)
pathString := strings.Join(paths, ":")
envMap := make(map[string]string, len(graphOutput.GraphEnv.EnvVars)+len(plan.Deploy.Variables)+1)
envMap := make(map[string]string, len(graphOutput.GraphEnv.EnvVars)+len(plan.Deploy.Variables)+2)
maps.Copy(envMap, graphOutput.GraphEnv.EnvVars)
maps.Copy(envMap, plan.Deploy.Variables)
envMap["PATH"] = pathString
envMap["RAILPACK_BUILT_AT"] = strconv.FormatInt(time.Now().Unix(), 10)
envVars := make([]string, 0, len(envMap))
for _, k := range slices.Sorted(maps.Keys(envMap)) {
+69
View File
@@ -0,0 +1,69 @@
package buildkit
import (
"context"
"slices"
"strconv"
"strings"
"testing"
"time"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/railwayapp/railpack/buildkit/build_llb"
"github.com/railwayapp/railpack/core/plan"
"github.com/stretchr/testify/require"
)
func TestGetImageEnvIncludesBuiltAt(t *testing.T) {
graphOutput := &build_llb.BuildGraphOutput{
GraphEnv: build_llb.NewGraphEnvironment(),
}
buildPlan := plan.NewBuildPlan()
buildPlan.Deploy.Variables = map[string]string{
"RAILPACK_BUILT_AT": "custom",
}
before := time.Now().Unix()
env := getImageEnv(graphOutput, buildPlan)
after := time.Now().Unix()
builtAt := ""
for _, variable := range env {
if value, ok := strings.CutPrefix(variable, "RAILPACK_BUILT_AT="); ok {
builtAt = value
}
}
timestamp, err := strconv.ParseInt(builtAt, 10, 64)
require.NoError(t, err)
require.GreaterOrEqual(t, timestamp, before)
require.LessOrEqual(t, timestamp, after)
require.True(t, slices.IsSorted(env))
}
func TestDeployVariablesDoNotAffectLLB(t *testing.T) {
firstPlan := plan.NewBuildPlan()
firstPlan.Deploy.Base = plan.NewImageLayer("alpine:latest")
firstPlan.Deploy.Variables = map[string]string{"VALUE": "one"}
secondPlan := plan.NewBuildPlan()
secondPlan.Deploy.Base = plan.NewImageLayer("alpine:latest")
secondPlan.Deploy.Variables = map[string]string{"VALUE": "two"}
opts := ConvertPlanOptions{
BuildPlatform: specs.Platform{
OS: "linux",
Architecture: "amd64",
},
}
firstState, firstImage, err := ConvertPlanToLLB(firstPlan, opts)
require.NoError(t, err)
secondState, secondImage, err := ConvertPlanToLLB(secondPlan, opts)
require.NoError(t, err)
firstDefinition, err := firstState.Marshal(context.Background())
require.NoError(t, err)
secondDefinition, err := secondState.Marshal(context.Background())
require.NoError(t, err)
require.Equal(t, firstDefinition.ToPB(), secondDefinition.ToPB())
require.NotEqual(t, firstImage.Config.Env, secondImage.Config.Env)
}
+14 -2
View File
@@ -42,8 +42,20 @@ Environment variables can be set in two ways:
}
```
Railpack always sets `RAILPACK_VERSION` on the final runtime image to the
Railpack version that produced the image (for example `0.12.3`).
## Runtime Railpack Variables
The final image includes runtime metadata that is not available to build
commands:
| Name | Description |
| :------------------ | :--------------------------------------- |
| `RAILPACK_VERSION` | Version used to produce the image |
| `RAILPACK_BUILT_AT` | Build time as Unix epoch seconds (UTC) |
Runtime metadata is added after generating the build graph, so it does not
invalidate application layer caches. The `RAILPACK_` namespace in the final
image is reserved for generated runtime metadata, which takes precedence over
deploy variables with the same name.
## Secrets