Skip to content

feat(config): resolve variable substitution in extends paths#254

Closed
skevetter wants to merge 1 commit into
mainfrom
feat/varsub-extends-paths
Closed

feat(config): resolve variable substitution in extends paths#254
skevetter wants to merge 1 commit into
mainfrom
feat/varsub-extends-paths

Conversation

@skevetter

@skevetter skevetter commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix variable substitution (${localEnv:X}, ${localWorkspaceFolder}, etc.) in devcontainer.json extends paths
  • Previously, extends resolution happened before variable substitution, so variable references in extends paths were used as literal strings
  • Apply substitution to extends ref strings before the resolver attempts to open referenced files, in both the top-level parse entry point and recursive resolution path

Spec reference: https://containers.dev/implementors/reference/

Summary by CodeRabbit

  • Bug Fixes

    • Variable substitution in devcontainer extends references now works correctly, enabling use of environment variables and workspace folder paths in extends declarations.
  • Tests

    • Added test coverage for variable substitution scenarios in extends field resolution.

The devcontainer spec allows variable substitution (e.g., ${localEnv:X},
${localWorkspaceFolder}) in all string properties including extends paths.
Previously, extends resolution happened before variable substitution, so
variable references in extends paths were used as literal strings.

Apply variable substitution to extends ref strings before the extends
resolver attempts to open the referenced files. This handles both the
top-level ParseDevContainerJSONFile entry point and the recursive
parseDevContainerJSONFileWithVisited path.
@netlify

netlify Bot commented May 7, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev failed.

Name Link
🔨 Latest commit 6bad718
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/69fc1e84eb2e360008bb2484

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds variable substitution support to devcontainer extends references. Before resolving parent configurations, the code now substitutes environment variables (e.g., ${localEnv:...}) and workspace folder paths (e.g., ${localWorkspaceFolder}) in the extends field, enabling dynamic inheritance paths based on runtime context.

Changes

Variable Substitution in Extends Resolution

Layer / File(s) Summary
Core Implementation
pkg/devcontainer/config/parse.go
New substituteExtendsRefs helper builds a substitution context from the config file's directory and environment variables (with Windows env-key normalization), then applies ResolveString to each extends reference.
Integration
pkg/devcontainer/config/extends.go
parseDevContainerJSONFileWithVisited calls substituteExtendsRefs on devContainer.Extends before calling resolveExtendsArray, ensuring variables are resolved prior to parent config merge.
Tests
pkg/devcontainer/config/extends_test.go
Three new test functions validate ${localEnv:...} resolution from environment, ${localWorkspaceFolder} resolution to parent directory of .devcontainer, and missing env variables resolving to empty string (causing parse error).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • devsy-org/devsy#249: Modifies core variable substitution semantics (two-pass/restrictedReplace) that directly affects the substitution mechanism used in extends refs.
  • devsy-org/devsy#253: Threads context through resolveExtendsArray and parseDevContainerJSONFile, which relates to the extends-resolution pipeline being modified here.

Suggested labels

size/l

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(config): resolve variable substitution in extends paths' directly and accurately describes the main change: enabling variable substitution in extends path references before resolution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/m label May 7, 2026
@coderabbitai coderabbitai Bot added the size/l label May 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/devcontainer/config/parse.go`:
- Around line 294-295: The computed localWorkspaceFolder in
substituteExtendsRefs is wrong for non-.devcontainer paths; pass the workspace
folder through the call chain instead of deriving it with
filepath.Dir(filepath.Dir(configFilePath)). Add a workspaceFolder string
parameter to ParseDevContainerJSONFile, parseDevContainerJSONFileWithVisited,
and substituteExtendsRefs (and update callers in extends.go) so the correct
workspace root is used; alternatively, as a minimal interim fix implement the
suggested heuristic inside substituteExtendsRefs to detect root-level and nested
.devcontainer layouts rather than always using Dir(Dir(configFilePath)). Ensure
all function signatures and recursive calls are updated to accept and forward
workspaceFolder.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dec43bd6-76a4-422a-b7f9-176ccf5481c0

📥 Commits

Reviewing files that changed from the base of the PR and between c6034f3 and 6bad718.

📒 Files selected for processing (3)
  • pkg/devcontainer/config/extends.go
  • pkg/devcontainer/config/extends_test.go
  • pkg/devcontainer/config/parse.go

Comment on lines +294 to +295
func substituteExtendsRefs(refs ExtendsRef, configFilePath string) ExtendsRef {
localWorkspaceFolder := filepath.Dir(filepath.Dir(configFilePath))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

localWorkspaceFolder is computed incorrectly for non-.devcontainer/ config paths.

filepath.Dir(filepath.Dir(configFilePath)) only yields the correct workspace root when configFilePath lives inside a .devcontainer/ subdirectory (the canonical .devcontainer/devcontainer.json layout). For the other two standard layouts it is wrong:

Config path Dir(Dir(path)) Correct workspace
/ws/.devcontainer/devcontainer.json /ws /ws
/ws/.devcontainer.json (root-level) parent of /ws /ws
/ws/.devcontainer/myproj/devcontainer.json /ws/.devcontainer /ws

And when substituteExtendsRefs is called recursively from extends.go with a parent file that lives outside a .devcontainer/ directory (e.g., a shared base at /shared/base.json), the computed workspace is again wrong.

The proper fix is to thread the workspace folder through the call chain as a parameter. A lower-effort intermediate heuristic that handles the two most common layouts:

🛠️ Suggested heuristic fix
 func substituteExtendsRefs(refs ExtendsRef, configFilePath string) ExtendsRef {
-	localWorkspaceFolder := filepath.Dir(filepath.Dir(configFilePath))
+	// localWorkspaceFolder: parent of the .devcontainer/ dir when the config
+	// is inside one, otherwise the directory that contains the config file.
+	configDir := filepath.Dir(configFilePath)
+	localWorkspaceFolder := configDir
+	if filepath.Base(configDir) == ".devcontainer" {
+		localWorkspaceFolder = filepath.Dir(configDir)
+	}

The long-term fix is to accept workspaceFolder string as a parameter on ParseDevContainerJSONFile, parseDevContainerJSONFileWithVisited, and substituteExtendsRefs, matching the devcontainer spec where this value is provided by the host.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func substituteExtendsRefs(refs ExtendsRef, configFilePath string) ExtendsRef {
localWorkspaceFolder := filepath.Dir(filepath.Dir(configFilePath))
func substituteExtendsRefs(refs ExtendsRef, configFilePath string) ExtendsRef {
// localWorkspaceFolder: parent of the .devcontainer/ dir when the config
// is inside one, otherwise the directory that contains the config file.
configDir := filepath.Dir(configFilePath)
localWorkspaceFolder := configDir
if filepath.Base(configDir) == ".devcontainer" {
localWorkspaceFolder = filepath.Dir(configDir)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/devcontainer/config/parse.go` around lines 294 - 295, The computed
localWorkspaceFolder in substituteExtendsRefs is wrong for non-.devcontainer
paths; pass the workspace folder through the call chain instead of deriving it
with filepath.Dir(filepath.Dir(configFilePath)). Add a workspaceFolder string
parameter to ParseDevContainerJSONFile, parseDevContainerJSONFileWithVisited,
and substituteExtendsRefs (and update callers in extends.go) so the correct
workspace root is used; alternatively, as a minimal interim fix implement the
suggested heuristic inside substituteExtendsRefs to detect root-level and nested
.devcontainer layouts rather than always using Dir(Dir(configFilePath)). Ensure
all function signatures and recursive calls are updated to accept and forward
workspaceFolder.

@skevetter

Copy link
Copy Markdown
Contributor Author

Closing — after investigation, this fix is not warranted:

  1. extends is not part of the formal devcontainer spec, so there's no spec mandate for varSub in extends paths
  2. The localWorkspaceFolder derivation in the fix is incorrect (assumes 2-level depth which breaks for .devcontainer.json and nested configs)
  3. The early-pass substitution lacks the full SubstitutionContext (InitEnv, DevContainerID) creating inconsistency
  4. No user demand or practical use case identified — extends paths are authored by developers who control file layout

@skevetter skevetter closed this May 7, 2026
@skevetter
skevetter deleted the feat/varsub-extends-paths branch May 17, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant