Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f17e2c2
Initial plan
Copilot Jan 29, 2026
d179eb0
Add common-setup.sh helper script with user selection logic and compr…
Copilot Jan 29, 2026
9b63ddf
Update all feature scripts to use common-setup.sh helper for user sel…
Copilot Jan 29, 2026
2eecb71
Update java wrapper.sh to use common-setup.sh helper
Copilot Jan 29, 2026
c5712f9
Add documentation for _common helper scripts
Copilot Jan 29, 2026
79add1a
Fix code review issues: validate _REMOTE_USER, fix common-utils none …
Copilot Jan 29, 2026
6345e9d
Increment minor version for all 17 features impacted by user selectio…
Copilot Feb 6, 2026
22b654d
Fix JSON indentation to preserve original formatting
Copilot Feb 6, 2026
33e2e26
Rename _common to .common to fix validation failure
Copilot Feb 6, 2026
67e27ee
Deploy common-setup.sh to each feature's _lib directory
Copilot Feb 6, 2026
5358fa3
Add sync mechanism for shared code with single source of truth
Copilot Feb 6, 2026
3f37648
Fix common-utils to restore original user creation logic
Copilot Feb 6, 2026
0b2a9e6
Update sync script to exclude common-utils (uses inline logic)
Copilot Feb 6, 2026
15c6f0a
Fix conda installation failure due to SHA1 signature rejection (#1565)
Copilot Feb 11, 2026
57e02bc
Fix kubectl-helm-minikube installation failures on debian:11 and ubun…
Copilot Feb 11, 2026
27284f4
Fix certificate verification for Ubuntu 24.04/Debian Trixie in docker…
Copilot Feb 11, 2026
61bed5f
Move common-setup.sh from _lib/ to feature root, fix test runner
abdurriq Mar 27, 2026
db61bb9
Fix merge conflicts: consistent CI-time sync approach
abdurriq Mar 31, 2026
a361ac5
Merge branch 'main' into copilot/unify-user-selection-logic
abdurriq Apr 1, 2026
e99a520
Update with new java version
abdurriq Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/docker-in-docker-stress-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand All @@ -30,6 +33,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Publish"
uses: devcontainers/action@v1
with:
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/test-all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,14 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

- name: "Generating tests for '${{ matrix.features }}' against '${{ matrix.baseImage }}'"
run: devcontainer features test --skip-scenarios -f ${{ matrix.features }} -i ${{ matrix.baseImage }} .
run: devcontainer features test --skip-scenarios -f ${{ matrix.features }} -i ${{ matrix.baseImage }} .

test-scenarios:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -98,6 +101,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand All @@ -110,6 +116,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test-manual.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/test-pr-arm64.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand All @@ -60,6 +63,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/test-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand All @@ -86,6 +89,9 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: "Sync shared scripts into features"
run: bash scripts/sync-common-setup.sh

- name: "Install latest devcontainer CLI"
run: npm install -g @devcontainers/cli

Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
workspace.code-workspace
workspace.code-workspace

# Auto-generated by scripts/sync-common-setup.sh at CI/build time
src/*/common-setup.sh
94 changes: 94 additions & 0 deletions SHARED_CODE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Shared Code Maintenance

This document explains how shared code is maintained across features in this repository.

## Problem

Multiple features need the same helper functions (e.g., user selection logic). The devcontainer specification currently packages each feature independently and doesn't support sharing code between features at runtime.

## Solution

We maintain a **single source of truth** with a **CI-time sync mechanism** to deploy to each feature:

### Single Source
- **Location**: `scripts/lib/common-setup.sh`
- **Contains**: Shared helper functions (currently user selection logic)
- **Maintenance**: All updates happen here

### Deployment
- **Mechanism**: `scripts/sync-common-setup.sh` (runs automatically in CI)
- **Target**: Copies to each feature's directory as `common-setup.sh` at build/test time
- **Reason**: Devcontainer packaging requires files to be within each feature's directory
- **Note**: The copies are `.gitignore`d β€” only the source file is tracked in git

## Workflow

### Making Changes

1. **Edit the source**: Modify `scripts/lib/common-setup.sh`
2. **Test**: Run `bash test/_global/test-common-setup.sh`
3. **Commit**: Only the source file needs to be committed

```bash
# Edit the source
vim scripts/lib/common-setup.sh

# Test
bash test/_global/test-common-setup.sh

# Commit just the source
git add scripts/lib/common-setup.sh
git commit -m "Update common-setup.sh helper function"
```

### Local Development

To generate the copies locally (e.g., for testing features outside CI):

```bash
./scripts/sync-common-setup.sh
```

### CI Integration

All CI workflows (test, release, stress test) automatically run `sync-common-setup.sh`
after checkout and before the devcontainer CLI packages features. This ensures the
copies are always present and up-to-date without tracking them in git.

## Why Not Use Shared Files?

The devcontainer CLI packages each feature independently. When a feature is installed:

1. Only files within the feature's directory are included in the package
2. Parent directories (`../common`) are not accessible
3. Hidden directories (`.common`) are excluded from packaging
4. Sibling feature directories are not accessible

This is a design decision in the devcontainer specification to ensure features are portable and self-contained.

## Future

The devcontainer spec has a proposal for an `include` property in `devcontainer-feature.json` ([spec#129](https://github.com/devcontainers/spec/issues/129)) that would enable native code sharing. Once implemented, the sync mechanism can be removed in favor of declarative includes:

```json
{
"id": "my-feature",
"include": ["../../scripts/lib/common-setup.sh"]
}
```

## Current Implementation

As of this PR:
- **Source**: `scripts/lib/common-setup.sh` (87 lines)
- **Deployed**: 16 features, each with `src/FEATURE/common-setup.sh` (generated at CI time, gitignored)
- **Sync Script**: `scripts/sync-common-setup.sh` (called by all CI workflows)
- **Tests**: `test/_global/test-common-setup.sh` (14 test cases)
- **Benefits**: Eliminated ~188 lines of inline duplicated logic from install scripts, zero duplicate files tracked in git

## References

- [Devcontainer Spec Issue #129 - Share code between features](https://github.com/devcontainers/spec/issues/129)
- [Features Library Proposal](https://github.com/devcontainers/spec/blob/main/proposals/features-library.md)
- Test documentation: `test/_global/test-common-setup.sh`
- Sync script documentation: `scripts/README.md`
59 changes: 59 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Shared Feature Code

This directory contains code that is shared across multiple features.

## Structure

```
scripts/
β”œβ”€β”€ lib/
β”‚ └── common-setup.sh # Source of truth for user selection helper
└── sync-common-setup.sh # Script to deploy helper to all features
```

## Maintenance

### The Source of Truth

**`scripts/lib/common-setup.sh`** is the single source of truth for the user selection helper function. All modifications should be made to this file.

### Deploying Changes

Due to the devcontainer CLI's packaging behavior (each feature is packaged independently), the helper must be present in each feature's directory at packaging time. This is handled automatically by CI β€” all workflows run `scripts/sync-common-setup.sh` after checkout and before packaging.

The copies are `.gitignore`d β€” only the source file is tracked in git.

### Workflow

1. **Edit**: Make changes to `scripts/lib/common-setup.sh`
2. **Test**: Run `bash test/_global/test-common-setup.sh` to verify
3. **Commit**: Only the source file needs to be committed

### Why Copies?

The devcontainer CLI packages each feature independently:
- Parent directories are not included in the build context
- Hidden directories (`.common`) are not included
- Sibling directories are not accessible

Therefore, each feature needs its own copy of the helper to ensure it's available at runtime during feature installation.

## Testing

Tests are located in `test/_global/` and source from the canonical `scripts/lib/common-setup.sh`:

```bash
bash test/_global/test-common-setup.sh
```

### Local Development

To generate the copies locally (e.g., for testing features outside CI):

```bash
./scripts/sync-common-setup.sh
```

## Future

This approach is a workaround for the current limitation. The devcontainer spec has a proposal for an `include` property in `devcontainer-feature.json` that would allow native code sharing (see [devcontainers/spec#129](https://github.com/devcontainers/spec/issues/129)). Once implemented, this sync mechanism can be removed.
87 changes: 87 additions & 0 deletions scripts/lib/common-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/bin/bash
#-------------------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://github.com/devcontainers/features/blob/main/LICENSE for license information.
#-------------------------------------------------------------------------------------------------------------------------
#
# Helper script for common feature setup tasks, including user selection logic.
# Maintainer: The Dev Container spec maintainers

# Determine the appropriate non-root user
# Usage: determine_user_from_input USERNAME [FALLBACK_USER]
#
# This function resolves the USERNAME variable based on the input value:
# - If USERNAME is "auto" or "automatic", it will detect an existing non-root user
# - If USERNAME is "none" or doesn't exist, it will fall back to root
# - Otherwise, it validates the specified USERNAME exists
#
# Arguments:
# USERNAME - The username input (typically from feature configuration)
# FALLBACK_USER - Optional fallback user when no user is found in automatic mode (defaults to "root")
#
# Returns:
# The resolved username is printed to stdout
#
# Examples:
# USERNAME=$(determine_user_from_input "automatic")
# USERNAME=$(determine_user_from_input "vscode")
# USERNAME=$(determine_user_from_input "auto" "vscode")
#
determine_user_from_input() {
local input_username="${1:-automatic}"
local fallback_user="${2:-root}"
local resolved_username=""

if [ "${input_username}" = "auto" ] || [ "${input_username}" = "automatic" ]; then
# Automatic mode: try to detect an existing non-root user

# First, check if _REMOTE_USER is set and is not root
if [ -n "${_REMOTE_USER:-}" ] && [ "${_REMOTE_USER}" != "root" ]; then
# Verify the user exists before using it
if id -u "${_REMOTE_USER}" > /dev/null 2>&1; then
resolved_username="${_REMOTE_USER}"
else
# _REMOTE_USER doesn't exist, fall through to normal detection
resolved_username=""
fi
fi

# If we didn't resolve via _REMOTE_USER, try to find a non-root user
if [ -z "${resolved_username}" ]; then
# Try to find a non-root user from a list of common usernames
# The list includes: devcontainer, vscode, node, codespace, and the user with UID 1000
local possible_users=("devcontainer" "vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '')")

for current_user in "${possible_users[@]}"; do
# Skip empty entries
if [ -z "${current_user}" ]; then
continue
fi

# Check if user exists
if id -u "${current_user}" > /dev/null 2>&1; then
resolved_username="${current_user}"
break
fi
done

# If no user found, use the fallback
if [ -z "${resolved_username}" ]; then
resolved_username="${fallback_user}"
fi
fi
elif [ "${input_username}" = "none" ]; then
# Explicit "none" means use root
resolved_username="root"
else
# Specific username provided - validate it exists
if id -u "${input_username}" > /dev/null 2>&1; then
resolved_username="${input_username}"
else
# User doesn't exist, fall back to root
resolved_username="root"
fi
fi

echo "${resolved_username}"
}
Loading
Loading