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: "true"
generate-docs: "false"
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Disabling generate-docs changes the release workflow behavior for all features and will stop updating the existing auto-generated feature READMEs (which currently indicate they’re generated from devcontainer-feature.json). If the goal is to keep a custom README for this feature, consider keeping generate-docs: "true" and using the devcontainers-supported NOTES.md mechanism for custom content, or otherwise updating the repo’s documentation approach consistently across features.

Suggested change
generate-docs: "false"
generate-docs: "true"

Copilot uses AI. Check for mistakes.

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
- ohmyposh
- microsoft-security-devops-cli
- prompty-dumpty
- copilot-persistence
baseImage:
- debian:latest
- ubuntu:latest
Expand All @@ -38,6 +39,7 @@ jobs:
- ohmyposh
- microsoft-security-devops-cli
- prompty-dumpty
- copilot-persistence
steps:
- uses: actions/checkout@v4

Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,32 @@ guardian init --force

Note: `guardian init` requires a git repository, so it must be run manually after the container starts (not during feature installation).

### Copilot CLI Persistence

Persists GitHub Copilot CLI settings and chat history across container rebuilds using a named Docker volume.

**Usage:**

```json
{
"features": {
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The README usage example enables only copilot-persistence, but the text describes persisting GitHub Copilot CLI state. Since this feature is intended to be used alongside the Copilot CLI feature, update the example to include ghcr.io/devcontainers/features/copilot-cli:1 (or explicitly document that Copilot CLI must be installed separately).

Suggested change
"features": {
"features": {
"ghcr.io/devcontainers/features/copilot-cli:1": {},

Copilot uses AI. Check for mistakes.
"ghcr.io/devcontainers/features/copilot-cli:1": {},
"ghcr.io/rosstaco/devcontainer-features/copilot-persistence:1": {}
}
}
```

**How It Works:**
- Mounts a named volume (scoped per dev container) to `/copilot-data`
- Creates a symlink from `~/.copilot` → `/copilot-data`
- Sets the `COPILOT_DATA_DIR` environment variable to `/copilot-data`

**What Persists:**
- Chat history and sessions
- CLI configuration (model preferences, settings)
- Command history
- Trusted folders

## Publishing

This repository uses a **GitHub Action** [workflow](.github/workflows/release.yaml) that publishes each Feature to GHCR (GitHub Container Registry).
Expand Down
49 changes: 49 additions & 0 deletions src/copilot-persistence/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Copilot CLI Persistence Feature

This devcontainer feature persists GitHub Copilot CLI settings and chat history across container rebuilds.

## 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`)

Comment on lines +9 to +12
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The README doesn’t mention that installation replaces ~/.copilot with a symlink (and may remove an existing directory). It would be helpful to document this explicitly (including any migration/backup behavior) so users aren’t surprised by potential data loss when enabling the feature.

Copilot uses AI. Check for mistakes.

## What Persists

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

## Usage

```json
{
"features": {
"ghcr.io/devcontainers/features/copilot-cli:1": {},
"ghcr.io/rosstaco/devcontainer-features/copilot-persistence:1": {}
}
}
```

## 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:
```bash
ls -la /copilot-data
```

Check the symlink:
```bash
ls -la ~/.copilot
```
20 changes: 20 additions & 0 deletions src/copilot-persistence/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"id": "copilot-persistence",
"version": "1.0.0",
"name": "Copilot CLI Persistence",
"documentationURL": "https://github.com/rosstaco/devcontainer-features/tree/main/src/copilot-persistence",
"description": "Persists GitHub Copilot CLI settings and chat history across rebuilds",
"mounts": [
Comment on lines +1 to +7
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

All existing features in this repo include a documentationURL in devcontainer-feature.json, but this new feature does not. Add documentationURL pointing at src/copilot-persistence to match the repository convention and to improve discoverability in registries/tools.

Copilot uses AI. Check for mistakes.
{
"source": "${devcontainerId}-copilot-data",
"target": "/copilot-data",
"type": "volume"
}
],
Comment on lines +7 to +13
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The volume name is hard-coded to copilot-cli-data, which means containers from unrelated projects on the same Docker host will share the same Copilot data by default. Consider adding a feature option to override the volume name (defaulting to the current value) so users can avoid cross-project data mixing when desired.

Copilot uses AI. Check for mistakes.
"installsAfter": [
"ghcr.io/devcontainers/features/copilot-cli"
],
"containerEnv": {
"COPILOT_DATA_DIR": "/copilot-data"
}
}
64 changes: 64 additions & 0 deletions src/copilot-persistence/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/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
if [ -z "${USER_HOME}" ]; then
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
exit 1
fi

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

USER_HOME fallback via eval echo "~${USERNAME}" can succeed even when the user doesn’t exist (it returns the literal string like ~someuser), so USER_HOME becomes a non-absolute path and the script may create /~someuser or similar and link config into the wrong place. Consider validating that the resolved home is an absolute existing directory (or that getent passwd/id succeeds for the user) and failing fast if the user cannot be resolved.

Suggested change
# Validate that USER_HOME is an absolute path to avoid creating paths like /~someuser
case "${USER_HOME}" in
/*)
# OK: absolute path
;;
*)
echo "Error: Resolved home directory '${USER_HOME}' for user '${USERNAME}' is not an absolute path" >&2
exit 1
;;
esac

Copilot uses AI. Check for mistakes.
# 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
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
Comment on lines +38 to +42
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The generated init script runs sudo chown -R ... unconditionally and suppresses all errors. On images without sudo or where the user doesn’t have passwordless sudo, this will silently fail (or can block if sudo prompts), leaving /copilot-data non-writable and breaking the feature. Consider handling the cases explicitly: if running as root use chown directly; otherwise only run sudo if it’s present and non-interactive (e.g., sudo -n), and emit a clear warning/error when ownership cannot be fixed.

Copilot uses AI. Check for mistakes.
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

# Create a symlink from the default location to our persistent volume
# This handles the case where Copilot doesn't use XDG_CONFIG_HOME correctly
mkdir -p "${USER_HOME}"
if [ ! -L "${USER_HOME}/.copilot" ] || [ "$(readlink "${USER_HOME}/.copilot")" != "/copilot-data" ]; then
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Replacing ~/.copilot via rm -rf can delete any pre-existing Copilot configuration/history that might already be present in the image or created earlier in the build. Consider migrating existing contents into /copilot-data (or at minimum backing them up and/or gating the removal behind an explicit check) before swapping to the symlink.

Suggested change
if [ ! -L "${USER_HOME}/.copilot" ] || [ "$(readlink "${USER_HOME}/.copilot")" != "/copilot-data" ]; then
if [ ! -L "${USER_HOME}/.copilot" ] || [ "$(readlink "${USER_HOME}/.copilot")" != "/copilot-data" ]; then
# If there is pre-existing Copilot data, back it up into /copilot-data before replacing it
if [ -e "${USER_HOME}/.copilot" ] && [ ! -L "${USER_HOME}/.copilot" ]; then
BACKUP_DIR="/copilot-data/preexisting-copilot-$(date +%s)"
mkdir -p "${BACKUP_DIR}" 2>/dev/null || true
# Copy contents rather than move to avoid failures across filesystems
cp -a "${USER_HOME}/.copilot/." "${BACKUP_DIR}/" 2>/dev/null || true
fi

Copilot uses AI. Check for mistakes.
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"
fi

echo "Copilot CLI persistence configured successfully for user: ${USERNAME}"
echo "Data will be stored in /copilot-data (mounted volume)"
23 changes: 23 additions & 0 deletions test/copilot-persistence/debian.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

# Scenario test: Debian base image
# Validates copilot-persistence works on Debian

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

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

check "COPILOT_DATA_DIR is set" test "$COPILOT_DATA_DIR" = "/copilot-data"

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"

reportResults
14 changes: 14 additions & 0 deletions test/copilot-persistence/scenarios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"debian": {
"image": "mcr.microsoft.com/devcontainers/base:debian",
"features": {
"copilot-persistence": {}
}
},
"ubuntu": {
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"copilot-persistence": {}
}
}
}
46 changes: 46 additions & 0 deletions test/copilot-persistence/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash

# This test file will be executed against an auto-generated devcontainer.json that
# includes the 'copilot-persistence' Feature with no options.
#
# Eg:
# {
# "image": "<..some-base-image...>",
# "features": {
# "copilot-persistence": {}
# }
# }
#
# Thus, the value of all options will fall back to the default value in the
# Feature's 'devcontainer-feature.json'.

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

# 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

check "COPILOT_DATA_DIR env var is set" test -n "$COPILOT_DATA_DIR"

check "COPILOT_DATA_DIR points to /copilot-data" test "$COPILOT_DATA_DIR" = "/copilot-data"

check "symlink exists at ~/.copilot" test -L ~/.copilot

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

# Report results
reportResults
23 changes: 23 additions & 0 deletions test/copilot-persistence/ubuntu.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

# Scenario test: Ubuntu base image
# Validates copilot-persistence works on Ubuntu

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

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

check "COPILOT_DATA_DIR is set" test "$COPILOT_DATA_DIR" = "/copilot-data"

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"

reportResults