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
12 changes: 12 additions & 0 deletions .devcontainer/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@
# 1. 有 WSL_HOME:bind mount 到指定目录(直接访问 WSL 文件)
# 2. 无 WSL_HOME:使用 named volume(数据在 Docker 管理区域)
# 3. compose.yaml 会自动处理,无需手动创建 volume

# ============================================
# 可选:镜像所有者标识(fork 用)
# ============================================
# 仅当你 fork 本模板、要消费自己 registry 下的 Prebuilt Image 时才需设置。
# 留空则使用上游默认镜像 xiao806852034/ai-dev-container。
# 发布契约见 docs/adr/0001-image-release-contract.md。
# 注意:GitHub Actions 侧请在 repo Settings → Variables 设同名变量(vars.*),
# 本地 compose 读不到 GitHub vars,故此处用 .env 提供。

# DOCKERHUB_USERNAME=your-dockerhub-username
# IMAGE_NAME=ai-dev-container
9 changes: 4 additions & 5 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,9 @@ vim ~/.tmux.conf

## 预构建镜像

此项目使用预构建镜像 `xiao806852034/ai-dev-container:latest`,包含所有工具和配置
默认使用上游 Prebuilt Image `xiao806852034/ai-dev-container:latest`(可在 `.env` 设 `DOCKERHUB_USERNAME`/`IMAGE_NAME` 改为自己的)。镜像构建源码见 `devimage-build/` 目录

**镜像构建源码**:见 `devimage-build/` 目录
发布行为(`:latest` 指向什么、要不要 pin 版本)由发布契约定义,见 [docs/adr/0001-image-release-contract.md](../docs/adr/0001-image-release-contract.md)。

**优势**:
- 无需本地构建,启动速度快
Expand All @@ -341,11 +341,10 @@ vim ~/.tmux.conf

**更新镜像**:
```bash
# 拉取最新镜像
# 拉取最新(Latest Pointer)
docker pull xiao806852034/ai-dev-container:latest

# 重建容器
# VS Code: F1 → Dev Containers: Rebuild Container
# 重建容器:VS Code F1 → Dev Containers: Rebuild Container
```

## 自定义配置
Expand Down
6 changes: 4 additions & 2 deletions .devcontainer/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
services:
devcontainer:
image: xiao806852034/ai-dev-container:latest
# pull_policy: always # 此处有 bug,VS Code 生成临时 compose 文件,把 image 指向这个本地镜像,然后远程没有
# 镜像引用参数化:默认即上游镜像,fork 者可在 .devcontainer/.env 设 DOCKERHUB_USERNAME/IMAGE_NAME。
# 发布契约见 docs/adr/0001-image-release-contract.md(默认消费 Latest Pointer :latest)。
image: ${DOCKERHUB_USERNAME:-xiao806852034}/${IMAGE_NAME:-ai-dev-container}:latest
# pull_policy: always # 已知 bug(见 ADR 0001「已知缺口」/ follow-up issue):VS Code 生成临时 compose 文件把 image 指向本地镜像,远程拉不到
volumes:
- ..:/home/vscode/workspace

Expand Down
52 changes: 39 additions & 13 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
# Prebuilt Image release mechanism. Contract: docs/adr/0001-image-release-contract.md
# Release Trigger: a Release Tag push (v*) or a Manual Rebuild (workflow_dispatch naming an existing tag).
# No main-branch release. Only a Release Tag push moves the Latest Pointer (:latest).
name: Build and Push Docker Image

on:
workflow_dispatch:

inputs:
tag:
description: 'Existing Release Tag to rebuild, e.g. v1.0.0 (Manual Rebuild; does NOT move :latest)'
required: true
type: string
push:
tags:
- 'v*'

env:
IMAGE_NAME: ai-dev-container
DOCKERHUB_USERNAME: xiao806852034
# Forkable: set repo Variables to publish to your own registry. Defaults keep the upstream image.
IMAGE_NAME: ${{ vars.IMAGE_NAME || 'ai-dev-container' }}
DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME || 'xiao806852034' }}

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout code
- name: Checkout code (the Release Tag being built)
uses: actions/checkout@v4
with:
# tag push -> the tag ref; Manual Rebuild -> the tag named in the input
ref: ${{ github.event.inputs.tag || github.ref }}

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
Expand All @@ -31,19 +42,34 @@ jobs:
username: ${{ env.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}

- name: Build and push image with devcontainer CLI
- name: Build and push the Release Tag image
env:
VERSION: ${{ github.event.inputs.tag || github.ref_name }}
run: |
# A Manual Rebuild must name an existing Release Tag (v*), never a branch.
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
case "$VERSION" in
v*) ;;
*) echo "::error::workflow_dispatch input 'tag' must be a Release Tag like v1.0.0 (got '$VERSION')"; exit 1 ;;
esac
fi

# Install devcontainer CLI
npm install -g @devcontainers/cli
# Build image with features (multi-arch)

# Build image with features (multi-arch) and push the immutable :$VERSION tag
devcontainer build \
--workspace-folder ./devimage-build \
--image-name ${{ env.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} \
--image-name "$DOCKERHUB_USERNAME/$IMAGE_NAME:$VERSION" \
--platform linux/amd64,linux/arm64 \
--push

# Tag and push latest

- name: Move the Latest Pointer (only on a Release Tag push)
if: github.event_name == 'push'
env:
VERSION: ${{ github.ref_name }}
run: |
# :latest is an alias for the most recent Release Tag. A Manual Rebuild never reaches this step.
docker buildx imagetools create \
--tag ${{ env.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:latest \
${{ env.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
--tag "$DOCKERHUB_USERNAME/$IMAGE_NAME:latest" \
"$DOCKERHUB_USERNAME/$IMAGE_NAME:$VERSION"
19 changes: 19 additions & 0 deletions .github/workflows/release-contract-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Guards the Prebuilt Image release contract against doc/automation drift.
# Contract: docs/adr/0001-image-release-contract.md
name: Release Contract Check

on:
pull_request:
push:
branches:
- main

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Check release-contract invariants
run: bash scripts/check-release-contract.sh
18 changes: 18 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,21 @@ _Avoid_: Public port, forwarded port
**Local Hub**:
A remote-control process started inside the development container that is reachable from the host machine but not published beyond the host unless the user adds a tunnel.
_Avoid_: Public service, relay

**Release Tag**:
A `v*` git tag whose push publishes an immutable Prebuilt Image at `:vX.Y.Z`. The unit of release for the template.
_Avoid_: version, build number

**Latest Pointer**:
The `:latest` image tag, defined as an alias for the most recent Release Tag. Moved only by a Release Tag push.
_Avoid_: newest, current

**Release Trigger**:
An event that publishes the Prebuilt Image — either a Release Tag push or a Manual Rebuild naming an existing Release Tag. Excludes pushes to `main`.
_Avoid_: build trigger

**Manual Rebuild**:
A `workflow_dispatch` run that takes a Release Tag as input and republishes that exact version. Does not create a new version and does not move the Latest Pointer.
_Avoid_: rerun, redeploy

See [docs/adr/0001-image-release-contract.md](docs/adr/0001-image-release-contract.md) for the release contract that owns these terms.
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ ta ai # 重新连接

详细说明见 [.devcontainer/README.md](.devcontainer/README.md)

## 镜像构建
## 镜像构建与发布

预构建镜像源码在 `devimage-build/` 目录,通过 GitHub Actions 自动构建
预构建镜像源码在 `devimage-build/` 目录,通过 GitHub Actions 按 Release Tag 契约发布

详细说明见 [devimage-build/README.md](devimage-build/README.md)
发布契约(触发、`:latest` 语义、pin 策略)见 [docs/adr/0001-image-release-contract.md](docs/adr/0001-image-release-contract.md);维护者发版操作见 [devimage-build/README.md](devimage-build/README.md)

## 自定义

Expand All @@ -175,17 +175,15 @@ ta ai # 重新连接

编辑 `devimage-build/.devcontainer/Dockerfile`,添加安装命令。

### 重新构建镜像
### 发布镜像

镜像发布遵循 Release Tag 契约——push 一个 `v*` tag 即构建并移动 `:latest`。详见 [发布契约](docs/adr/0001-image-release-contract.md) 与 [devimage-build/README.md](devimage-build/README.md)。本地构建仅供测试,不发布:

```bash
cd devimage-build
devcontainer build --workspace-folder .
docker tag <image-id> xiao806852034/ai-dev-container:latest
docker push xiao806852034/ai-dev-container:latest
devcontainer build --workspace-folder . # 本地测试,不推送、不动 :latest
```

或推送 tag 到 GitHub,自动触发构建。

## 常见问题

### Q: 脚本报错 `$'\r': command not found`?
Expand Down
59 changes: 30 additions & 29 deletions devimage-build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,44 +71,41 @@ devcontainer build --workspace-folder .
# VS Code: F1 → Dev Containers: Reopen in Container
```

## 自动构建
## 发布(Release)

### GitHub Actions 工作流
> 本节是 Prebuilt Image 发布的**维护者向单一散文所有者**。持久决策与 why 见
> [docs/adr/0001-image-release-contract.md](../docs/adr/0001-image-release-contract.md);
> 机制的可执行真相是 [`.github/workflows/build-image.yml`](../.github/workflows/build-image.yml)。
> 其它文档只链接到这里,不复述发布事实。

`.github/workflows/build-image.yml` 配置了自动构建:
### 契约速览

**触发条件**:
- 推送 tag(格式:`v*`)
- 推送到 main 分支
- **Release Trigger**:镜像只由两种事件发布——push 一个 `v*` **Release Tag**,或一次指名已存在 tag 的 **Manual Rebuild**(`workflow_dispatch`)。**排除** main 分支 push。
- **Latest Pointer**:`:latest` = 最近一个 Release Tag 的别名;**只有 Release Tag push 会移动它**,Manual Rebuild 不会。
- **慢变基线**:镜像只烘焙慢变内容;AI Agent Toolchain 走 Startup Install(容器创建时装),不进镜像。所以发布是低频、有意为之的动作。

**构建步骤**:
1. 检出代码
2. 设置 QEMU(多架构支持)
3. 设置 Docker Buildx
4. 登录 Docker Hub
5. 构建并推送镜像
### 前置配置

### 配置 GitHub Secrets
GitHub 仓库 **Settings → Secrets and variables → Actions** 配置:

在 GitHub 仓库设置中添加:
| 类型 | 名称 | 必需 | 说明 |
|------|------|------|------|
| Secret | `DOCKER_HUB_TOKEN` | 是 | Docker Hub Access Token(必须是 secret) |
| Variable | `DOCKERHUB_USERNAME` | 否 | 不设则用默认 `xiao806852034`;fork 发到自己 registry 时设置 |
| Variable | `IMAGE_NAME` | 否 | 不设则用默认 `ai-dev-container` |

- `DOCKER_HUB_USERNAME`:Docker Hub 用户名
- `DOCKER_HUB_TOKEN`:Docker Hub Access Token

### 触发构建

**方式一:推送 tag**
### 发布新版本(会移动 :latest)

```bash
git tag v1.0.0
git push origin v1.0.0
```

**方式二:推送到 main**
push 后 workflow 构建多架构 `:(v1.0.0)` 并把 `:latest` 指向它。

```bash
git push origin main
```
### Manual Rebuild(重建已存在版本,不动 :latest)

GitHub **Actions → Build and Push Docker Image → Run workflow**,在 `tag` 输入框填一个已存在的 Release Tag(如 `v1.0.0`)。用于重跑失败的发布构建;**不**新建版本、**不**移动 `:latest`(从分支误触发分支构建的漏洞已就此堵死)。

## 自定义修改

Expand Down Expand Up @@ -259,7 +256,7 @@ RUN rm -rf /var/lib/apt/lists/*

### 多架构支持

使用 Docker Buildx 构建多架构镜像
QEMU + Docker Buildx 提供多架构能力,实际构建与推送由 devcontainer CLI 完成(详见 workflow)

```yaml
- name: Set up QEMU
Expand All @@ -268,10 +265,14 @@ RUN rm -rf /var/lib/apt/lists/*
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
- name: Build and push the Release Tag image
run: |
npm install -g @devcontainers/cli
devcontainer build \
--workspace-folder ./devimage-build \
--image-name "$DOCKERHUB_USERNAME/$IMAGE_NAME:$VERSION" \
--platform linux/amd64,linux/arm64 \
--push
```

## 常见问题
Expand Down
41 changes: 41 additions & 0 deletions docs/adr/0001-image-release-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# ADR 0001:Prebuilt Image Release Contract

- **状态**:Accepted
- **日期**:2026-06-24
- **关联**:CONTEXT.md(*Prebuilt Image* / *Startup Install* / *Release Tag* / *Latest Pointer* / *Release Trigger* / *Manual Rebuild*)、`.github/workflows/build-image.yml`、`.devcontainer/compose.yaml`、Issue #2

本 ADR 是 Prebuilt Image 发布契约的**单一真相源**。散文文档(根 `README.md`、`devimage-build/README.md`、`.devcontainer/README.md`)只**链接**到这里,不得各自复述发布事实;`build-image.yml` 是发布**机制**的可执行真相。

## Context

一次架构审查发现"文档"与"自动化"之间存在漂移:`devimage-build/README.md` 声称镜像会在"推送到 main 分支"时构建、要求配置一个 `DOCKER_HUB_USERNAME` secret、并以 `docker/build-push-action@v5` 为例——这三点都与实际 workflow 不符;根 `README.md` 的"重新构建镜像"指引直接 `docker push :latest`(无版本 tag),与 workflow 的"先版本 tag 再 latest"模型矛盾;`workflow_dispatch` 由于用 `${github.ref_name}` 命名镜像,从 `main` 手动触发会构建 `:main` 并把 `:latest` 指过去——一个契约漏洞。此外没有任何 CI 防止这类漂移复发。

关键设计事实决定了整个契约:**Prebuilt Image 是一个被刻意做薄的慢变基线**。`Dockerfile` + Features 只烘焙慢变内容(系统工具、Node/Python/Rust/gh 运行时、git/tmux/zsh 配置);所有快变的 **AI Agent Toolchain**(claude/codex/gemini/hapi)通过 **Startup Install** 在容器创建时 `npm install -g`,刻意**不**烘焙进镜像。因此镜像只有在改动 `Dockerfile`/Features/configs 时才需要重新发布——这是低频、有意为之的动作,而不是每次提交都要发的东西。

## Decision

1. **Release Trigger(触发)**:镜像仅由两种事件发布——push 一个 `v*` 形态的 **Release Tag**,或一次指名某个已存在 Release Tag 的 `workflow_dispatch`(**Manual Rebuild**)。**不**在 push 到 `main` 时自动发布。理由:基线慢变,发布应是版本化、有意的动作;per-commit 发布只会无谓地搅动 `:latest`。

2. **Latest 语义**:**Latest Pointer**(`:latest`)定义为"最近一个 Release Tag"的别名。**只有 Release Tag push 能移动它**。Manual Rebuild 重建指定版本的镜像,但**绝不**移动 Latest Pointer,因此不会出现 `:latest` 指向 `:main` 之类分支构建的情况。漏洞就此堵死。

3. **消费端 pin 策略**:模板默认消费 `:latest`(常见场景=总是拿到最新基线,因波动已外移到 Startup Install,所以安全)。pin 到具体 `:vX.Y.Z` 作为**可选**项写进文档,面向需要可复现或团队统一的用户。

4. **所有权**:本 ADR 拥有持久决策与 why(单一真相源);`build-image.yml` 是机制的可执行真相;`devimage-build/README.md` 的"发布"节是面向维护者的**唯一散文所有者**(怎么发版、怎么 Manual Rebuild);根 `README.md`、`.devcontainer/README.md` 只保留一句话 + 链接,不复述。

5. **CI 不变量**:`scripts/check-release-contract.sh`(由 `.github/workflows/release-contract-check.yml` 在 PR/push 时运行)断言三条不变量:
- ① `compose.yaml` 的默认镜像引用 = `build-image.yml` 的默认 `DOCKERHUB_USERNAME`/`IMAGE_NAME`;
- ② `build-image.yml` 的 `on:` 恰为 `{ push tags 'v*', workflow_dispatch }`,且**不含** `branches:`;
- ③ grep-guard:已退役的错误声明(main 分支触发、`DOCKER_HUB_USERNAME` secret、`docker/build-push-action`)不得在散文文档中复现。

6. **可 fork**:镜像所有者标识不再硬编码。`DOCKERHUB_USERNAME` 与 `IMAGE_NAME` 提为 GitHub repo variables(`vars.*`,带默认值 `xiao806852034` / `ai-dev-container`);`compose.yaml` 用 docker-compose 变量替换 `${DOCKERHUB_USERNAME:-…}/${IMAGE_NAME:-…}`,由 `.devcontainer/.env` 驱动(GitHub `vars` 在本地读不到,故消费端走 `.env`)。Docker Hub access token 仍是 secret(`DOCKER_HUB_TOKEN`)。

## Consequences

- `build-image.yml`:`workflow_dispatch` 新增 required 输入 `tag`;构建版本取 `inputs.tag`(dispatch)或 `github.ref_name`(tag push);checkout 该 tag;移动 `:latest` 的步骤加 `if: github.event_name == 'push'` 守卫;username/image 改读 `vars.*`。
- `compose.yaml`:镜像引用参数化;`.env.example` 增补 `DOCKERHUB_USERNAME`/`IMAGE_NAME` 示例。
- 文档:`devimage-build/README.md` 发布节重写为真相、修掉三处漂移;根 README 与 `.devcontainer/README.md` 的镜像章节缩为链接。
- fork 者:在 repo Settings → Variables 设 `DOCKERHUB_USERNAME`/`IMAGE_NAME`,在 `.env` 设同名变量,并配 `DOCKER_HUB_TOKEN` secret,即可发布到自己的 registry,无需改 workflow。

### 已知缺口(不在本契约内,单独跟踪)

- `compose.yaml` 的 `pull_policy: always` 因 VS Code 生成临时 compose 文件把 `image` 指向本地镜像而被注释——消费 `:latest` 时"如何真正拉到更新"的机制问题。作为 ready-for-agent follow-up issue 单独处理,不阻塞本契约。
Loading
Loading