From ebc7d202fed2f39cf6a632e56dcba57488ceca12 Mon Sep 17 00:00:00 2001 From: rosstaco Date: Thu, 12 Feb 2026 07:22:28 +0000 Subject: [PATCH 1/5] feat: add copilot-persistence feature Persists GitHub Copilot CLI settings and chat history across container rebuilds using a named Docker volume mounted at /copilot-data with a symlink from ~/.copilot. - Add install script with root/non-root user handling - Add devcontainer-feature.json with volume mount and env var - Add tests (default, debian, ubuntu scenarios) - Update root README with feature documentation - Disable generate-docs to preserve custom feature READMEs --- .github/workflows/release.yaml | 2 +- README.md | 25 ++++++++++ src/copilot-persistence/README.md | 49 +++++++++++++++++++ .../devcontainer-feature.json | 19 +++++++ src/copilot-persistence/install.sh | 37 ++++++++++++++ test/copilot-persistence/debian.sh | 18 +++++++ test/copilot-persistence/scenarios.json | 14 ++++++ test/copilot-persistence/test.sh | 37 ++++++++++++++ test/copilot-persistence/ubuntu.sh | 18 +++++++ 9 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/copilot-persistence/README.md create mode 100644 src/copilot-persistence/devcontainer-feature.json create mode 100755 src/copilot-persistence/install.sh create mode 100644 test/copilot-persistence/debian.sh create mode 100644 test/copilot-persistence/scenarios.json create mode 100644 test/copilot-persistence/test.sh create mode 100644 test/copilot-persistence/ubuntu.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 57572c7..1c14ffb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,7 +18,7 @@ jobs: with: publish-features: "true" base-path-to-features: "./src" - generate-docs: "true" + generate-docs: "false" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index a4cd49d..01a8b2c 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,31 @@ 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": { + "ghcr.io/rosstaco/devcontainer-features/copilot-persistence:1": {} + } +} +``` + +**How It Works:** +- Mounts a named volume (`copilot-cli-data`) 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). diff --git a/src/copilot-persistence/README.md b/src/copilot-persistence/README.md new file mode 100644 index 0000000..de7147d --- /dev/null +++ b/src/copilot-persistence/README.md @@ -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** to `/copilot-data` (neutral location, no permission issues) +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 +- ✅ 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 +``` diff --git a/src/copilot-persistence/devcontainer-feature.json b/src/copilot-persistence/devcontainer-feature.json new file mode 100644 index 0000000..b824711 --- /dev/null +++ b/src/copilot-persistence/devcontainer-feature.json @@ -0,0 +1,19 @@ +{ + "id": "copilot-persistence", + "version": "1.0.0", + "name": "Copilot CLI Persistence", + "description": "Persists GitHub Copilot CLI settings and chat history across rebuilds", + "mounts": [ + { + "source": "copilot-cli-data", + "target": "/copilot-data", + "type": "volume" + } + ], + "installsAfter": [ + "ghcr.io/devcontainers/features/copilot-cli" + ], + "containerEnv": { + "COPILOT_DATA_DIR": "/copilot-data" + } +} diff --git a/src/copilot-persistence/install.sh b/src/copilot-persistence/install.sh new file mode 100755 index 0000000..6450de3 --- /dev/null +++ b/src/copilot-persistence/install.sh @@ -0,0 +1,37 @@ +#!/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 properly (root uses /root, others use /home/) +if [ "$USERNAME" = "root" ]; then + USER_HOME="/root" +else + USER_HOME="/home/${USERNAME}" +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 +chown -R "${USER_UID}:${USER_GID}" /copilot-data +chmod 755 /copilot-data + +# 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" ]; 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" +fi + +echo "Copilot CLI persistence configured successfully for user: ${USERNAME}" +echo "Data will be stored in /copilot-data (mounted volume)" diff --git a/test/copilot-persistence/debian.sh b/test/copilot-persistence/debian.sh new file mode 100644 index 0000000..b1045b0 --- /dev/null +++ b/test/copilot-persistence/debian.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Scenario test: Debian base image +# Validates copilot-persistence works on Debian + +set -e + +source dev-container-features-test-lib + +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 diff --git a/test/copilot-persistence/scenarios.json b/test/copilot-persistence/scenarios.json new file mode 100644 index 0000000..0d9e9eb --- /dev/null +++ b/test/copilot-persistence/scenarios.json @@ -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": {} + } + } +} diff --git a/test/copilot-persistence/test.sh b/test/copilot-persistence/test.sh new file mode 100644 index 0000000..f190645 --- /dev/null +++ b/test/copilot-persistence/test.sh @@ -0,0 +1,37 @@ +#!/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 + +# Feature-specific tests + +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 diff --git a/test/copilot-persistence/ubuntu.sh b/test/copilot-persistence/ubuntu.sh new file mode 100644 index 0000000..37dc426 --- /dev/null +++ b/test/copilot-persistence/ubuntu.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Scenario test: Ubuntu base image +# Validates copilot-persistence works on Ubuntu + +set -e + +source dev-container-features-test-lib + +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 From c087356acc8588e2986cd127995e41be4d1c107c Mon Sep 17 00:00:00 2001 From: rosstaco Date: Thu, 12 Feb 2026 07:26:10 +0000 Subject: [PATCH 2/5] ci: add copilot-persistence to test matrix --- .github/workflows/test.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3921583..f5896a5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -16,6 +16,7 @@ jobs: - ohmyposh - microsoft-security-devops-cli - prompty-dumpty + - copilot-persistence baseImage: - debian:latest - ubuntu:latest @@ -38,6 +39,7 @@ jobs: - ohmyposh - microsoft-security-devops-cli - prompty-dumpty + - copilot-persistence steps: - uses: actions/checkout@v4 From bd153dd7ab3fe17c57fab2c6ab1254787ce0ad4a Mon Sep 17 00:00:00 2001 From: rosstaco Date: Thu, 12 Feb 2026 07:30:45 +0000 Subject: [PATCH 3/5] fix: use profile.d init script to fix volume permissions at runtime Instead of chmod 777, add a /etc/profile.d script that fixes ownership of /copilot-data on login when the volume mount overrides build-time permissions. --- src/copilot-persistence/install.sh | 23 ++++++++++++++++++++++- test/copilot-persistence/debian.sh | 5 +++++ test/copilot-persistence/test.sh | 9 +++++++++ test/copilot-persistence/ubuntu.sh | 5 +++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/copilot-persistence/install.sh b/src/copilot-persistence/install.sh index 6450de3..e8d4039 100755 --- a/src/copilot-persistence/install.sh +++ b/src/copilot-persistence/install.sh @@ -20,10 +20,31 @@ mkdir -p /copilot-data 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 +# Fix ownership for the user (build-time, may be overridden by volume mount) chown -R "${USER_UID}:${USER_GID}" /copilot-data chmod 755 /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 + # 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}" diff --git a/test/copilot-persistence/debian.sh b/test/copilot-persistence/debian.sh index b1045b0..53b5836 100644 --- a/test/copilot-persistence/debian.sh +++ b/test/copilot-persistence/debian.sh @@ -7,6 +7,11 @@ 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" diff --git a/test/copilot-persistence/test.sh b/test/copilot-persistence/test.sh index f190645..cce6391 100644 --- a/test/copilot-persistence/test.sh +++ b/test/copilot-persistence/test.sh @@ -19,8 +19,17 @@ 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 diff --git a/test/copilot-persistence/ubuntu.sh b/test/copilot-persistence/ubuntu.sh index 37dc426..703ab97 100644 --- a/test/copilot-persistence/ubuntu.sh +++ b/test/copilot-persistence/ubuntu.sh @@ -7,6 +7,11 @@ 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" From 50e161fe4dd643006a5b829022e26acf744d6b1b Mon Sep 17 00:00:00 2001 From: rosstaco Date: Thu, 12 Feb 2026 08:39:53 +0000 Subject: [PATCH 4/5] refactor: address PR review feedback - Resolve home directory via getent/passwd instead of hardcoding /home/ - Use chmod 700 instead of 755 for sensitive copilot data - Fix symlink guard to also verify target path points to /copilot-data - Add documentationURL to devcontainer-feature.json - Include copilot-cli feature in README usage example Skipped configurable volume name - cross-project sharing is the intended behavior for copilot data persistence. --- README.md | 1 + .../devcontainer-feature.json | 1 + src/copilot-persistence/install.sh | 20 ++++++++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 01a8b2c..3f51d48 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Persists GitHub Copilot CLI settings and chat history across container rebuilds ```json { "features": { + "ghcr.io/devcontainers/features/copilot-cli:1": {}, "ghcr.io/rosstaco/devcontainer-features/copilot-persistence:1": {} } } diff --git a/src/copilot-persistence/devcontainer-feature.json b/src/copilot-persistence/devcontainer-feature.json index b824711..65f9ea5 100644 --- a/src/copilot-persistence/devcontainer-feature.json +++ b/src/copilot-persistence/devcontainer-feature.json @@ -2,6 +2,7 @@ "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": [ { diff --git a/src/copilot-persistence/install.sh b/src/copilot-persistence/install.sh index e8d4039..90699f7 100755 --- a/src/copilot-persistence/install.sh +++ b/src/copilot-persistence/install.sh @@ -6,11 +6,17 @@ echo "Setting up Copilot CLI persistence..." # Determine the user (defaults to vscode if not set) USERNAME="${_REMOTE_USER:-"${USERNAME:-"vscode"}"}" -# Resolve home directory properly (root uses /root, others use /home/) -if [ "$USERNAME" = "root" ]; then - USER_HOME="/root" -else - USER_HOME="/home/${USERNAME}" +# 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 # Create the persistent data directory in a neutral location @@ -22,7 +28,7 @@ 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 755 /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, @@ -48,7 +54,7 @@ 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" ]; then +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" From cb0c011c937c051310f293626db28238249f0d78 Mon Sep 17 00:00:00 2001 From: rosstaco Date: Thu, 12 Feb 2026 08:40:58 +0000 Subject: [PATCH 5/5] feat: scope volume per dev container using ${devcontainerId} Matches the shell-history pattern from stuartleeks/dev-container-features. Each dev container gets its own isolated copilot data volume. --- README.md | 2 +- src/copilot-persistence/README.md | 2 +- src/copilot-persistence/devcontainer-feature.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3f51d48..95b0ec7 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Persists GitHub Copilot CLI settings and chat history across container rebuilds ``` **How It Works:** -- Mounts a named volume (`copilot-cli-data`) to `/copilot-data` +- 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` diff --git a/src/copilot-persistence/README.md b/src/copilot-persistence/README.md index de7147d..5347400 100644 --- a/src/copilot-persistence/README.md +++ b/src/copilot-persistence/README.md @@ -6,7 +6,7 @@ This devcontainer feature persists GitHub Copilot CLI settings and chat history 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** to `/copilot-data` (neutral location, no permission issues) +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`) diff --git a/src/copilot-persistence/devcontainer-feature.json b/src/copilot-persistence/devcontainer-feature.json index 65f9ea5..dc4de51 100644 --- a/src/copilot-persistence/devcontainer-feature.json +++ b/src/copilot-persistence/devcontainer-feature.json @@ -6,7 +6,7 @@ "description": "Persists GitHub Copilot CLI settings and chat history across rebuilds", "mounts": [ { - "source": "copilot-cli-data", + "source": "${devcontainerId}-copilot-data", "target": "/copilot-data", "type": "volume" }