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/.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 diff --git a/README.md b/README.md index a4cd49d..95b0ec7 100644 --- a/README.md +++ b/README.md @@ -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": { + "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). diff --git a/src/copilot-persistence/README.md b/src/copilot-persistence/README.md new file mode 100644 index 0000000..5347400 --- /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** (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`) + + +## 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..dc4de51 --- /dev/null +++ b/src/copilot-persistence/devcontainer-feature.json @@ -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": [ + { + "source": "${devcontainerId}-copilot-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..90699f7 --- /dev/null +++ b/src/copilot-persistence/install.sh @@ -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 + +# 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 +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 + 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..53b5836 --- /dev/null +++ b/test/copilot-persistence/debian.sh @@ -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 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..cce6391 --- /dev/null +++ b/test/copilot-persistence/test.sh @@ -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 diff --git a/test/copilot-persistence/ubuntu.sh b/test/copilot-persistence/ubuntu.sh new file mode 100644 index 0000000..703ab97 --- /dev/null +++ b/test/copilot-persistence/ubuntu.sh @@ -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