Skip to content

Commit dec885e

Browse files
committed
Default devcontainer to template's prebuilt image for fast startup
New repos from this template hit a chicken-and-egg problem: they can't use their own prebuilt image because CI hasn't built it yet. Default to the template's prebuilt image so new repos get fast startup immediately. Three devcontainer states: template-prebuilt (default), local-build (dev-use-local), and repo-prebuilt (dev-use-prebuilt). Fix awk bug in both switching scripts where `if (/\}/)` matched `{}` inside feature values like `"./claude-code": {}`, causing premature block termination. - Default devcontainer.json to template prebuilt image - Add dev-use-local script and pixi task - Fix awk closing-brace patterns in both switching scripts - Add docker-in-docker and common-utils features to CI devcontainer - Protect template image URL in rename_project.sh - Update README with three-state documentation table
1 parent cedc565 commit dec885e

File tree

8 files changed

+94
-63
lines changed

8 files changed

+94
-63
lines changed

.devcontainer/ci/devcontainer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"context": "../.."
66
},
77
"features": {
8-
"../claude-code": {}
8+
"../claude-code": {},
9+
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
10+
"ghcr.io/devcontainers/features/common-utils:2": {}
911
}
1012
}

.devcontainer/devcontainer.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
{
22
"name": "python_template",
33

4-
"build": {
5-
"dockerfile": "Dockerfile",
6-
"context": ".."
7-
},
4+
// Uses the template's prebuilt image by default for fast startup.
5+
// To build locally: pixi run dev-use-local
6+
// To use this repo's own prebuilt image: pixi run dev-use-prebuilt
7+
// "build": {
8+
// "dockerfile": "Dockerfile",
9+
// "context": ".."
10+
// },
11+
"image": "ghcr.io/blooop/python_template/devcontainer:latest",
12+
813
"features": {
914
"./claude-code": {},
1015
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
1116
"ghcr.io/devcontainers/features/common-utils:2": {}
1217
},
1318

14-
// To use a prebuilt image instead of building locally (faster startup),
15-
// comment out "build" and "features" above, uncomment "image" below,
16-
// and ensure the GHCR package is public (see README).
17-
// "image": "ghcr.io/blooop/python_template/devcontainer:latest",
18-
1919
"initializeCommand": ".devcontainer/claude-code/init-host.sh",
2020
"customizations": {
2121
"vscode": {

README.md

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -59,48 +59,30 @@ See [.claude/README.md](.claude/README.md) for detailed information about the Cl
5959

6060
# Devcontainer
6161

62-
By default, the devcontainer builds locally from `.devcontainer/Dockerfile`. This works out of the box for all users, including forks and projects created from this template.
62+
The devcontainer has three states:
6363

64-
## Switching to a prebuilt image (optional)
64+
| State | When | `image` | `build` | `features` |
65+
|---|---|---|---|---|
66+
| **Template prebuilt** (default) | Fresh clones, after rename | template URL | commented | active |
67+
| **Local build** | After `dev-use-local` | commented | active | active |
68+
| **Repo prebuilt** | After `dev-use-prebuilt` | repo URL | commented | commented |
6569

66-
A CI workflow (`.github/workflows/devcontainer.yml`) automatically builds and pushes a devcontainer image to GHCR whenever `.devcontainer/` files change on `main`. The image is published as `ghcr.io/<owner>/<repo>/devcontainer:latest`, where `<owner>` is your GitHub username or organization and `<repo>` is the repository name (e.g. `ghcr.io/myuser/myproject/devcontainer:latest`).
70+
By default, the devcontainer uses a prebuilt image from the template repository (`ghcr.io/blooop/python_template/devcontainer:latest`) for fast startup. This works immediately for new repos created from this template — no CI build needed.
6771

68-
You can migrate automatically with:
72+
To switch to building locally from `.devcontainer/Dockerfile` (e.g. after customizing the Dockerfile):
6973

7074
```bash
71-
pixi run dev-use-prebuilt
75+
pixi run dev-use-local
7276
```
7377

74-
Or manually:
75-
76-
1. Edit `.devcontainer/devcontainer.json`: comment out the `"build"` and `"features"` blocks, uncomment the `"image"` line
77-
2. Update the image reference to match your repo: `ghcr.io/<owner>/<repo>/devcontainer:latest`
78-
3. Push to `main` and wait for the CI workflow to complete. For new repos or forks where the workflow hasn't run yet, you can trigger it manually from the Actions tab or via `gh workflow run devcontainer.yml`
79-
4. **Make the GHCR package public** (see below) — GHCR packages are private by default and will fail with `MANIFEST_UNKNOWN` otherwise
80-
81-
### Making the GHCR package public
82-
83-
**Option 1: GitHub Web UI**
84-
85-
1. Go to your repository's package settings:
86-
- Personal repos: `https://github.com/users/<username>/packages/container/<repo>%2Fdevcontainer/settings`
87-
- Organization repos: `https://github.com/orgs/<org>/packages/container/<repo>%2Fdevcontainer/settings`
88-
2. Under "Danger Zone", click **Change visibility**
89-
3. Select **Public** and confirm
90-
91-
**Option 2: GitHub CLI**
78+
To switch to this repo's own prebuilt image (built by CI on each push to `main`):
9279

9380
```bash
94-
# Ensure your token has the write:packages scope
95-
gh auth refresh -s write:packages
96-
97-
# For personal repos:
98-
gh api --method PATCH /user/packages/container/<repo>%2Fdevcontainer -f visibility=public
99-
100-
# For organization repos:
101-
gh api --method PATCH /orgs/<org>/packages/container/<repo>%2Fdevcontainer -f visibility=public
81+
pixi run dev-use-prebuilt
10282
```
10383

84+
This detects the repo from git remote, updates `devcontainer.json` to use `ghcr.io/<owner>/<repo>/devcontainer:latest`, and attempts to make the GHCR package public. See the script output for manual steps if automatic visibility fails.
85+
10486
# Github setup
10587

10688
There are github workflows for CI, codecov and automated pypi publishing in `ci.yml` and `publish.yml`.

pixi.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ clear-pixi = "rm -rf .pixi pixi.lock"
100100
setup-git-merge-driver = "git config merge.ourslock.driver true"
101101
update-from-template-repo = "./scripts/update_from_template.sh"
102102
dev-use-prebuilt = "./scripts/devcontainer_use_prebuilt.sh"
103+
dev-use-local = "./scripts/devcontainer_use_local.sh"
103104

104105
[tool.pylint]
105106
extension-pkg-whitelist = ["numpy"]

scripts/devcontainer_use_local.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Switch devcontainer.json to build locally from Dockerfile.
5+
6+
DEVCONTAINER_JSON=".devcontainer/devcontainer.json"
7+
8+
# Already using local build?
9+
if grep -q '^[[:space:]]*"build"' "$DEVCONTAINER_JSON"; then
10+
echo "Already using local build. Nothing to do."
11+
exit 0
12+
fi
13+
14+
awk '
15+
# Uncomment commented build block
16+
/^ \/\/ "build": \{/ { in_build=1 }
17+
in_build {
18+
sub(/^ \/\/ /, " ")
19+
if (/^ \}/) in_build=0
20+
print; next
21+
}
22+
23+
# Uncomment commented features block
24+
/^ \/\/ "features": \{/ { in_features=1 }
25+
in_features {
26+
sub(/^ \/\/ /, " ")
27+
if (/^ \}/) in_features=0
28+
print; next
29+
}
30+
31+
# Comment out uncommented image line
32+
/^ "image":/ {
33+
sub(/^ /, " // ")
34+
print; next
35+
}
36+
37+
{ print }
38+
' "$DEVCONTAINER_JSON" > "${DEVCONTAINER_JSON}.tmp" && mv "${DEVCONTAINER_JSON}.tmp" "$DEVCONTAINER_JSON"
39+
40+
echo "Switched to local build from Dockerfile."

scripts/devcontainer_use_prebuilt.sh

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/bash
22
set -euo pipefail
33

4-
# Migrate devcontainer.json from local builds to a prebuilt GHCR image.
4+
# Switch devcontainer.json to use this repo's own prebuilt GHCR image.
55
# Also attempts to make the GHCR package public.
66

77
DEVCONTAINER_JSON=".devcontainer/devcontainer.json"
@@ -20,7 +20,6 @@ REMOTE_URL=$(git remote get-url origin 2>/dev/null) || {
2020
exit 1
2121
}
2222

23-
# Handle common GitHub remote URL formats
2423
REPO=$(echo "$REMOTE_URL" | sed -E 's#(ssh://git@github\.com/|git@github\.com:|https?://github\.com/|git://github\.com/)##; s/\.git$//')
2524

2625
if [[ -z "$REPO" || "$REPO" != */* ]]; then
@@ -32,43 +31,47 @@ IMAGE="ghcr.io/${REPO}/devcontainer:latest"
3231
echo "Repository: $REPO"
3332
echo "Image: $IMAGE"
3433

35-
# --- Check current state: look for an uncommented "image": line ---
36-
if grep -q '^[[:space:]]*"image"[[:space:]]*:' "$DEVCONTAINER_JSON"; then
37-
echo "Already using a prebuilt image. Nothing to do."
34+
# --- Check if already using this repo's prebuilt image (uncommented) ---
35+
if grep -q '^[[:space:]]*"image": "'"${IMAGE}"'"' "$DEVCONTAINER_JSON"; then
36+
echo "Already using repo prebuilt image. Nothing to do."
3837
exit 0
3938
fi
4039

41-
# --- Replace the file using awk for reliable block manipulation ---
40+
# --- Transform devcontainer.json ---
4241
awk -v image="$IMAGE" '
43-
# Comment out uncommented "build" block (4-space indent open/close).
44-
# NOTE: assumes no nested {} within these blocks.
42+
# Comment out uncommented build block
4543
/^ "build": \{/ { in_build=1 }
4644
in_build {
4745
sub(/^ /, " // ")
4846
if (/^ \/\/ \}/) in_build=0
4947
print; next
5048
}
5149
52-
# Comment out uncommented "features" block (4-space indent open/close).
53-
# NOTE: assumes no nested {} within these blocks.
50+
# Comment out uncommented features block
5451
/^ "features": \{/ { in_features=1 }
5552
in_features {
5653
sub(/^ /, " // ")
5754
if (/^ \/\/ \}/) in_features=0
5855
print; next
5956
}
6057
61-
# Uncomment the image line and set the correct reference
58+
# Uncomment commented image line and set repo URL
6259
/^ \/\/ "image":/ {
6360
printf " \"image\": \"%s\",\n", image
6461
next
6562
}
6663
64+
# Replace uncommented image line (e.g. template URL) with repo URL
65+
/^ "image":/ {
66+
printf " \"image\": \"%s\",\n", image
67+
next
68+
}
69+
6770
{ print }
6871
' "$DEVCONTAINER_JSON" > "${DEVCONTAINER_JSON}.tmp" && mv "${DEVCONTAINER_JSON}.tmp" "$DEVCONTAINER_JSON"
6972

7073
echo ""
71-
echo "Updated $DEVCONTAINER_JSON to use prebuilt image."
74+
echo "Updated $DEVCONTAINER_JSON to use repo prebuilt image."
7275

7376
# --- Try to make the GHCR package public ---
7477
echo ""

scripts/rename_project.sh

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
mv python_template "$1"
44

5-
# change project name in all files
6-
find . \( -type d -name .git -prune \) -o \( -type f -not -name 'tasks.json' -not -name 'update_from_template.sh' -not -name 'pixi.lock' \) -print0 | xargs -0 sed -i "s/python_template/$1/g"
5+
# change project name in all files (exclude main devcontainer.json to protect template image URL)
6+
find . \( -type d -name .git -prune \) -o \( -type f -not -name 'tasks.json' -not -name 'update_from_template.sh' -not -name 'pixi.lock' -not -path './.devcontainer/devcontainer.json' \) -print0 | xargs -0 sed -i "s/python_template/$1/g"
7+
8+
# update just the name field in devcontainer.json
9+
sed -i "s/\"name\": \"python_template\"/\"name\": \"$1\"/" .devcontainer/devcontainer.json
710

811
# regenerate lockfile to match renamed project
912
pixi update
@@ -18,7 +21,7 @@ if [ -n "$3" ]; then
1821
find . \( -type d -name .git -prune \) -o \( -type f -not -name 'tasks.json' -not -name 'update_from_template.sh' \) -print0 | xargs -0 sed -i "s/blooop@gmail.com/$3/g"
1922
fi
2023

21-
# github username
24+
# github username (exclude main devcontainer.json to protect template image URL)
2225
if [ -n "$4" ]; then
23-
find . \( -type d -name .git -prune \) -o \( -type f -not -name 'setup_host.sh' -not -name 'update_from_template.sh' \) -print0 | xargs -0 sed -i "s/blooop/$4/g"
26+
find . \( -type d -name .git -prune \) -o \( -type f -not -name 'setup_host.sh' -not -name 'update_from_template.sh' -not -path './.devcontainer/devcontainer.json' \) -print0 | xargs -0 sed -i "s/blooop/$4/g"
2427
fi

0 commit comments

Comments
 (0)