diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1c14ffb..57572c7 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: "false" + generate-docs: "true" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/copilot-persistence/NOTES.md b/src/copilot-persistence/NOTES.md new file mode 100644 index 0000000..0b03dde --- /dev/null +++ b/src/copilot-persistence/NOTES.md @@ -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-/` 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 +``` diff --git a/src/copilot-persistence/README.md b/src/copilot-persistence/README.md index 5347400..b17daed 100644 --- a/src/copilot-persistence/README.md +++ b/src/copilot-persistence/README.md @@ -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 @@ -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: diff --git a/src/copilot-persistence/install.sh b/src/copilot-persistence/install.sh index 90699f7..4b81854 100755 --- a/src/copilot-persistence/install.sh +++ b/src/copilot-persistence/install.sh @@ -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 - 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" +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" diff --git a/test/copilot-persistence/debian.sh b/test/copilot-persistence/debian.sh index 53b5836..05da458 100644 --- a/test/copilot-persistence/debian.sh +++ b/test/copilot-persistence/debian.sh @@ -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 @@ -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 diff --git a/test/copilot-persistence/test.sh b/test/copilot-persistence/test.sh index cce6391..673a95c 100644 --- a/test/copilot-persistence/test.sh +++ b/test/copilot-persistence/test.sh @@ -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 @@ -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 diff --git a/test/copilot-persistence/ubuntu.sh b/test/copilot-persistence/ubuntu.sh index 703ab97..eb9e431 100644 --- a/test/copilot-persistence/ubuntu.sh +++ b/test/copilot-persistence/ubuntu.sh @@ -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 @@ -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