From 927c4b5d6e11af207c9262ba27adf3aa9c991f3a Mon Sep 17 00:00:00 2001 From: S Smith Date: Mon, 1 Jun 2026 21:18:33 +0000 Subject: [PATCH] [minor] Add field-merge, improve formatting --- README.md | 10 +- group_vars/all.example.yml | 18 +-- inventory.yml | 2 +- roles/devcontainer_sync/defaults/main.yml | 2 +- roles/devcontainer_sync/tasks/main.yml | 33 +++- .../templates/devcontainer.json.j2 | 23 +-- tests/golden_output | 151 +++++++++--------- .../.devcontainer/devcontainer.json | 41 +++-- .../.devcontainer/devcontainer.json | 71 ++++---- .../.devcontainer/devcontainer.json | 37 +++-- tests/rust-example/.devcontainer/install.sh | 2 +- 11 files changed, 196 insertions(+), 194 deletions(-) diff --git a/README.md b/README.md index 857eb99..523b369 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ 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 @@ -20,6 +20,14 @@ 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 diff --git a/group_vars/all.example.yml b/group_vars/all.example.yml index 5429059..1391413 100644 --- a/group_vars/all.example.yml +++ b/group_vars/all.example.yml @@ -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 @@ -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 @@ -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 diff --git a/inventory.yml b/inventory.yml index ece60a1..b9cbfc6 100644 --- a/inventory.yml +++ b/inventory.yml @@ -1,4 +1,4 @@ all: hosts: localhost: - ansible_connection: local \ No newline at end of file + ansible_connection: local diff --git a/roles/devcontainer_sync/defaults/main.yml b/roles/devcontainer_sync/defaults/main.yml index 2f7e954..e2e12b6 100644 --- a/roles/devcontainer_sync/defaults/main.yml +++ b/roles/devcontainer_sync/defaults/main.yml @@ -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" \ No newline at end of file +devcontainer_sync_extra_files_root: "{{ playbook_dir }}/files" diff --git a/roles/devcontainer_sync/tasks/main.yml b/roles/devcontainer_sync/tasks/main.yml index 9443a98..e634319 100644 --- a/roles/devcontainer_sync/tasks/main.yml +++ b/roles/devcontainer_sync/tasks/main.yml @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 \ No newline at end of file + when: not item.stat.exists and not devcontainer_sync_create_missing | bool diff --git a/roles/devcontainer_sync/templates/devcontainer.json.j2 b/roles/devcontainer_sync/templates/devcontainer.json.j2 index db7794f..277aa22 100644 --- a/roles/devcontainer_sync/templates/devcontainer.json.j2 +++ b/roles/devcontainer_sync/templates/devcontainer.json.j2 @@ -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) }} diff --git a/tests/golden_output b/tests/golden_output index 4acf737..f42b912 100644 --- a/tests/golden_output +++ b/tests/golden_output @@ -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" }, @@ -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 diff --git a/tests/java-example/.devcontainer/devcontainer.json b/tests/java-example/.devcontainer/devcontainer.json index f630ef4..0057fa3 100644 --- a/tests/java-example/.devcontainer/devcontainer.json +++ b/tests/java-example/.devcontainer/devcontainer.json @@ -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*" +} diff --git a/tests/python-example/.devcontainer/devcontainer.json b/tests/python-example/.devcontainer/devcontainer.json index 1b0c187..56762a7 100644 --- a/tests/python-example/.devcontainer/devcontainer.json +++ b/tests/python-example/.devcontainer/devcontainer.json @@ -1,8 +1,29 @@ { - // 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" }, @@ -12,36 +33,14 @@ }, "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*" +} diff --git a/tests/rust-example/.devcontainer/devcontainer.json b/tests/rust-example/.devcontainer/devcontainer.json index a4c2978..64b027d 100644 --- a/tests/rust-example/.devcontainer/devcontainer.json +++ b/tests/rust-example/.devcontainer/devcontainer.json @@ -1,21 +1,20 @@ { - // 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*" +} diff --git a/tests/rust-example/.devcontainer/install.sh b/tests/rust-example/.devcontainer/install.sh index b5b5685..c62db75 100755 --- a/tests/rust-example/.devcontainer/install.sh +++ b/tests/rust-example/.devcontainer/install.sh @@ -1,3 +1,3 @@ #!/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