{{ message }}
config: substitute variables in feature-contributed metadata#53
Merged
Conversation
Feature-supplied mounts, env, and lifecycle commands now get host-context
substitution. Previously only the user's devcontainer.json fields were
resolved by ResolveBytes; feature/base-image metadata folded in by
MergeMetadata kept their literal ${devcontainerId}, ${localWorkspaceFolder},
${localEnv:*} tokens and reached ContainerCreate unchanged, causing Docker
to reject mounts like dind-var-lib-docker-${devcontainerId} as having
invalid volume name characters.
MergeMetadata now accepts a SubstitutionContext and resolves layer-
contributed strings before folding them into cfg. Callers (Up's
applyMetadataMerge, Attach) build the context from cfg + localEnv.
Fixes #52.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
up.go (1)
263-277:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize
localEnvbefore metadata substitution in Up paths.
applyMetadataMergecurrently usesopts.LocalEnvas-is. When nil,${localEnv:*}from feature/base metadata will not resolve from host env, which conflicts with theUpOptions.LocalEnvcontract (nil => useos.Environ()) and differs fromAttachWithbehavior.Suggested fix
func applyMetadataMerge(cfg *config.ResolvedConfig, baseLayers []config.FeatureMetadata, localEnv map[string]string) { + if localEnv == nil { + localEnv = environAsMap(os.Environ()) + } chain := make([]config.FeatureMetadata, 0, len(baseLayers)+len(cfg.Features)) chain = append(chain, baseLayers...) for _, f := range cfg.Features { if f.AlreadyInstalled { continue } chain = append(chain, f.Metadata) } subCtx := config.SubstitutionContext{ LocalWorkspaceFolder: cfg.LocalWorkspaceFolder, ContainerWorkspaceFolder: cfg.ContainerWorkspaceFolder, DevcontainerID: cfg.DevcontainerID, LocalEnv: localEnv, } config.MergeMetadata(cfg, subCtx, chain) cfg.Finalize() }Also applies to: 475-475, 863-879
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f31bd3dd-e0b5-473a-af10-4790707b6cba
📒 Files selected for processing (5)
attach.goconfig/merge_metadata.goconfig/merge_metadata_test.gotest/integration/feature_mount_substitution_test.goup.go
After `if x == nil { t.Fatal(...) }`, staticcheck doesn't model
t.Fatal/t.Fatalf as terminating, so subsequent dereferences trip
SA5011. Add an explicit return so the analyzer sees the post-nil
code as unreachable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Fix #52: feature-contributed mounts (and env, lifecycle commands, …) now get host-context substitution before reaching Docker.
Previously
ResolveBytesonly substituted variables in the user'sdevcontainer.json. Fields folded in afterwards byMergeMetadatafrom feature / base-image metadata kept their literal\${devcontainerId},\${localWorkspaceFolder},\${localEnv:*}tokens and flowed straight intoContainerCreate. The docker-in-docker feature'sdind-var-lib-docker-\${devcontainerId}volume mount tripped Docker's volume name validator.MergeMetadatanow takes aSubstitutionContextand resolves layer-contributed strings before folding them in. Substitution copies — input layers are not mutated, so cached feature metadata stays clean. Call sites (applyMetadataMergeinup.go,Attach) build the context fromcfg+localEnv.Test plan
TestMergeMetadata_SubstitutesLayerStringscovers mount sources, env values, and lifecycle commands with\${devcontainerId}/\${localWorkspaceFolder}/\${localEnv:*}TestMergeMetadata_SubstitutionDoesNotMutateLayerspins the no-input-mutation invariant (feature metadata is often shared / cached)integration, requires Docker):TestFeatureMountSubstitutionreproduces the docker-in-docker mount shape with a local feature, assertsUpsucceeds and the inspected container mount path embeds the substituted volume name🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests