Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
with:
publish-features: "true"
base-path-to-features: "./src"
generate-docs: "false"
generate-docs: "true"

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
28 changes: 28 additions & 0 deletions src/copilot-persistence/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## How It Works

Inspired by the [shell-history pattern](https://github.com/stuartleeks/dev-container-features/tree/main/src/shell-history), this feature:

1. **Mounts a named volume** (scoped per dev container via `${devcontainerId}`) to `/copilot-data`
2. **Creates a symlink** from `~/.copilot` → `/copilot-data`
3. **Sets ownership** to the container user during installation (auto-detects from `$_REMOTE_USER`)

> **Note:** If `~/.copilot` already exists as a directory during installation, it is moved into the volume at `/copilot-data/migrated-<timestamp>/` before the symlink is created.

## What Persists

- ✅ Chat history and sessions
- ✅ CLI configuration (model preferences, settings)
- ✅ Command history
- ✅ Trusted folders

## Troubleshooting

View the volume data:
```bash
ls -la /copilot-data
```

Check the symlink:
```bash
ls -la ~/.copilot
```
8 changes: 0 additions & 8 deletions src/copilot-persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Inspired by the [shell-history pattern](https://github.com/stuartleeks/dev-conta
2. **Creates a symlink** from `~/.copilot` → `/copilot-data`
3. **Sets ownership** to the container user during installation (auto-detects from `$_REMOTE_USER`)


## What Persists

- ✅ Chat history and sessions
Expand All @@ -29,13 +28,6 @@ Inspired by the [shell-history pattern](https://github.com/stuartleeks/dev-conta
}
```

## Benefits Over Direct Mount

- No permission conflicts (volume created in neutral location)
- Works even if Copilot CLI has XDG_CONFIG_HOME bugs
- Clean lifecycle management during feature install
- Easy to share across projects

## Troubleshooting

View the volume data:
Expand Down
63 changes: 15 additions & 48 deletions src/copilot-persistence/install.sh
Original file line number Diff line number Diff line change
@@ -1,64 +1,31 @@
#!/bin/bash
set -e

echo "Setting up Copilot CLI persistence..."

# Determine the user (defaults to vscode if not set)
USERNAME="${_REMOTE_USER:-"${USERNAME:-"vscode"}"}"

# Resolve home directory via the passwd database or shell expansion
USER_HOME=""
if command -v getent >/dev/null 2>&1; then
USER_HOME="$(getent passwd "${USERNAME}" | cut -d: -f6)"
fi
USER_HOME="${_REMOTE_USER_HOME:-"$(getent passwd "${USERNAME}" 2>/dev/null | cut -d: -f6)"}"
if [ -z "${USER_HOME}" ]; then
Comment on lines +5 to 6
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

USER_HOME resolution now unconditionally invokes getent inside a command substitution while set -e is enabled. On images where getent is missing (or returns non-zero), this can cause the installer to exit before emitting the fallback error message. Consider guarding with command -v getent and/or reintroducing the ~${USERNAME} fallback used elsewhere in this repo (e.g., src/ohmyposh/install.sh).

Suggested change
USER_HOME="${_REMOTE_USER_HOME:-"$(getent passwd "${USERNAME}" 2>/dev/null | cut -d: -f6)"}"
if [ -z "${USER_HOME}" ]; then
USER_HOME="${_REMOTE_USER_HOME:-}"
if [ -z "${USER_HOME}" ]; then
if command -v getent >/dev/null 2>&1; then
USER_HOME="$(getent passwd "${USERNAME}" 2>/dev/null | cut -d: -f6 || true)"
fi
fi
if [ -z "${USER_HOME}" ]; then
USER_HOME="$(eval echo "~${USERNAME}")"
fi
if [ -z "${USER_HOME}" ]; then

Copilot uses AI. Check for mistakes.
USER_HOME="$(eval echo "~${USERNAME}" 2>/dev/null || true)"
fi
if [ -z "${USER_HOME}" ]; then
echo "Error: Unable to determine home directory for user '${USERNAME}'" >&2
echo "ERROR: Could not determine home directory for user '${USERNAME}'" >&2
exit 1
fi

# Create the persistent data directory in a neutral location
mkdir -p /copilot-data

# Get the user's UID and GID
USER_UID=$(id -u "${USERNAME}" 2>/dev/null || echo "1000")
USER_GID=$(id -g "${USERNAME}" 2>/dev/null || echo "1000")

# Fix ownership for the user (build-time, may be overridden by volume mount)
chown -R "${USER_UID}:${USER_GID}" /copilot-data
# Prepare the persistent data directory (permissions carry into named volume on first use)
mkdir -p /copilot-data
chown "${USER_UID}:${USER_GID}" /copilot-data
chmod 700 /copilot-data

# Create an init script that fixes volume permissions at container start.
# When a named volume is mounted, build-time permissions may not carry over,
# so we fix ownership on every shell login.
mkdir -p /usr/local/share/copilot-persistence
cat > /usr/local/share/copilot-persistence/init.sh << 'INIT'
#!/bin/bash
# Fix /copilot-data ownership if it exists and is not writable by current user
if [ -d /copilot-data ] && [ ! -w /copilot-data ]; then
sudo chown -R "$(id -u):$(id -g)" /copilot-data 2>/dev/null || true
fi
INIT
chmod 755 /usr/local/share/copilot-persistence/init.sh

# Source the init script from profile so it runs on container start
cat > /etc/profile.d/copilot-persistence.sh << 'PROFILE'
# Fix copilot-data volume permissions on login
if [ -f /usr/local/share/copilot-persistence/init.sh ]; then
. /usr/local/share/copilot-persistence/init.sh
fi
PROFILE
# Fix volume permissions at login (build-time ownership may not carry into mounted volumes)
cat > /etc/profile.d/copilot-persistence.sh << 'EOF'
[ -d /copilot-data ] && [ ! -w /copilot-data ] && sudo -n chown -R "$(id -u):$(id -g)" /copilot-data 2>/dev/null || true
EOF

# Create a symlink from the default location to our persistent volume
# This handles the case where Copilot doesn't use XDG_CONFIG_HOME correctly
# Migrate any pre-existing Copilot data into the volume, then symlink
mkdir -p "${USER_HOME}"
if [ ! -L "${USER_HOME}/.copilot" ] || [ "$(readlink "${USER_HOME}/.copilot")" != "/copilot-data" ]; then
rm -rf "${USER_HOME}/.copilot" 2>/dev/null || true
ln -sf /copilot-data "${USER_HOME}/.copilot"
chown -h "${USER_UID}:${USER_GID}" "${USER_HOME}/.copilot"
if [ -e "${USER_HOME}/.copilot" ] && [ ! -L "${USER_HOME}/.copilot" ]; then
mv "${USER_HOME}/.copilot" "/copilot-data/migrated-$(date +%s%N)-$$"
fi
ln -sfn /copilot-data "${USER_HOME}/.copilot"
Comment on lines +23 to +28
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The migration logic runs during feature installation, but /copilot-data is declared as a runtime named-volume mount in devcontainer-feature.json. If the volume mount is applied after install, anything moved into /copilot-data/... here may end up in the image layer and be hidden once the volume mounts at container start, effectively losing the migrated data. To make migration reliable, perform it in a runtime hook (e.g., the /etc/profile.d/copilot-persistence.sh script) after confirming the volume is mounted, or in a container-start script executed after mounts are applied.

Copilot uses AI. Check for mistakes.
chown -h "${USER_UID}:${USER_GID}" "${USER_HOME}/.copilot"

echo "Copilot CLI persistence configured successfully for user: ${USERNAME}"
echo "Data will be stored in /copilot-data (mounted volume)"
echo "Copilot persistence: ~/.copilot → /copilot-data"
9 changes: 5 additions & 4 deletions test/copilot-persistence/debian.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ set -e

source dev-container-features-test-lib

# Run the init script to fix volume permissions (normally runs via /etc/profile.d on login)
if [ -f /usr/local/share/copilot-persistence/init.sh ]; then
. /usr/local/share/copilot-persistence/init.sh
fi
# Verify profile script was created and source it to fix volume permissions
check "copilot-persistence profile script exists" test -f /etc/profile.d/copilot-persistence.sh
. /etc/profile.d/copilot-persistence.sh

check "copilot-data directory exists" test -d /copilot-data

Expand All @@ -20,4 +19,6 @@ check "symlink exists at ~/.copilot" test -L ~/.copilot

check "can write to copilot-data" bash -c "touch /copilot-data/test-file && rm /copilot-data/test-file"

check "copilot-data has restricted permissions" bash -c 'test "$(stat -c %a /copilot-data)" = "700"'

reportResults
30 changes: 22 additions & 8 deletions test/copilot-persistence/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,12 @@ set -e
# Optional: Import test library bundled with the devcontainer CLI
source dev-container-features-test-lib

# Run the init script to fix volume permissions (normally runs via /etc/profile.d on login)
if [ -f /usr/local/share/copilot-persistence/init.sh ]; then
. /usr/local/share/copilot-persistence/init.sh
fi
# Verify profile script was created and source it to fix volume permissions
check "copilot-persistence profile script exists" test -f /etc/profile.d/copilot-persistence.sh
. /etc/profile.d/copilot-persistence.sh

# Feature-specific tests

check "init script exists" test -f /usr/local/share/copilot-persistence/init.sh

check "profile.d script exists" test -f /etc/profile.d/copilot-persistence.sh

check "copilot-data directory exists" test -d /copilot-data

check "copilot-data directory is writable" test -w /copilot-data
Expand All @@ -42,5 +37,24 @@ check "symlink exists at ~/.copilot" test -L ~/.copilot

check "symlink target is /copilot-data" test "$(readlink ~/.copilot)" = "/copilot-data"

check "copilot-data has restricted permissions" bash -c 'test "$(stat -c %a /copilot-data)" = "700"'

check "data written to volume is accessible via symlink" bash -c 'echo "test" > /copilot-data/test-persist && test "$(cat ~/.copilot/test-persist)" = "test" && rm /copilot-data/test-persist'

# Test migration: simulate pre-existing .copilot directory and verify mv behavior
check "migration preserves pre-existing data" bash -c '
rm -f ~/.copilot
mkdir -p ~/.copilot
echo "precious-data" > ~/.copilot/history.json
if [ -e ~/.copilot ] && [ ! -L ~/.copilot ]; then
mv ~/.copilot "/copilot-data/migrated-test"
fi
ln -sfn /copilot-data ~/.copilot
test -f /copilot-data/migrated-test/history.json &&
test "$(cat /copilot-data/migrated-test/history.json)" = "precious-data" &&
test -L ~/.copilot &&
rm -rf /copilot-data/migrated-test
'

# Report results
reportResults
9 changes: 5 additions & 4 deletions test/copilot-persistence/ubuntu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ set -e

source dev-container-features-test-lib

# Run the init script to fix volume permissions (normally runs via /etc/profile.d on login)
if [ -f /usr/local/share/copilot-persistence/init.sh ]; then
. /usr/local/share/copilot-persistence/init.sh
fi
# Verify profile script was created and source it to fix volume permissions
check "copilot-persistence profile script exists" test -f /etc/profile.d/copilot-persistence.sh
. /etc/profile.d/copilot-persistence.sh

check "copilot-data directory exists" test -d /copilot-data

Expand All @@ -20,4 +19,6 @@ check "symlink exists at ~/.copilot" test -L ~/.copilot

check "can write to copilot-data" bash -c "touch /copilot-data/test-file && rm /copilot-data/test-file"

check "copilot-data has restricted permissions" bash -c 'test "$(stat -c %a /copilot-data)" = "700"'

reportResults