forked from wrenn/wrenn
Add pre/post build stages, fix exec timeout, expand guest PATH
Build phases: - Pre-build (apt update) and post-build (apt clean, autoremove, rm lists) run with 10-minute timeout; user recipe commands keep 30s timeout - Log entries include phase field for UI grouping - Always send explicit TimeoutSec to host agent (0 defaulted to 30s) Frontend: - Pre-build/post-build steps show phase label without exposing commands - Recipe steps numbered independently starting from 1 Guest PATH: - Add /usr/games:/usr/local/games to wrenn-init.sh PATH export (standard Ubuntu paths, needed for packages like cowsay)
This commit is contained in:
@ -2,6 +2,7 @@ import { apiFetch, type ApiResult } from '$lib/api/client';
|
|||||||
|
|
||||||
export type BuildLogEntry = {
|
export type BuildLogEntry = {
|
||||||
step: number;
|
step: number;
|
||||||
|
phase: string; // "pre-build", "recipe", or "post-build"
|
||||||
cmd: string;
|
cmd: string;
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
|
|||||||
@ -521,6 +521,9 @@
|
|||||||
{#if build.logs && build.logs.length > 0}
|
{#if build.logs && build.logs.length > 0}
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
{#each build.logs as log, i (i)}
|
{#each build.logs as log, i (i)}
|
||||||
|
{@const isInternal = log.phase === 'pre-build' || log.phase === 'post-build'}
|
||||||
|
{@const recipeIdx = log.phase === 'recipe' ? build.logs.filter(l => l.phase === 'recipe' && l.step <= log.step).length : 0}
|
||||||
|
{@const phaseLabel = isInternal ? (log.phase === 'pre-build' ? 'Pre-build' : 'Post-build') : `Step ${recipeIdx}`}
|
||||||
<div class="rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-1)] overflow-hidden">
|
<div class="rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-1)] overflow-hidden">
|
||||||
<!-- Step header -->
|
<!-- Step header -->
|
||||||
<button
|
<button
|
||||||
@ -533,10 +536,16 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--color-red)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--color-red)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if isInternal}
|
||||||
|
<span class="flex-1 text-label font-semibold text-[var(--color-text-tertiary)]">
|
||||||
|
{phaseLabel}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
<span class="text-label font-semibold text-[var(--color-text-tertiary)]">
|
<span class="text-label font-semibold text-[var(--color-text-tertiary)]">
|
||||||
Step {log.step}
|
{phaseLabel}
|
||||||
</span>
|
</span>
|
||||||
<code class="flex-1 truncate font-mono text-meta text-[var(--color-text-primary)]">{log.cmd}</code>
|
<code class="flex-1 truncate font-mono text-meta text-[var(--color-text-primary)]">{log.cmd}</code>
|
||||||
|
{/if}
|
||||||
<span class="shrink-0 font-mono text-label text-[var(--color-text-muted)]">{log.elapsed_ms}ms</span>
|
<span class="shrink-0 font-mono text-label text-[var(--color-text-muted)]">{log.elapsed_ms}ms</span>
|
||||||
{#if log.exit !== 0}
|
{#if log.exit !== 0}
|
||||||
<span class="shrink-0 rounded-full bg-[var(--color-red)]/10 px-1.5 py-0.5 font-mono text-label text-[var(--color-red)]">
|
<span class="shrink-0 rounded-full bg-[var(--color-red)]/10 px-1.5 py-0.5 font-mono text-label text-[var(--color-red)]">
|
||||||
|
|||||||
@ -25,7 +25,7 @@ echo "nameserver 8.8.8.8" > /etc/resolv.conf
|
|||||||
echo "nameserver 8.8.4.4" >> /etc/resolv.conf
|
echo "nameserver 8.8.4.4" >> /etc/resolv.conf
|
||||||
|
|
||||||
# Set a standard PATH so envd and all child processes can find common binaries.
|
# Set a standard PATH so envd and all child processes can find common binaries.
|
||||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
|
||||||
|
|
||||||
# Write chrony config to sync time from the KVM PTP hardware clock.
|
# Write chrony config to sync time from the KVM PTP hardware clock.
|
||||||
# /dev/ptp0 is a paravirtual clock exposed by KVM — no network required.
|
# /dev/ptp0 is a paravirtual clock exposed by KVM — no network required.
|
||||||
|
|||||||
@ -49,6 +49,7 @@ type buildAgentClient interface {
|
|||||||
// BuildLogEntry represents a single entry in the build log JSONB array.
|
// BuildLogEntry represents a single entry in the build log JSONB array.
|
||||||
type BuildLogEntry struct {
|
type BuildLogEntry struct {
|
||||||
Step int `json:"step"`
|
Step int `json:"step"`
|
||||||
|
Phase string `json:"phase"` // "pre-build", "recipe", or "post-build"
|
||||||
Cmd string `json:"cmd"`
|
Cmd string `json:"cmd"`
|
||||||
Stdout string `json:"stdout"`
|
Stdout string `json:"stdout"`
|
||||||
Stderr string `json:"stderr"`
|
Stderr string `json:"stderr"`
|
||||||
@ -182,18 +183,13 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse user recipe and wrap with pre/post build stages.
|
// Parse user recipe.
|
||||||
var userRecipe []string
|
var recipe []string
|
||||||
if err := json.Unmarshal(build.Recipe, &userRecipe); err != nil {
|
if err := json.Unmarshal(build.Recipe, &recipe); err != nil {
|
||||||
s.failBuild(ctx, buildID, fmt.Sprintf("invalid recipe JSON: %v", err))
|
s.failBuild(ctx, buildID, fmt.Sprintf("invalid recipe JSON: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
recipe := make([]string, 0, len(userRecipe)+len(preBuildCmds)+len(postBuildCmds))
|
|
||||||
recipe = append(recipe, preBuildCmds...)
|
|
||||||
recipe = append(recipe, userRecipe...)
|
|
||||||
recipe = append(recipe, postBuildCmds...)
|
|
||||||
|
|
||||||
// Pick a platform host and create a sandbox.
|
// Pick a platform host and create a sandbox.
|
||||||
host, err := s.Scheduler.SelectHost(ctx, id.PlatformTeamID, false)
|
host, err := s.Scheduler.SelectHost(ctx, id.PlatformTeamID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -232,24 +228,41 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
HostID: host.ID,
|
HostID: host.ID,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Execute recipe commands.
|
// Execute build phases: pre-build → user recipe → post-build.
|
||||||
var logs []BuildLogEntry
|
var logs []BuildLogEntry
|
||||||
for i, cmd := range recipe {
|
step := 0
|
||||||
log.Info("executing build step", "step", i+1, "cmd", cmd)
|
|
||||||
|
// Helper to run a list of commands in a given phase.
|
||||||
|
// timeout=0 means no timeout (uses parent context).
|
||||||
|
runPhase := func(phase string, cmds []string, timeout time.Duration) bool {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
step++
|
||||||
|
log.Info("executing build step", "phase", phase, "step", step, "cmd", cmd)
|
||||||
|
|
||||||
|
execCtx := ctx
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
// When no timeout is specified, use 10 minutes as a generous upper
|
||||||
|
// bound. The host agent defaults TimeoutSec=0 to 30s, so we must
|
||||||
|
// always send an explicit value.
|
||||||
|
effectiveTimeout := timeout
|
||||||
|
if effectiveTimeout <= 0 {
|
||||||
|
effectiveTimeout = 10 * time.Minute
|
||||||
|
}
|
||||||
|
execCtx, cancel = context.WithTimeout(ctx, effectiveTimeout)
|
||||||
|
timeoutSec := int32(effectiveTimeout.Seconds())
|
||||||
|
|
||||||
execCtx, cancel := context.WithTimeout(ctx, buildCommandTimeout)
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
execResp, err := agent.Exec(execCtx, connect.NewRequest(&pb.ExecRequest{
|
execResp, err := agent.Exec(execCtx, connect.NewRequest(&pb.ExecRequest{
|
||||||
SandboxId: sandboxIDStr,
|
SandboxId: sandboxIDStr,
|
||||||
Cmd: "/bin/sh",
|
Cmd: "/bin/sh",
|
||||||
Args: []string{"-c", cmd},
|
Args: []string{"-c", cmd},
|
||||||
TimeoutSec: int32(buildCommandTimeout.Seconds()),
|
TimeoutSec: timeoutSec,
|
||||||
}))
|
}))
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
entry := BuildLogEntry{
|
entry := BuildLogEntry{
|
||||||
Step: i + 1,
|
Step: step,
|
||||||
|
Phase: phase,
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
Elapsed: time.Since(start).Milliseconds(),
|
Elapsed: time.Since(start).Milliseconds(),
|
||||||
}
|
}
|
||||||
@ -258,10 +271,10 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
entry.Stderr = err.Error()
|
entry.Stderr = err.Error()
|
||||||
entry.Ok = false
|
entry.Ok = false
|
||||||
logs = append(logs, entry)
|
logs = append(logs, entry)
|
||||||
s.updateLogs(ctx, buildID, i+1, logs)
|
s.updateLogs(ctx, buildID, step, logs)
|
||||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||||
s.failBuild(ctx, buildID, fmt.Sprintf("step %d exec error: %v", i+1, err))
|
s.failBuild(ctx, buildID, fmt.Sprintf("%s step %d failed: %v", phase, step, err))
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Stdout = string(execResp.Msg.Stdout)
|
entry.Stdout = string(execResp.Msg.Stdout)
|
||||||
@ -269,14 +282,25 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
|||||||
entry.Exit = execResp.Msg.ExitCode
|
entry.Exit = execResp.Msg.ExitCode
|
||||||
entry.Ok = execResp.Msg.ExitCode == 0
|
entry.Ok = execResp.Msg.ExitCode == 0
|
||||||
logs = append(logs, entry)
|
logs = append(logs, entry)
|
||||||
|
s.updateLogs(ctx, buildID, step, logs)
|
||||||
s.updateLogs(ctx, buildID, i+1, logs)
|
|
||||||
|
|
||||||
if execResp.Msg.ExitCode != 0 {
|
if execResp.Msg.ExitCode != 0 {
|
||||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||||
s.failBuild(ctx, buildID, fmt.Sprintf("step %d failed with exit code %d", i+1, execResp.Msg.ExitCode))
|
s.failBuild(ctx, buildID, fmt.Sprintf("%s step %d failed with exit code %d", phase, step, execResp.Msg.ExitCode))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !runPhase("pre-build", preBuildCmds, 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !runPhase("recipe", recipe, buildCommandTimeout) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !runPhase("post-build", postBuildCmds, 0) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Healthcheck or direct snapshot.
|
// Healthcheck or direct snapshot.
|
||||||
|
|||||||
Reference in New Issue
Block a user