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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ Install Ansible, then run these commands from the cloned folder so `ansible.cfg`
export WORKSPACE_ROOT=/path/to/parent/directory/
```

### 2. Review the targets and defaults in `group_vars/all.yml`, then preview the changes:
### 2. Create targets and defaults in `group_vars/all.yml`, then preview the changes:

```bash
make check
```

or: `ansible-playbook --check --diff playbook.yml`

#### Note on Merge Behavior

- dict/map keys are recursively merged with `devcontainer_defaults`. This includes `features`, `container_env`, `remote_env`, `vscode_settings`.

- list keys are appended with duplicate replacement/removal. This includes `vscode_extensions`, `mounts`, `run_args`, `forwardPorts`.

- Scalar/string/bool keys are replaced by the per-container value, if it exists. This includes `image`, `post_start_command`, `install`, `name`...

### 3. When the diff looks right, apply it with:

```bash
Expand Down
18 changes: 8 additions & 10 deletions group_vars/all.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ devcontainer_sync_create_missing: false

devcontainer_defaults:
image: mcr.microsoft.com/devcontainers/base:jammy
vscode_extensions:
- Anthropic.claude-code
- openai.chatgpt
post_start_command: sudo chown -R vscode:vscode /home/vscode/.claude*
post_create_command: bash .devcontainer/install.sh
post_attach_command: ""
install: ""
container_env:
CLAUDE_HOME: /home/vscode/.claude_project
# note: the following keys get merged. see README.
container_env: {}
vscode_extensions:
- Anthropic.claude-code
- openai.chatgpt
mounts:
- source=claude_config,target=${containerWorkspaceFolder}/.claude,type=volume
- source=claude_config_${localWorkspaceFolderBasename},target=${containerWorkspaceFolder}/.claude,type=volume
- source=claude_config,target=/home/vscode/.claude,type=volume
- source=codex_config,target=/home/vscode/.codex,type=volume
- source=claude_config_${localWorkspaceFolderBasename},target=/home/vscode/.claude_project,type=volume

devcontainers:
- path: java-example/.devcontainer/devcontainer.json
Expand All @@ -35,7 +35,7 @@ devcontainers:
image: mcr.microsoft.com/devcontainers/rust:2-1-trixie
install: |
sudo apt update
sudo -n apt-get install -y --no-install-recommends default-jre-headless
sudo -n apt-get install -y --no-install-recommends default-jre-headless
- path: python-example/.devcontainer/devcontainer.json
image: mcr.microsoft.com/devcontainers/python:3.14
post_attach_command: pip install -r ${containerWorkspaceFolder}/requirements.txt
Expand All @@ -50,8 +50,6 @@ devcontainers:
- aaron-bond.better-comments
- KevinRose.vsc-python-indent
- mikestead.dotenv
- Anthropic.claude-code
- openai.chatgpt
vscode_settings:
python.testing.unittestArgs:
- -v
Expand Down
2 changes: 1 addition & 1 deletion inventory.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
all:
hosts:
localhost:
ansible_connection: local
ansible_connection: local
2 changes: 1 addition & 1 deletion roles/devcontainer_sync/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
devcontainer_sync_create_missing: false
devcontainer_sync_backup: true
devcontainer_sync_backup_existing_dir: true
devcontainer_sync_extra_files_root: "{{ playbook_dir }}/files"
devcontainer_sync_extra_files_root: "{{ playbook_dir }}/files"
33 changes: 28 additions & 5 deletions roles/devcontainer_sync/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,29 @@
loop_control:
label: "{{ item.item.path }}"
vars:
devcontainer: "{{ devcontainer_defaults | combine(item.item, recursive=True) }}"
devcontainer: &merged_devcontainer "{{ devcontainer_defaults | combine(item.item, recursive=True, list_merge='append_rp') }}"
devcontainer_json: &devcontainer_json >-
{{
{
'name': devcontainer.name | default(devcontainer.path | regex_replace('/\\.devcontainer/devcontainer\\.json$', '') | basename),
'image': devcontainer.image,
'customizations': {
'vscode': {'extensions': devcontainer.vscode_extensions}
| combine(({'settings': devcontainer.vscode_settings} if devcontainer.vscode_settings is defined and devcontainer.vscode_settings else {})),
},
'postStartCommand': devcontainer.post_start_command,
'postCreateCommand': devcontainer.post_create_command,
'containerEnv': devcontainer.container_env,
'mounts': devcontainer.mounts,
}
| combine(({'updateRemoteUserUID': devcontainer.update_remote_user_uid} if devcontainer.update_remote_user_uid is defined else {}))
| combine(({'features': devcontainer.features} if devcontainer.features is defined and devcontainer.features else {}))
| combine(({'remoteEnv': devcontainer.remote_env} if devcontainer.remote_env is defined and devcontainer.remote_env else {}))
| combine(({'initializeCommand': devcontainer.initialize_command} if devcontainer.initialize_command is defined and devcontainer.initialize_command else {}))
| combine(({'postAttachCommand': devcontainer.post_attach_command} if devcontainer.post_attach_command is defined and devcontainer.post_attach_command else {}))
| combine(({'runArgs': devcontainer.run_args} if devcontainer.run_args is defined and devcontainer.run_args else {}))
| combine(({'forwardPorts': devcontainer.forwardPorts} if devcontainer.forwardPorts is defined and devcontainer.forwardPorts else {}))
}}
register: devcontainer_sync_devcontainer_render_preview
when: item.stat.exists

Expand All @@ -33,7 +55,7 @@
loop_control:
label: "{{ item.item.path | dirname }}/install.sh"
vars:
devcontainer: "{{ devcontainer_defaults | combine(item.item, recursive=True) }}"
devcontainer: *merged_devcontainer
register: devcontainer_sync_install_render_preview
when: item.stat.exists

Expand Down Expand Up @@ -80,7 +102,8 @@
loop_control:
label: "{{ item.item.path }}"
vars:
devcontainer: "{{ devcontainer_defaults | combine(item.item, recursive=True) }}"
devcontainer: *merged_devcontainer
devcontainer_json: *devcontainer_json
when: item.stat.exists or devcontainer_sync_create_missing | bool

- name: Render install.sh files
Expand All @@ -93,7 +116,7 @@
loop_control:
label: "{{ item.item.path | dirname }}/install.sh"
vars:
devcontainer: "{{ devcontainer_defaults | combine(item.item, recursive=True) }}"
devcontainer: *merged_devcontainer
when: item.stat.exists or devcontainer_sync_create_missing | bool

- name: Copy project extra .devcontainer files
Expand All @@ -110,4 +133,4 @@
loop: "{{ devcontainer_sync_targets.results }}"
loop_control:
label: "{{ item.item.path }}"
when: not item.stat.exists and not devcontainer_sync_create_missing | bool
when: not item.stat.exists and not devcontainer_sync_create_missing | bool
23 changes: 1 addition & 22 deletions roles/devcontainer_sync/templates/devcontainer.json.j2
Original file line number Diff line number Diff line change
@@ -1,22 +1 @@
{
// Managed by devcontainer-sync. Edit group_vars/all.yml instead.
"name": "{{ devcontainer.name | default(devcontainer.path | regex_replace('/\\.devcontainer/devcontainer\\.json$', '') | basename) }}",
"image": "{{ devcontainer.image }}"{% if devcontainer.update_remote_user_uid is defined %},
"updateRemoteUserUID": {{ devcontainer.update_remote_user_uid | to_json }}{% endif %}{% if devcontainer.features is defined and devcontainer.features %},
"features": {{ devcontainer.features | to_nice_json(indent=4) | indent(4) }}{% endif %}{% if devcontainer.remote_env is defined and devcontainer.remote_env %},
"remoteEnv": {{ devcontainer.remote_env | to_nice_json(indent=4) | indent(4) }}{% endif %}{% if devcontainer.initialize_command is defined and devcontainer.initialize_command %},
"initializeCommand": {{ devcontainer.initialize_command | to_json }}{% endif %},
"customizations": {
"vscode": {
"extensions": {{ devcontainer.vscode_extensions | to_nice_json(indent=4) | indent(4) }}{% if devcontainer.vscode_settings is defined and devcontainer.vscode_settings %},
"settings": {{ devcontainer.vscode_settings | to_nice_json(indent=4) | indent(4) }}{% endif %}
}
},
"postStartCommand": "{{ devcontainer.post_start_command }}",
"postCreateCommand": "{{ devcontainer.post_create_command }}"{% if devcontainer.post_attach_command is defined and devcontainer.post_attach_command %},
"postAttachCommand": "{{ devcontainer.post_attach_command }}"{% endif %},
"containerEnv": {{ devcontainer.container_env | to_nice_json(indent=4) | indent(4) }},
"mounts": {{ devcontainer.mounts | to_nice_json(indent=4) | indent(4) }}{% if devcontainer.run_args is defined and devcontainer.run_args %},
"runArgs": {{ devcontainer.run_args | to_nice_json(indent=4) | indent(4) }}{% endif %}{% if devcontainer.forwardPorts is defined and devcontainer.forwardPorts %},
"forwardPorts": {{ devcontainer.forwardPorts | to_nice_json(indent=4) | indent(4) }}{% endif %}
}
{{ devcontainer_json | to_nice_json(indent=4) }}
151 changes: 74 additions & 77 deletions tests/golden_output
Original file line number Diff line number Diff line change
@@ -1,38 +1,58 @@
{
// Managed by devcontainer-sync. Edit group_vars/all.yml instead.
"name": "java-example",
"image": "mcr.microsoft.com/devcontainers/java:3-25-trixie",
"features": {
"containerEnv": {},
"customizations": {
"vscode": {
"extensions": [
"Anthropic.claude-code",
"openai.chatgpt"
]
}
},
"features": {
"ghcr.io/devcontainers/features/java:1": {
"installGradle": false,
"installMaven": false,
"version": "none"
}
},
"customizations": {
"vscode": {
"extensions": [
"Anthropic.claude-code",
"openai.chatgpt"
] }
},
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*",
"postCreateCommand": "bash .devcontainer/install.sh",
"containerEnv": {
"CLAUDE_HOME": "/home/vscode/.claude_project"
},
"mounts": [
"source=claude_config,target=${containerWorkspaceFolder}/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume",
"source=claude_config_${localWorkspaceFolderBasename},target=/home/vscode/.claude_project,type=volume"
]}
"image": "mcr.microsoft.com/devcontainers/java:3-25-trixie",
"mounts": [
"source=claude_config_${localWorkspaceFolderBasename},target=${containerWorkspaceFolder}/.claude,type=volume",
"source=claude_config,target=/home/vscode/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume"
],
"name": "devcontainer.json",
"postCreateCommand": "bash .devcontainer/install.sh",
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*"
}
#!/bin/bash
curl -sL -o cfr.jar 'https://www.benf.org/other/cfr/cfr-0.152.jar'
{
// Managed by devcontainer-sync. Edit group_vars/all.yml instead.
"name": "python-example",
"image": "mcr.microsoft.com/devcontainers/python:3.14",
"features": {
"containerEnv": {},
"customizations": {
"vscode": {
"extensions": [
"Anthropic.claude-code",
"openai.chatgpt",
"aaron-bond.better-comments",
"KevinRose.vsc-python-indent",
"mikestead.dotenv"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/python/current/bin/python",
"python.testing.pytestEnabled": false,
"python.testing.unittestArgs": [
"-v",
"-s",
".",
"-p",
"*_test.py"
],
"python.testing.unittestEnabled": true
}
}
},
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {
"version": "latest"
},
Expand All @@ -42,63 +62,40 @@ curl -sL -o cfr.jar 'https://www.benf.org/other/cfr/cfr-0.152.jar'
},
"ghcr.io/larsnieuwenhuizen/features/jqyq:0": {}
},
"customizations": {
"vscode": {
"extensions": [
"aaron-bond.better-comments",
"KevinRose.vsc-python-indent",
"mikestead.dotenv",
"Anthropic.claude-code",
"openai.chatgpt"
"image": "mcr.microsoft.com/devcontainers/python:3.14",
"mounts": [
"source=claude_config_${localWorkspaceFolderBasename},target=${containerWorkspaceFolder}/.claude,type=volume",
"source=claude_config,target=/home/vscode/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume"
],
"settings": {
"python.defaultInterpreterPath": "/usr/local/python/current/bin/python",
"python.testing.pytestEnabled": false,
"python.testing.unittestArgs": [
"-v",
"-s",
".",
"-p",
"*_test.py"
],
"python.testing.unittestEnabled": true
} }
},
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*",
"postCreateCommand": "bash .devcontainer/install.sh",
"postAttachCommand": "pip install -r ${containerWorkspaceFolder}/requirements.txt",
"containerEnv": {
"CLAUDE_HOME": "/home/vscode/.claude_project"
},
"mounts": [
"source=claude_config,target=${containerWorkspaceFolder}/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume",
"source=claude_config_${localWorkspaceFolderBasename},target=/home/vscode/.claude_project,type=volume"
]}
"name": "devcontainer.json",
"postAttachCommand": "pip install -r ${containerWorkspaceFolder}/requirements.txt",
"postCreateCommand": "bash .devcontainer/install.sh",
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*"
}
#!/bin/bash
sudo apt update
sudo apt install -y --no-install-recommends ansible
{
// Managed by devcontainer-sync. Edit group_vars/all.yml instead.
"name": "rust-example",
"image": "mcr.microsoft.com/devcontainers/rust:2-1-trixie",
"customizations": {
"vscode": {
"extensions": [
"Anthropic.claude-code",
"openai.chatgpt"
] }
},
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*",
"postCreateCommand": "bash .devcontainer/install.sh",
"containerEnv": {
"CLAUDE_HOME": "/home/vscode/.claude_project"
"containerEnv": {},
"customizations": {
"vscode": {
"extensions": [
"Anthropic.claude-code",
"openai.chatgpt"
]
}
},
"mounts": [
"source=claude_config,target=${containerWorkspaceFolder}/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume",
"source=claude_config_${localWorkspaceFolderBasename},target=/home/vscode/.claude_project,type=volume"
]}
"image": "mcr.microsoft.com/devcontainers/rust:2-1-trixie",
"mounts": [
"source=claude_config_${localWorkspaceFolderBasename},target=${containerWorkspaceFolder}/.claude,type=volume",
"source=claude_config,target=/home/vscode/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume"
],
"name": "devcontainer.json",
"postCreateCommand": "bash .devcontainer/install.sh",
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*"
}
#!/bin/bash
sudo apt update
sudo -n apt-get install -y --no-install-recommends default-jre-headless
sudo -n apt-get install -y --no-install-recommends default-jre-headless
41 changes: 20 additions & 21 deletions tests/java-example/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
{
// Managed by devcontainer-sync. Edit group_vars/all.yml instead.
"name": "java-example",
"image": "mcr.microsoft.com/devcontainers/java:3-25-trixie",
"features": {
"containerEnv": {},
"customizations": {
"vscode": {
"extensions": [
"Anthropic.claude-code",
"openai.chatgpt"
]
}
},
"features": {
"ghcr.io/devcontainers/features/java:1": {
"installGradle": false,
"installMaven": false,
"version": "none"
}
},
"customizations": {
"vscode": {
"extensions": [
"Anthropic.claude-code",
"openai.chatgpt"
] }
},
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*",
"postCreateCommand": "bash .devcontainer/install.sh",
"containerEnv": {
"CLAUDE_HOME": "/home/vscode/.claude_project"
},
"mounts": [
"source=claude_config,target=${containerWorkspaceFolder}/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume",
"source=claude_config_${localWorkspaceFolderBasename},target=/home/vscode/.claude_project,type=volume"
]}
"image": "mcr.microsoft.com/devcontainers/java:3-25-trixie",
"mounts": [
"source=claude_config_${localWorkspaceFolderBasename},target=${containerWorkspaceFolder}/.claude,type=volume",
"source=claude_config,target=/home/vscode/.claude,type=volume",
"source=codex_config,target=/home/vscode/.codex,type=volume"
],
"name": "devcontainer.json",
"postCreateCommand": "bash .devcontainer/install.sh",
"postStartCommand": "sudo chown -R vscode:vscode /home/vscode/.claude*"
}
Loading