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
13 changes: 11 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"remoteEnv": {
// Allow X11 apps to run inside the container
"DISPLAY": "${localEnv:DISPLAY}",
// Mark this shell as running inside the devcontainer
"IN_DEVCONTAINER": "1",
// Put things that allow it in the persistent cache
"PRE_COMMIT_HOME": "/cache/pre-commit",
"UV_CACHE_DIR": "/cache/uv",
Expand All @@ -29,7 +31,10 @@
"python.terminal.activateEnvironment": false,
// Workaround to prevent garbled python REPL in the terminal
// https://github.com/microsoft/vscode-python/issues/25505
"python.terminal.shellIntegration.enabled": false
"python.terminal.shellIntegration.enabled": false,
// Only forward explicitly listed ports — auto-detection races with
// sphinx-autobuild and steals the port on restart
"remote.autoForwardPorts": false
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
Expand All @@ -43,7 +48,11 @@
]
}
},
// Create the config folder for the bash-config feature and uv cache
// Explicitly forward sphinx-autobuild port (auto-detection disabled above)
"forwardPorts": [
8000
],
Comment on lines +51 to +54
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? When you click on the link it autoforwards the port anyway...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got pulled in from my project that was having issues with sphinx-autobuild - I'm not sure why it fighting it - but we should not have this by default - agreed.

// Create host-side dirs needed for bind mounts before the container starts
"initializeCommand": "mkdir -p ${localEnv:HOME}/.config/terminal-config",
"runArgs": [
// Allow the container to access the host X11 display and EPICS CA
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# The developer stage is used as a devcontainer including dev versions
# of the build dependencies
# The devcontainer should use the developer target and run as root with podman
# or docker with user namespaces.
FROM ghcr.io/diamondlightsource/ubuntu-devcontainer:noble AS developer

# Add any system dependencies for the developer/build environment here
Expand Down
24 changes: 24 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,30 @@ docker_debug:
be useful if debugging the service inside of the cluster
infrastructure is required.

add_claude:
type: bool
help: |
Add a Claude Code sandbox to the devcontainer?
Disables host SSH agent / VS Code git credential injection inside
the container, mounts ~/.claude from the host, installs Claude Code
CLI, and enables `--dangerously-skip-permissions` autopilot mode.

install_gh:
type: bool
when: "{{ add_claude }}"
help: |
Install the GitHub CLI (gh) so Claude can push/pull via PAT auth?
Only useful inside the Claude sandbox — ordinary users typically
rely on SSH keys or VS Code git credentials.

install_glab:
type: bool
when: "{{ add_claude }}"
help: |
Install the GitLab CLI (glab) for projects that talk to a GitLab
instance (e.g. gitlab.diamond.ac.uk submodules)?
Only useful inside the Claude sandbox.
Comment on lines +124 to +138
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I reckon we should probably just include these in the ubuntu devcontainer unconditionally

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my first instinct and then I thought people would push back on it especially since glab would be a less common requirement. But if you like always including then I do to.


docs_type:
type: str
help: |
Expand Down
3 changes: 3 additions & 0 deletions example-answers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ description: An expanded https://github.com/DiamondLightSource/python-copier-tem
distribution_name: dls-python-copier-template-example
docker: true
docker_debug: true
add_claude: true
install_gh: true
install_glab: true
docs_type: sphinx
git_platform: github.com
github_org: DiamondLightSource
Expand Down
1 change: 0 additions & 1 deletion template/.devcontainer

This file was deleted.

115 changes: 115 additions & 0 deletions template/.devcontainer/devcontainer.json.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// For format details, see https://containers.dev/implementors/json_reference/
{
"name": "Python 3 Developer Container",
"build": {
"dockerfile": "../Dockerfile",
"target": "developer"
},{% if add_claude %}
// Tell VS Code the remote user is root so copyGitConfig writes to
// /root/.gitconfig (matching $HOME in the shell); otherwise it falls back
// to the base image's USER and the copy lands in the wrong home.
"remoteUser": "root",{% endif %}
"remoteEnv": {
// Allow X11 apps to run inside the container
"DISPLAY": "${localEnv:DISPLAY}",{% if add_claude %}
// Disable SSH agent forwarding — prevents Claude from using host SSH keys
"SSH_AUTH_SOCK": "",
// Disable VS Code git credential injection — prevents askpass from
// relaying host GitHub credentials into the container over the IPC socket
"GIT_ASKPASS": "",
"VSCODE_GIT_IPC_HANDLE": "",
"VSCODE_GIT_ASKPASS_MAIN": "",
"VSCODE_GIT_ASKPASS_NODE": "",
"VSCODE_GIT_ASKPASS_EXTRA_ARGS": "",{% endif %}
// Mark this shell as running inside the devcontainer
"IN_DEVCONTAINER": "1",
// Put things that allow it in the persistent cache
"PRE_COMMIT_HOME": "/cache/pre-commit",
"UV_CACHE_DIR": "/cache/uv",
"UV_PYTHON_CACHE_DIR": "/cache/uv-python",
// Make a venv that is specific for this workspace path as the cache is shared
"UV_PROJECT_ENVIRONMENT": "/cache/venv-for${localWorkspaceFolder}",
// Do the equivalent of "activate" the venv so we don't have to "uv run" everything
"VIRTUAL_ENV": "/cache/venv-for${localWorkspaceFolder}",
"PATH": "/cache/venv-for${localWorkspaceFolder}/bin:${containerEnv:PATH}"
},
"customizations": {
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
// Use the container's python by default
"python.defaultInterpreterPath": "/cache/venv-for${localWorkspaceFolder}/bin/python",
// Don't activate the venv as it is already in the PATH
"python.terminal.activateEnvInCurrentTerminal": false,
"python.terminal.activateEnvironment": false,
// Workaround to prevent garbled python REPL in the terminal
// https://github.com/microsoft/vscode-python/issues/25505
"python.terminal.shellIntegration.enabled": false{% if sphinx %},
// Only forward explicitly listed ports — auto-detection races with
// sphinx-autobuild and steals the port on restart
"remote.autoForwardPorts": false{% endif %}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"github.vscode-github-actions",
"tamasfe.even-better-toml",
"redhat.vscode-yaml",
"ryanluker.vscode-coverage-gutters",
"charliermarsh.ruff",
"ms-azuretools.vscode-docker"{% if add_claude %},
"anthropic.claude-code"{% endif %}
]
}
},{% if sphinx %}
// Explicitly forward sphinx-autobuild port (auto-detection disabled above)
"forwardPorts": [
8000
],{% endif %}
// Create host-side dirs needed for bind mounts before the container starts
"initializeCommand": "mkdir -p ${localEnv:HOME}/.config/terminal-config{% if add_claude %} ${localEnv:HOME}/.claude{% endif %}",
"runArgs": [
// Allow the container to access the host X11 display and EPICS CA
"--net=host",
// Make sure SELinux does not disable with access to host filesystems like tmp
"--security-opt=label=disable"
],
"mounts": [
// Mount in the user terminal config folder so it can be edited
{
"source": "${localEnv:HOME}/.config/terminal-config",
"target": "/user-terminal-config",
"type": "bind"
},
// Keep a persistent cross container cache for uv, pre-commit, and the venvs
{
"source": "devcontainer-shared-cache",
"target": "/cache",
"type": "volume"
}{% if install_gh %},
// Persist gh auth across container rebuilds with per-repo scoped PAT
{
"source": "gh-auth-${localWorkspaceFolderBasename}",
"target": "/root/.config/gh",
"type": "volume"
}{% endif %}{% if install_glab %},
// Persist glab auth across container rebuilds (GitLab CLI)
{
"source": "glab-auth-${localWorkspaceFolderBasename}",
"target": "/root/.config/glab-cli",
"type": "volume"
}{% endif %}{% if add_claude %},
// Mount Claude config from host (settings, memory, skills)
{
"source": "${localEnv:HOME}/.claude",
"target": "/root/.claude",
"type": "bind"
}{% endif %}
],
// Mount the parent as /workspaces so we can pip install peers as editable
"workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",{% if add_claude %}
"postCreateCommand": ".devcontainer/postCreate.sh",
"postStartCommand": ".devcontainer/postStart.sh"{% else %}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we always put a postCreate in and jinja template just the claude bits?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think yes - its tidier than long bash entries embedded in the devcontainer.json

// After the container is created, recreate the venv then make pre-commit first run faster
"postCreateCommand": "uv venv --clear && uv sync && pre-commit install --install-hooks"{% endif %}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail

# Install Claude Code CLI
curl -fsSL https://claude.ai/install.sh | bash

# Install Python dependencies and pre-commit hooks
uv venv --clear
uv sync
pre-commit install --install-hooks

# Initialise git submodules if any are declared
[ -f .gitmodules ] && git submodule update --init || true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line is useful in the non-claude case too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - I thought I had dropped that. It is useful for first opening a devcontainer that needs submodules to build, but ...... if you rebuild your container while you are in progress with submodule changes they disapear. I think that cost outweighs the benefit.

So I vote to take this back out.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash
set -euo pipefail

# Wipe any credential helpers and SSH URL rewrites injected by VS Code's
# Dev Containers extension when it copies the host gitconfig. An empty-string
# value resets the helper list so only an explicit PAT via `just gh-auth`
# can authenticate to remotes.
git config --global credential.helper ''
git config --global --unset-all url.ssh://git@github.com/.insteadOf 2>/dev/null || true

# Force all SSH-style remotes to use HTTPS so the gh/glab credential helpers
# handle auth. This keeps the container SSH-key-free (Claude stays sandboxed)
# while still allowing push/pull on repos whose remotes are set to git@...:.
git config --global url."https://github.com/".insteadOf "git@github.com:"
{%- if install_glab %}
git config --global url."https://gitlab.diamond.ac.uk/".insteadOf "git@gitlab.diamond.ac.uk:"
{%- endif %}

{% if install_gh -%}
# If gh CLI has cached credentials (survive container rebuild), re-register
# its git credential helper so HTTPS remotes authenticate automatically.
if gh auth status &>/dev/null; then
gh auth setup-git
fi
{%- endif %}
24 changes: 23 additions & 1 deletion template/Dockerfile.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,29 @@ FROM ghcr.io/diamondlightsource/ubuntu-devcontainer:noble AS developer
# Add any system dependencies for the developer/build environment here
RUN apt-get update -y && apt-get install -y --no-install-recommends \
graphviz \
&& apt-get dist-clean{% if docker %}
&& apt-get dist-clean{% if add_claude %}

# Node is required by Claude Code's hook runtime
RUN apt-get update -y && apt-get install -y --no-install-recommends \
nodejs \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this plus gh plus glab be in the ubuntu devcontainer base?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes.

&& apt-get dist-clean{% endif %}{% if install_gh %}

# GitHub CLI — used by Claude to authenticate to github.com via PAT
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | \
dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \
apt-get update && apt-get install -y --no-install-recommends gh && \
apt-get dist-clean{% endif %}{% if install_glab %}
Comment on lines +15 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2.46.0-4 is in apt for 26.04, is that new enough?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should think yes - so lets do as you suggest below and put these in the base container from 26.04 onwards and claude enabled PCT will use that.


# GitLab CLI — used by Claude to authenticate to gitlab instances via PAT.
# No apt repo, so install from the upstream release tarball.
ARG GLAB_VERSION=1.92.1
RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_amd64.tar.gz" \
| tar -xz -C /tmp bin/glab && \
install -m 0755 /tmp/bin/glab /usr/local/bin/glab && \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.53.0-1build1 is in apt for 26.04, is that new enough?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above.

rm -rf /tmp/bin{% endif %}{% if docker %}

# The build stage installs the context into the venv
FROM developer AS build
Expand Down
23 changes: 23 additions & 0 deletions template/{% if add_claude %}justfile{% endif %}.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Start Claude Code in sandbox mode (no SSH agent, skip permission prompts)
claude:
SSH_AUTH_SOCK= IS_SANDBOX=1 claude --dangerously-skip-permissions{% if install_gh %}


# Authenticate gh CLI with a GitHub PAT (token not stored in shell history)
gh-auth:
#!/bin/bash
read -sp "GitHub PAT: " t && echo
echo "$t" | gh auth login --with-token
unset t
gh auth setup-git
gh auth status{% endif %}{% if install_glab %}


# Authenticate glab CLI with a GitLab PAT (token not stored in shell history).
# --git-protocol https prevents glab's SSH insteadOf rewrite.
glab-auth hostname="gitlab.com":
#!/bin/bash
read -sp "GitLab PAT for {{ '{{' }} hostname {{ '}}' }}: " t && echo
echo "$t" | glab auth login --stdin --hostname {{ '{{' }} hostname {{ '}}' }} --git-protocol https
unset t
glab auth status{% endif %}
22 changes: 22 additions & 0 deletions tests/test_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,28 @@ def test_gitignore_same():
assert top_gi.read() == template_gi.read()


def test_meta_matches_no_claude_template(tmp_path: Path):
"""The meta repo's .devcontainer/devcontainer.json and Dockerfile must
match what the template renders with all Claude options off (and docker
off, since the meta repo isn't a deployable service). Catches drift
between the meta repo's own dev experience and what we ship to projects
that opt out of the Claude sandbox."""
copy_project(
tmp_path,
add_claude=False,
install_gh=False,
install_glab=False,
docker=False,
docker_debug=False,
)
for relpath in [".devcontainer/devcontainer.json", "Dockerfile"]:
rendered = (tmp_path / relpath).read_text()
meta = (TOP / relpath).read_text()
assert rendered == meta, (
f"{relpath} drift between meta repo and template (add_claude=no)"
)


def test_private_member_access(tmp_path: Path):
code = """
class MyClass:
Expand Down
Loading