-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add copilot-persistence feature #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ebc7d20
c087356
bd153dd
50e161f
cb0c011
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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": { | ||||||||
|
||||||||
| "features": { | |
| "features": { | |
| "ghcr.io/devcontainers/features/copilot-cli:1": {}, |
| 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
|
||
|
|
||
| ## 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 | ||
| ``` | ||
| 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
|
||
| { | ||
| "source": "${devcontainerId}-copilot-data", | ||
| "target": "/copilot-data", | ||
| "type": "volume" | ||
| } | ||
| ], | ||
|
Comment on lines
+7
to
+13
|
||
| "installsAfter": [ | ||
| "ghcr.io/devcontainers/features/copilot-cli" | ||
| ], | ||
| "containerEnv": { | ||
| "COPILOT_DATA_DIR": "/copilot-data" | ||
| } | ||
| } | ||
| 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 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| # 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
AI
Feb 12, 2026
There was a problem hiding this comment.
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
AI
Feb 12, 2026
There was a problem hiding this comment.
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.
| 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 |
| 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 |
| 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": {} | ||
| } | ||
| } | ||
| } |
| 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 |
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disabling
generate-docschanges the release workflow behavior for all features and will stop updating the existing auto-generated feature READMEs (which currently indicate they’re generated fromdevcontainer-feature.json). If the goal is to keep a custom README for this feature, consider keepinggenerate-docs: "true"and using the devcontainers-supportedNOTES.mdmechanism for custom content, or otherwise updating the repo’s documentation approach consistently across features.