Add scion build command + Dockerfile hash fix#244
Conversation
Introduces a top-level `scion build <harness-config-name>` CLI command that builds a container image from a Dockerfile bundled in a harness-config directory. Supports --tag, --base-image, --push, --platform, and --dry-run flags. After a successful build, updates the harness-config's config.yaml image field to reference the built image. Also fixes Dockerfile content hashing: Dockerfiles were previously excluded from ComputeHarnessConfigRevision, so changes to them did not trigger re-sync to the Hub. Removes Dockerfile from the skipBasenames exclusion list. Extracts detectContainerRuntime() from pkg/hub/maintenance_executors.go into a shared pkg/runtime/container.go so both the new build command and the Hub executor can use it.
There was a problem hiding this comment.
Code Review
This pull request introduces a new build command to build container images from a harness-config Dockerfile, extracts container runtime detection into a shared package, and ensures that Dockerfile changes are included when computing harness-config revisions. Feedback on the changes highlights a critical issue in cmd/build.go where updating config.yaml via unmarshaling and marshaling into a struct can cause data and formatting loss. It is recommended to use yaml.Node for in-place modification and to perform an atomic file write to prevent corruption.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| var configEntry config.HarnessConfigEntry | ||
| if err := yaml.Unmarshal(configData, &configEntry); err != nil { | ||
| return fmt.Errorf("failed to parse config.yaml: %w", err) | ||
| } | ||
| configEntry.Image = outputImage | ||
| updatedData, err := yaml.Marshal(&configEntry) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to marshal updated config.yaml: %w", err) | ||
| } | ||
| if err := os.WriteFile(configPath, updatedData, 0644); err != nil { | ||
| return fmt.Errorf("failed to write updated config.yaml: %w", err) | ||
| } |
There was a problem hiding this comment.
Updating config.yaml by unmarshaling into a struct (config.HarnessConfigEntry) and marshaling it back has two major issues:
- Data Loss: Any fields in
config.yamlthat are not mapped in theHarnessConfigEntrystruct (e.g., newer fields or custom extensions) will be silently discarded. - Comment/Formatting Loss: All comments, custom formatting, and key ordering in the YAML file will be stripped, which degrades the user experience for manually managed configuration files.
Additionally, writing directly to configPath using os.WriteFile is not atomic. If the write fails (e.g., due to disk full or power loss), the configuration file can be corrupted or truncated.
Recommendation:
Use yaml.Node from gopkg.in/yaml.v3 to parse the document as an AST, modify the image field in-place, and write the updated YAML atomically using a temporary file and os.Rename.
var node yaml.Node
if err := yaml.Unmarshal(configData, &node); err != nil {
return fmt.Errorf("failed to parse config.yaml: %w", err)
}
if len(node.Content) == 0 {
node.Kind = yaml.DocumentNode
node.Content = []*yaml.Node{
{Kind: yaml.MappingNode, Tag: "!!map"},
}
}
updated := false
if node.Content[0].Kind == yaml.MappingNode {
mapping := node.Content[0]
for i := 0; i < len(mapping.Content); i += 2 {
if mapping.Content[i].Value == "image" {
mapping.Content[i+1].Value = outputImage
mapping.Content[i+1].Tag = "!!str"
updated = true
break
}
}
if !updated {
mapping.Content = append(mapping.Content,
&yaml.Node{Kind: yaml.ScalarNode, Value: "image", Tag: "!!str"},
&yaml.Node{Kind: yaml.ScalarNode, Value: outputImage, Tag: "!!str"},
)
}
}
updatedData, err := yaml.Marshal(&node)
if err != nil {
return fmt.Errorf("failed to marshal updated config.yaml: %w", err)
}
tmpPath := configPath + ".tmp"
if err := os.WriteFile(tmpPath, updatedData, 0644); err != nil {
return fmt.Errorf("failed to write temporary config.yaml: %w", err)
}
if err := os.Rename(tmpPath, configPath); err != nil {
return fmt.Errorf("failed to replace config.yaml atomically: %w", err)
}…tings H1: Replace destructive yaml.Unmarshal/Marshal round-trip with targeted yaml.Node edit for config.yaml image update. Preserves comments, field order, and unknown fields. M1: Handle all os.Stat errors on Dockerfile, not just IsNotExist. M2: Load settings once instead of twice when both --base-image is unset and --push is set. L3: Remove extra blank line in maintenance_executors.go.
Add empty-path check after FindHarnessConfigDir to prevent synthetic harness-configs (e.g. 'generic') from resolving Dockerfile against CWD. Verify yaml.MappingNode kind before manipulating doc.Content[0] to handle malformed config.yaml gracefully.
Summary
scion buildCLI command (cmd/build.go) — builds container images from harness-config Dockerfiles with support for--tag,--base-image,--push,--platform, and--dry-runflags. After build, updates the harness-config'sconfig.yamlimage field.skipBasenamesinComputeHarnessConfigRevision()so Dockerfile changes are reflected in content hashing and trigger re-sync to the Hub.DetectContainerRuntime()— extracts the container runtime detection utility frompkg/hub/maintenance_executors.gotopkg/runtime/container.gofor reuse by both the build command and the Hub executor.Files changed
cmd/build.goscion buildcommandpkg/runtime/container.goDetectContainerRuntime()pkg/config/harness_config.gopkg/config/harness_config_test.gopkg/hub/maintenance_executors.goDetectContainerRuntime(), remove local copyTest plan
go build ./...passesgo vetclean on all changed packagesTestComputeHarnessConfigRevision_SkipsNonRuntimeFilesupdated and passingscion build <name>with a harness-config containing a Dockerfilebuildnot visible inSCION_CLI_MODE=agent scion --helpbuildvisible inSCION_CLI_MODE=assistant scion --help