Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bf357fa
Initial plan
Copilot Jan 29, 2026
8c41a74
Add common-setup.sh helper script with user selection logic and compr…
Copilot Jan 29, 2026
8c1c8dc
Update all feature scripts to use common-setup.sh helper for user sel…
Copilot Jan 29, 2026
79767c0
Update java wrapper.sh to use common-setup.sh helper
Copilot Jan 29, 2026
5c4e356
Add documentation for _common helper scripts
Copilot Jan 29, 2026
964dccf
Fix code review issues: validate _REMOTE_USER, fix common-utils none …
Copilot Jan 29, 2026
8132d89
Merge branch 'main' into copilot/unify-user-selection-logic
abdurriq Feb 6, 2026
e6afb13
Increment minor version for all 17 features impacted by user selectio…
Copilot Feb 6, 2026
997feb3
Fix JSON indentation to preserve original formatting
Copilot Feb 6, 2026
e359246
Rename _common to .common to fix validation failure
Copilot Feb 6, 2026
3592f90
Deploy common-setup.sh to each feature's _lib directory
Copilot Feb 6, 2026
d036296
Add sync mechanism for shared code with single source of truth
Copilot Feb 6, 2026
35a5d93
Fix common-utils to restore original user creation logic
Copilot Feb 6, 2026
c2f0fbf
Update sync script to exclude common-utils (uses inline logic)
Copilot Feb 6, 2026
4e3ccd7
Fix conda installation failure due to SHA1 signature rejection (#1565)
Copilot Feb 11, 2026
dc5ef27
Merge branch 'main' into copilot/unify-user-selection-logic
abdurriq Feb 11, 2026
0428414
Fix kubectl-helm-minikube installation failures on debian:11 and ubun…
Copilot Feb 11, 2026
3f0dee2
Fix certificate verification for Ubuntu 24.04/Debian Trixie in docker…
Copilot Feb 11, 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
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 **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`
- **Target**: Copies to each feature's `_lib/` directory
- **Reason**: Devcontainer packaging requires files to be within each feature's directory

## Workflow

### Making Changes

1. **Edit the source**: Modify `scripts/lib/common-setup.sh`
2. **Test**: Run `bash test/_lib/test-common-setup.sh`
3. **Sync**: Run `./scripts/sync-common-setup.sh`
4. **Commit**: Include both source and deployed copies

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

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

# Deploy to all features
./scripts/sync-common-setup.sh

# Commit everything
git add scripts/lib/common-setup.sh src/*/_lib/common-setup.sh
git commit -m "Update common-setup.sh helper function"
```

### Verification

The sync script is idempotent - running it multiple times with the same source produces the same result. After syncing, you can verify:

```bash
# Check that all copies are identical
for f in src/*/_lib/common-setup.sh; do
diff -q scripts/lib/common-setup.sh "$f" || echo "MISMATCH: $f"
done
```

## 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**: 17 features, each with `src/FEATURE/_lib/common-setup.sh`
- **Sync Script**: `scripts/sync-common-setup.sh`
- **Tests**: `test/_lib/test-common-setup.sh` (14 test cases)
- **Benefits**: Eliminated ~188 lines of inline duplicated logic from install scripts

## 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/_lib/README.md`
- 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 deployed to each feature's `_lib/` directory. We maintain this through a sync script:

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

This copies `scripts/lib/common-setup.sh` to all features:
- `src/anaconda/_lib/common-setup.sh`
- `src/docker-in-docker/_lib/common-setup.sh`
- etc.

### Workflow

1. **Edit**: Make changes to `scripts/lib/common-setup.sh`
2. **Test**: Run `bash test/_lib/test-common-setup.sh` to verify
3. **Sync**: Run `./scripts/sync-common-setup.sh` to deploy to all features
4. **Commit**: Commit both the source and all copies together

### 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/_lib/` and reference the anaconda feature's copy as the source:

```bash
bash test/_lib/test-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}"
}
75 changes: 75 additions & 0 deletions scripts/sync-common-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/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.
#-------------------------------------------------------------------------------------------------------------------------
#
# Script to sync common-setup.sh from the source to all feature _lib directories
# This maintains a single source of truth while deploying to each feature for packaging
#
# Usage: ./scripts/sync-common-setup.sh
#

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"

# The source of truth for common-setup.sh
SOURCE_FILE="${REPO_ROOT}/scripts/lib/common-setup.sh"

# Features that use the common-setup helper
# Note: common-utils is excluded because it creates users (different semantics)
FEATURES=(
"anaconda"
"conda"
"desktop-lite"
"docker-in-docker"
"docker-outside-of-docker"
"go"
"hugo"
"java"
"kubectl-helm-minikube"
"node"
"oryx"
"php"
"python"
"ruby"
"rust"
"sshd"
)

echo "Syncing common-setup.sh to all features..."
echo "Source: ${SOURCE_FILE}"
echo ""

if [ ! -f "${SOURCE_FILE}" ]; then
echo "Error: Source file not found: ${SOURCE_FILE}"
exit 1
fi

UPDATED_COUNT=0

for feature in "${FEATURES[@]}"; do
TARGET_DIR="${REPO_ROOT}/src/${feature}/_lib"
TARGET_FILE="${TARGET_DIR}/common-setup.sh"

# Create _lib directory if it doesn't exist
mkdir -p "${TARGET_DIR}"

# Copy the file
cp "${SOURCE_FILE}" "${TARGET_FILE}"

echo "βœ“ Synced to src/${feature}/_lib/common-setup.sh"
UPDATED_COUNT=$((UPDATED_COUNT + 1))
done

echo ""
echo "======================================"
echo "Sync complete!"
echo "Updated ${UPDATED_COUNT} features"
echo "======================================"
echo ""
echo "Note: After running this script, commit the changes:"
echo " git add src/*/lib/common-setup.sh"
echo " git commit -m 'Sync common-setup.sh to all features'"
Loading
Loading