Skip to content

feat(config): parse and resolve devcontainer secrets property#83

Merged
skevetter merged 3 commits into
mainfrom
sk-fix-secrets-property
Apr 23, 2026
Merged

feat(config): parse and resolve devcontainer secrets property#83
skevetter merged 3 commits into
mainfrom
sk-fix-secrets-property

Conversation

@skevetter

@skevetter skevetter commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add Secrets map[string]SecretConfig field to DevContainerConfigBase for parsing the devcontainer spec secrets property
  • Define SecretConfig struct with Description and DocumentationUrl fields
  • Resolve declared secrets from host environment variables and inject them into lifecycle command environments
  • Add unit tests for config parsing round-trip and lifecycle secret resolution

Summary by CodeRabbit

  • New Features
    • Added support for declaring secrets in dev container configuration, enabling secrets to be referenced from the host environment and made available during container setup.

@netlify

netlify Bot commented Apr 23, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit bc52c33
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/69ea60eec5c541000800d3f9

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@skevetter has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 39 minutes and 53 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 39 minutes and 53 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71fb778b-ac64-4b47-a0a2-55a2748d372d

📥 Commits

Reviewing files that changed from the base of the PR and between 170f1b0 and bc52c33.

📒 Files selected for processing (1)
  • pkg/devcontainer/config/parse_test.go
📝 Walkthrough

Walkthrough

Introduces a new SecretConfig struct and adds a Secrets map field to DevContainerConfigBase for devcontainer configuration. Updates JSON parsing/serialization and lifecycle environment resolution to handle declared secrets from the host process environment.

Changes

Cohort / File(s) Summary
Config Model
pkg/devcontainer/config/config.go
Introduces SecretConfig struct with Description and DocumentationUrl fields; adds Secrets map to DevContainerConfigBase with JSON tag secrets,omitempty.
Config Parsing & Serialization
pkg/devcontainer/config/parse_test.go
Adds comprehensive test coverage for parsing secrets from JSON files and verifying round-trip serialization/deserialization of SecretConfig entries.
Lifecycle Hooks
pkg/devcontainer/setup/lifecyclehooks.go
Updates resolveLifecycleEnv to resolve declared secrets from host process environment using os.Getenv and apply non-empty values to the merged remote environment.
Lifecycle Hooks Testing
pkg/devcontainer/setup/lifecyclehooks_test.go
Adds test verifying that secrets defined in config are populated from host environment into lifecycleEnv.remoteEnv when present, and excluded when absent.

Sequence Diagram

sequenceDiagram
    participant ConfigFile as Config File
    participant Parser as Config Parser
    participant LifecycleEnv as Lifecycle Env Resolver
    participant HostEnv as Host Environment
    participant RemoteEnv as Remote Environment

    ConfigFile->>Parser: Parse devcontainer.json
    Parser->>Parser: Extract Secrets declarations
    Parser->>LifecycleEnv: Pass MergedConfig with Secrets
    LifecycleEnv->>LifecycleEnv: Build merged remote environment
    LifecycleEnv->>HostEnv: Resolve each secret via os.Getenv
    HostEnv-->>LifecycleEnv: Return secret values
    LifecycleEnv->>RemoteEnv: Apply resolved secrets to env
    LifecycleEnv-->>Parser: Return lifecycleEnv with secrets
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(config): parse and resolve devcontainer secrets property' accurately summarizes the main change - adding support for parsing and resolving secrets in the devcontainer config, which aligns with all file changes across config parsing, lifecycle handling, and their tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.


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.

@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: 3

🧹 Nitpick comments (1)
pkg/devcontainer/config/config.go (1)

113-116: Consider renaming DocumentationUrl to DocumentationURL.

Go style (and common linters like revive/golint var-naming) recommends capitalizing initialisms such as URL. The JSON tag keeps the wire format unchanged, so this is purely a Go-side identifier rename.

Proposed rename
 type SecretConfig struct {
 	Description      string `json:"description,omitempty"`
-	DocumentationUrl string `json:"documentationUrl,omitempty"`
+	DocumentationURL string `json:"documentationUrl,omitempty"`
 }

The test in pkg/devcontainer/config/parse_test.go (line 43) would need the matching rename.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/config/config.go` around lines 113 - 116, Rename the
SecretConfig field DocumentationUrl to DocumentationURL to follow Go initialism
conventions; update the struct in SecretConfig (change DocumentationUrl →
DocumentationURL) but keep the json tag `documentationUrl` unchanged so wire
format is preserved, and update any references in tests and code (e.g.,
pkg/devcontainer/config/parse_test.go and any uses of
SecretConfig.DocumentationUrl) to the new identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/devcontainer/config/config.go`:
- Around line 107-110: MergeConfiguration currently copies
DevContainerConfigBase (which contains Secrets) from the user config once and
never re-merges imageMetadataEntries, causing feature-provided secrets to be
dropped; update MergeConfiguration in pkg/devcontainer/config/merge.go to merge
Secrets the same way RemoteEnv/ContainerEnv are merged by calling mergeMaps for
the Secrets map on each imageMetadataEntries iteration (use the same last-wins
merge policy as the other env maps) and add a brief comment documenting the
chosen policy so feature-contributed secrets are not silently ignored.

In `@pkg/devcontainer/setup/lifecyclehooks.go`:
- Around line 46-51: The current loop over mergedConfig.Secrets uses os.Getenv
and skips host-provided empty-string values and logs nothing when a secret is
missing; change it to use os.LookupEnv to distinguish unset vs empty-string so
intentionally-empty secrets are propagated according to policy, and add a
debug/warn log (without printing secret values) when LookupEnv reports the
variable is not present; update the loop that writes into env (referencing
mergedConfig.Secrets and env) to call os.LookupEnv and call the package logger
(or processLogger) to record unresolved secret names only.
- Around line 44-56: The current code applies mergedConfig.Secrets after calling
mergeRemoteEnv, letting host secrets override author-specified RemoteEnv; change
the order so secrets are merged into probedEnv before calling mergeRemoteEnv
(i.e., apply host secrets to the probedEnv map, then call
mergeRemoteEnv(probedEnv,...)) so mergeRemoteEnv can correctly let
mergedConfig.RemoteEnv override secrets; update any test (e.g.,
TestResolveLifecycleEnvIncludesSecrets) to include a collision case asserting
author RemoteEnv wins, and reference mergeRemoteEnv, mergedConfig.Secrets, and
lifecycleEnv when making the change.

---

Nitpick comments:
In `@pkg/devcontainer/config/config.go`:
- Around line 113-116: Rename the SecretConfig field DocumentationUrl to
DocumentationURL to follow Go initialism conventions; update the struct in
SecretConfig (change DocumentationUrl → DocumentationURL) but keep the json tag
`documentationUrl` unchanged so wire format is preserved, and update any
references in tests and code (e.g., pkg/devcontainer/config/parse_test.go and
any uses of SecretConfig.DocumentationUrl) to the new identifier.
🪄 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: 6c3cea80-2034-4f1e-b60f-5688490606d1

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6d7c8 and 170f1b0.

📒 Files selected for processing (4)
  • pkg/devcontainer/config/config.go
  • pkg/devcontainer/config/parse_test.go
  • pkg/devcontainer/setup/lifecyclehooks.go
  • pkg/devcontainer/setup/lifecyclehooks_test.go

Comment on lines +107 to +110
// Secrets declared by the dev container. Keys are secret names; values describe the secret.
// At runtime, each key is looked up in the host environment and, if set, injected into lifecycle commands.
Secrets map[string]SecretConfig `json:"secrets,omitempty"`
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm Secrets is not merged across ImageMetadata entries.
rg -nP -C3 '\bSecrets\b' --type=go

Repository: devsy-org/devsy

Length of output: 12220


🏁 Script executed:

cat -n pkg/devcontainer/config/merge.go

Repository: devsy-org/devsy

Length of output: 8867


🏁 Script executed:

# Search for ImageMetadata struct definition and its fields
rg -n "type ImageMetadata struct" -A 50 --type=go

Repository: devsy-org/devsy

Length of output: 1896


🏁 Script executed:

# Look for merge tests related to Secrets or features
fd -e "*_test.go" -x grep -l "MergeConfiguration\|merge.*[Ss]ecrets" {} \;

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Find all merge-related test files
fd -e "merge*test.go" --type=f

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# List all test files in config package
fd -e "*test.go" pkg/devcontainer/config/

Repository: devsy-org/devsy

Length of output: 460


🏁 Script executed:

# List all test files in config package
fd . 'pkg/devcontainer/config/' -e '*test.go'

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Search for test files in the config directory
ls -la pkg/devcontainer/config/ | grep test

Repository: devsy-org/devsy

Length of output: 263


🏁 Script executed:

# Search for MergeConfiguration in test files
rg -n "MergeConfiguration" pkg/devcontainer/config/ --type=go

Repository: devsy-org/devsy

Length of output: 119


🏁 Script executed:

# Search for MergeConfiguration usage across the codebase
rg -n "MergeConfiguration" --type=go

Repository: devsy-org/devsy

Length of output: 490


🏁 Script executed:

# Search for feature-related content and secrets in the codebase
rg -n "feature" -i pkg/devcontainer/config/merge.go

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Search for devcontainer spec references or feature documentation
rg -n "feature.*secret|secret.*feature" -i --type=go

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Check for any comments in merge.go about missing functionality or TODOs
rg -n "TODO|FIXME|XXX|BUG" pkg/devcontainer/config/merge.go

Repository: devsy-org/devsy

Length of output: 96


🏁 Script executed:

# Check the devcontainer spec comment in config.go for Secrets context
rg -n -B5 "Secrets.*secret" pkg/devcontainer/config/config.go

Repository: devsy-org/devsy

Length of output: 511


🏁 Script executed:

# Check how imageMetadataEntries are populated/created to understand if features can declare secrets
rg -n "imageMetadata|ImageMetadata" pkg/devcontainer/ -A3 | head -80

Repository: devsy-org/devsy

Length of output: 5049


🏁 Script executed:

# Find FeatureConfigToImageMetadata function
rg -n "FeatureConfigToImageMetadata" -A 20 --type=go

Repository: devsy-org/devsy

Length of output: 3279


🏁 Script executed:

# Check the FeatureConfig struct definition
rg -n "type FeatureConfig struct" -A 30 --type=go

Repository: devsy-org/devsy

Length of output: 2881


Secrets from feature/image metadata are dropped during merge.

Per MergeConfiguration in pkg/devcontainer/config/merge.go, the DevContainerConfigBase (which includes Secrets) is copied from the user config once at initialization (line 31) and never re-merged from the imageMetadataEntries. Since ImageMetadata embeds DevContainerConfigBase, any secrets declared in feature metadata are silently ignored.

The codebase already uses mergeMaps() for other maps like RemoteEnv and ContainerEnv. Apply the same pattern to Secrets to ensure feature-contributed secrets are properly merged (e.g., using a last-wins or first-wins policy with clear documentation).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/config/config.go` around lines 107 - 110, MergeConfiguration
currently copies DevContainerConfigBase (which contains Secrets) from the user
config once and never re-merges imageMetadataEntries, causing feature-provided
secrets to be dropped; update MergeConfiguration in
pkg/devcontainer/config/merge.go to merge Secrets the same way
RemoteEnv/ContainerEnv are merged by calling mergeMaps for the Secrets map on
each imageMetadataEntries iteration (use the same last-wins merge policy as the
other env maps) and add a brief comment documenting the chosen policy so
feature-contributed secrets are not silently ignored.

Comment on lines +44 to +56
env := mergeRemoteEnv(mergedConfig.RemoteEnv, probedEnv, remoteUser)

// Resolve declared secrets from the host environment.
for name := range mergedConfig.Secrets {
if val := os.Getenv(name); val != "" {
env[name] = val
}
}

return lifecycleEnv{
remoteUser: remoteUser,
workspaceFolder: setupInfo.SubstitutionContext.ContainerWorkspaceFolder,
remoteEnv: mergeRemoteEnv(mergedConfig.RemoteEnv, probedEnv, remoteUser),
remoteEnv: env,

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

🧩 Analysis chain

🌐 Web query:

devcontainer.json secrets spec precedence remoteEnv

💡 Result:

In devcontainer.json, there is a "secrets" property for declaring recommended secrets, primarily used in GitHub Codespaces to prompt users to set user-specific secrets. These are not stored in the file but reference secrets provided by the implementing tool/service (e.g., Codespaces secrets). Secrets are handled similarly to "remoteEnv": implementing tools inject them as environment variables for the runtime environment (VS Code server, terminals, tasks, etc.) after the container's ENTRYPOINT. The devcontainers CLI supports passing secrets via --secrets-file for commands like "up", applying them like remoteEnv. Precedence: devcontainer.json takes precedence over image/feature metadata for properties like remoteEnv and containerEnv (per-variable last value wins, devcontainer.json last when order matters). Secrets follow similar logic if contributed by features/images. No native Docker buildkit --secret support in devcontainer.json; use workarounds like docker-compose build.secrets or build.options. Example declaration: { "secrets": { "MY_SECRET": { "description": "Personal API key" } }, "remoteEnv": { "MY_SECRET": "${secret:MY_SECRET}" } }

Citations:


🏁 Script executed:

# First, let's look at the full function and surrounding context
cat -n pkg/devcontainer/setup/lifecyclehooks.go | head -80

Repository: devsy-org/devsy

Length of output: 2961


🏁 Script executed:

# Look for test cases related to secret handling
fd -t f lifecyclehooks_test.go

Repository: devsy-org/devsy

Length of output: 104


🏁 Script executed:

# Search for how secrets are used elsewhere in the codebase
rg -i "secrets" pkg/devcontainer/setup/ -A 3 -B 3 --max-count=50

Repository: devsy-org/devsy

Length of output: 1754


🏁 Script executed:

# Check for documentation or spec about secret precedence
fd -t f -i "spec\|readme\|doc" pkg/devcontainer/

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# View the full test case for secret handling
cat -n pkg/devcontainer/setup/lifecyclehooks_test.go | grep -A 40 "TestResolveLifecycleEnvIncludesSecrets"

Repository: devsy-org/devsy

Length of output: 1350


🏁 Script executed:

# Find the mergeRemoteEnv function definition
rg "func mergeRemoteEnv" pkg/devcontainer/setup/ -A 20

Repository: devsy-org/devsy

Length of output: 1673


🏁 Script executed:

# Check the Secrets configuration in the config package
rg "Secrets\s*map\[string\]" pkg/devcontainer/config/ -B 2 -A 2

Repository: devsy-org/devsy

Length of output: 494


🏁 Script executed:

# Search for any comments or TODOs about secret precedence
rg -i "secret.*precedence\|precedence.*secret\|secret.*override\|override.*secret" pkg/devcontainer/

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Check if there are any related GitHub issues or discussions referenced
rg -i "secret.*remoteenv\|remoteenv.*secret" pkg/devcontainer/ -B 3 -A 3

Repository: devsy-org/devsy

Length of output: 41


🏁 Script executed:

# Look at git history or comments around the secret handling code
rg "Resolve declared secrets" pkg/devcontainer/ -B 5 -A 10

Repository: devsy-org/devsy

Length of output: 1111


Secrets applied after mergeRemoteEnv create inconsistent precedence — author-specified values may be unexpectedly overwritten.

Secrets from the host environment (lines 47–50) are injected after mergeRemoteEnv(), which means a secret name matching a key in mergedConfig.RemoteEnv will overwrite the author-explicitly-set value. However, mergeRemoteEnv() itself states: "remoteEnv should always override probedEnv as it has been specified explicitly by the devcontainer author." This creates conflicting precedence: author-set RemoteEnv is prioritized over probed env, but then overridden by secrets. Either the order should be reversed (secrets first, then RemoteEnv), or the behavior should be clarified with a warning when a collision occurs. The existing test (TestResolveLifecycleEnvIncludesSecrets) does not cover the collision scenario.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/setup/lifecyclehooks.go` around lines 44 - 56, The current
code applies mergedConfig.Secrets after calling mergeRemoteEnv, letting host
secrets override author-specified RemoteEnv; change the order so secrets are
merged into probedEnv before calling mergeRemoteEnv (i.e., apply host secrets to
the probedEnv map, then call mergeRemoteEnv(probedEnv,...)) so mergeRemoteEnv
can correctly let mergedConfig.RemoteEnv override secrets; update any test
(e.g., TestResolveLifecycleEnvIncludesSecrets) to include a collision case
asserting author RemoteEnv wins, and reference mergeRemoteEnv,
mergedConfig.Secrets, and lifecycleEnv when making the change.

Comment on lines +46 to +51
// Resolve declared secrets from the host environment.
for name := range mergedConfig.Secrets {
if val := os.Getenv(name); val != "" {
env[name] = val
}
}

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 | 🟡 Minor

Empty-string host values silently skip declared secrets; also consider logging unresolved secrets.

Two concerns on this block:

  1. if val := os.Getenv(name); val != "" conflates "unset" with "set to empty string". os.LookupEnv is the idiomatic way to distinguish the two; whether an intentionally-empty secret should be propagated is a policy decision worth making explicit.
  2. When a declared secret is missing from the host environment, nothing is logged. Given these are declared secrets users opted into, a debug/warn log (without the value) would aid troubleshooting when lifecycle commands fail due to missing env vars.
Suggested adjustment
-	// Resolve declared secrets from the host environment.
-	for name := range mergedConfig.Secrets {
-		if val := os.Getenv(name); val != "" {
-			env[name] = val
-		}
-	}
+	// Resolve declared secrets from the host environment.
+	for name := range mergedConfig.Secrets {
+		if val, ok := os.LookupEnv(name); ok {
+			env[name] = val
+		} else {
+			log.Debugf("declared secret %q is not set in host environment; skipping", name)
+		}
+	}

Note: the existing test asserts that an absent secret is not present in remoteEnv, which the suggested change preserves. The val != "" check test case isn't covered — worth adding if you keep that semantic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainer/setup/lifecyclehooks.go` around lines 46 - 51, The current
loop over mergedConfig.Secrets uses os.Getenv and skips host-provided
empty-string values and logs nothing when a secret is missing; change it to use
os.LookupEnv to distinguish unset vs empty-string so intentionally-empty secrets
are propagated according to policy, and add a debug/warn log (without printing
secret values) when LookupEnv reports the variable is not present; update the
loop that writes into env (referencing mergedConfig.Secrets and env) to call
os.LookupEnv and call the package logger (or processLogger) to record unresolved
secret names only.

Split TestSecretsRoundTrip into TestParseSecrets and TestSecretsRoundTrip
to reduce complexity. Add nolint:cyclop directive for test assertions and
nosec G306 for test file writes (consistent with existing tests).
@skevetter
skevetter merged commit 64a6d4f into main Apr 23, 2026
45 checks passed
@skevetter
skevetter deleted the sk-fix-secrets-property branch April 23, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant