From 10a9e9f86b36b421792274d27726093940b217d0 Mon Sep 17 00:00:00 2001 From: wanghj Date: Thu, 11 Jun 2026 11:34:42 +0800 Subject: [PATCH] build(embed): gate docker bump and selective image builds Decouple local embed version bumps from docker builds. Bump agent.toml only when csgclaw-cli or template Dockerfiles change vs HEAD, at most once per baseline; skip workspace-only edits. Build embed images only for changed CLI/Dockerfile inputs. Add shell tests and build docs. Bump PicoClaw embed templates for picoclaw base 2026.6.10. --- docs/build.md | 9 +- docs/build.zh.md | 9 +- .../embed/picoclaw-manager/Dockerfile | 2 +- .../embed/picoclaw-manager/agent.toml | 4 +- .../embed/picoclaw-worker/Dockerfile | 2 +- .../embed/picoclaw-worker/agent.toml | 4 +- scripts/build-docker-embed-images.sh | 17 +- scripts/bump-docker-embed-version.sh | 2 + scripts/docker-embed-agent-toml-common.sh | 182 +++++++++++++++++ scripts/docker-embed-build_test.sh | 99 ++++++++++ scripts/docker-embed-bump_test.sh | 187 ++++++++++++++++++ 11 files changed, 505 insertions(+), 12 deletions(-) create mode 100755 scripts/docker-embed-build_test.sh create mode 100755 scripts/docker-embed-bump_test.sh diff --git a/docs/build.md b/docs/build.md index 04cdafcd..1af1ffc5 100644 --- a/docs/build.md +++ b/docs/build.md @@ -79,14 +79,19 @@ Frontend structure and verification details: [docs/web/development.md](web/devel Builtin runtime templates live under `internal/templates/embed//` and are embedded directly via `go:embed` (`agent.toml`, `workspace/`, etc.). Each docker embed template carries a `version` field and matching `image.ref`. -**Local (before PR)**: `make build-all` bumps `version`, syncs `image.ref`, rebuilds `csgclaw` (so embed matches image tags), then builds Docker images. +**Local (before PR)**: `make build-all` runs two independent steps: + +1. **version/ref (bump)**: increments the last segment and syncs `image.ref` per template only when `cmd/csgclaw-cli/` or that template's `Dockerfile` differs from git `HEAD` and working-tree `version` still matches `HEAD` (at most once vs baseline). `workspace/` changes do not bump. Force: `DOCKER_EMBED_FORCE_BUMP=1`. +2. **Docker images (build)**: `csgclaw-cli` changed → build all embed images; one `Dockerfile` changed → build that template only; repeatable without changing `version`. Skips when no image inputs changed. Force all: `DOCKER_EMBED_FORCE_BUILD=1`. Explicit targets (e.g. `make build-picoclaw-manager-image`) always build the named image. + +Then rebuilds `csgclaw` (`workspace` ships via `go:embed`, not the sandbox image). **GitLab CI (main)**: reads committed `version` / `image.ref`, builds and pushes images **without** modifying `agent.toml`; runs when embed `agent.toml` changed in the pushed range (`CI_COMMIT_BEFORE_SHA..HEAD`) or `version` differs from the compare base. | Target | Description | |--------|-------------| | `make sync-docker-embed-image-refs` | Sync `image.ref` from current `version` (no bump) | -| `make bump-docker-embed-version` | Bump `version` and sync `image.ref` for all docker embed templates | +| `make bump-docker-embed-version` | Bump (or skip per image-input rules) `version` and sync `image.ref` per template (independent of docker build) | | `make ensure-docker-embed-manifests` | Run `sync-docker-embed-image-refs` when `image.ref` is missing or out of sync with `version` (used by `make build` / `make test`) | Templates with a `Dockerfile` are discovered by `scripts/list-docker-embed-templates.sh` (currently `openclaw-manager`, `openclaw-worker`, `picoclaw-manager`, and `picoclaw-worker`). diff --git a/docs/build.zh.md b/docs/build.zh.md index 6e0da593..9c9fa6fa 100644 --- a/docs/build.zh.md +++ b/docs/build.zh.md @@ -79,14 +79,19 @@ make build-all 内置运行时模板位于 `internal/templates/embed//`,通过 `go:embed` 直接嵌入(`agent.toml`、`workspace/` 等)。每个 docker embed 模板在 `agent.toml` 中有 `version` 字段与对应的 `image.ref`。 -**本地(PR 前)**:`make build-all` 会先递增 `version`、同步 `image.ref`,再编译 `csgclaw`(使 embed 与镜像 tag 一致),最后构建 Docker 镜像。 +**本地(PR 前)**:`make build-all` 分两步,互不影响: + +1. **version/ref(bump)**:仅当 `cmd/csgclaw-cli/` 或某模板 `Dockerfile` 相对 git `HEAD` 有改动,且工作区 `version` 仍等于 `HEAD` 时,对该模板 `version` 末段 +1 并同步 `image.ref`;相对基线最多自增一次。`workspace/` 改动不 bump。强制递增:`DOCKER_EMBED_FORCE_BUMP=1`。 +2. **Docker 镜像(build)**:`csgclaw-cli` 改动 → 构建全部 embed 镜像;某模板 `Dockerfile` 改动 → 仅构建该模板;可多次构建,不改变 `version`。无改动时跳过 docker build。强制全建:`DOCKER_EMBED_FORCE_BUILD=1`。单独 target(如 `make build-picoclaw-manager-image`)始终构建指定镜像。 + +随后编译 `csgclaw`(`workspace` 改动通过 `go:embed` 进入二进制,无需 bump 镜像版本)。 **GitLab CI(main)**:仅读取已提交的 `version` / `image.ref` 构建并 push 镜像,**不会**修改 `agent.toml`;当本次 push 范围内(`CI_COMMIT_BEFORE_SHA..HEAD`)embed `agent.toml` 发生变化或 `version` 相对 compare base 改变时触发构建。 | 目标 | 说明 | |------|------| | `make sync-docker-embed-image-refs` | 按当前 `version` 同步 `image.ref`(不递增) | -| `make bump-docker-embed-version` | 递增全部 docker embed 模板的 `version` 并同步 `image.ref` | +| `make bump-docker-embed-version` | 按镜像输入规则递增(或跳过)各模板 `version` 并同步 `image.ref`(与 docker build 独立) | | `make ensure-docker-embed-manifests` | `image.ref` 缺失或与 `version` 不一致时调用 `sync-docker-embed-image-refs`(`make build` / `make test` 使用) | 带 `Dockerfile` 的模板由 `scripts/list-docker-embed-templates.sh` 发现(当前为 `openclaw-manager`、`openclaw-worker`、`picoclaw-manager` 和 `picoclaw-worker`)。 diff --git a/internal/templates/embed/picoclaw-manager/Dockerfile b/internal/templates/embed/picoclaw-manager/Dockerfile index 232bbfca..fde84dc0 100644 --- a/internal/templates/embed/picoclaw-manager/Dockerfile +++ b/internal/templates/embed/picoclaw-manager/Dockerfile @@ -12,7 +12,7 @@ # -t picoclaw-manager:local . # Default upstream picoclaw base image (single source of truth for local make / CI builds). -ARG PICOCLAW_IMAGE=opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw:2026.6.8 +ARG PICOCLAW_IMAGE=opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw:2026.6.10 FROM ${PICOCLAW_IMAGE} diff --git a/internal/templates/embed/picoclaw-manager/agent.toml b/internal/templates/embed/picoclaw-manager/agent.toml index 562cbeb7..e1b19954 100644 --- a/internal/templates/embed/picoclaw-manager/agent.toml +++ b/internal/templates/embed/picoclaw-manager/agent.toml @@ -3,7 +3,7 @@ description = "Builtin PicoClaw manager template" role = "manager" runtime_kind = "picoclaw_sandbox" updated_at = "2026-05-27T00:00:00Z" -version = "0.1.2" +version = "0.1.3" [image] -ref = "opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw-manager:0.1.2" +ref = "opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw-manager:0.1.3" diff --git a/internal/templates/embed/picoclaw-worker/Dockerfile b/internal/templates/embed/picoclaw-worker/Dockerfile index da409c6c..25414b6f 100644 --- a/internal/templates/embed/picoclaw-worker/Dockerfile +++ b/internal/templates/embed/picoclaw-worker/Dockerfile @@ -12,7 +12,7 @@ # -t picoclaw-worker:local . # Default upstream picoclaw base image (keep in sync with picoclaw-manager/Dockerfile). -ARG PICOCLAW_IMAGE=opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw:2026.6.8 +ARG PICOCLAW_IMAGE=opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw:2026.6.10 FROM ${PICOCLAW_IMAGE} diff --git a/internal/templates/embed/picoclaw-worker/agent.toml b/internal/templates/embed/picoclaw-worker/agent.toml index 7c68dbf6..ec808322 100644 --- a/internal/templates/embed/picoclaw-worker/agent.toml +++ b/internal/templates/embed/picoclaw-worker/agent.toml @@ -3,7 +3,7 @@ description = "Builtin PicoClaw worker template" role = "worker" runtime_kind = "picoclaw_sandbox" updated_at = "2026-05-27T00:00:00Z" -version = "0.1.1" +version = "0.1.2" [image] -ref = "opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw-worker:0.1.1" +ref = "opencsg-registry.cn-beijing.cr.aliyuncs.com/opencsghq/picoclaw-worker:0.1.2" diff --git a/scripts/build-docker-embed-images.sh b/scripts/build-docker-embed-images.sh index 13151e28..255a179f 100755 --- a/scripts/build-docker-embed-images.sh +++ b/scripts/build-docker-embed-images.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash # Build container images for embed templates that include a Dockerfile. -# Image tag defaults to agent.toml version (after bump-docker-embed-version.sh). +# Image tag defaults to agent.toml version. Build selection is independent from bump: +# - cmd/csgclaw-cli/ changed vs HEAD -> build all templates +# - template Dockerfile changed vs HEAD -> build that template +# - PICOCLAW_BASE_IMAGE / OPENCLAW_BASE_IMAGE set -> build matching family +# Set DOCKER_EMBED_FORCE_BUILD=1 to build all templates regardless. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -87,11 +91,20 @@ build_one() { } chmod +x "${LIST_SCRIPT}" +built_any=false if [ "$#" -eq 0 ]; then while IFS= read -r name; do [ -z "${name}" ] && continue - build_one "${name}" + if should_build_docker_embed_image "${name}"; then + build_one "${name}" + built_any=true + else + echo "skip docker build ${name}: image inputs unchanged (set DOCKER_EMBED_FORCE_BUILD=1 to force)" + fi done < <("${LIST_SCRIPT}") + if [ "${built_any}" = false ]; then + echo "skip docker embed builds: no templates with changed image inputs" + fi else for name in "$@"; do build_one "${name}" diff --git a/scripts/bump-docker-embed-version.sh b/scripts/bump-docker-embed-version.sh index 91aef21d..42928291 100755 --- a/scripts/bump-docker-embed-version.sh +++ b/scripts/bump-docker-embed-version.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash # Bump agent.toml version (last segment) and sync [image].ref for docker embed templates. +# Bumps at most once vs git HEAD per image-input change (cmd/csgclaw-cli/ or template Dockerfile). +# Workspace changes do not bump. Independent from docker build. Set DOCKER_EMBED_FORCE_BUMP=1 to always increment. # # Usage: # ./scripts/bump-docker-embed-version.sh # all docker templates diff --git a/scripts/docker-embed-agent-toml-common.sh b/scripts/docker-embed-agent-toml-common.sh index f0cbe9f3..ee216693 100755 --- a/scripts/docker-embed-agent-toml-common.sh +++ b/scripts/docker-embed-agent-toml-common.sh @@ -110,6 +110,179 @@ increment_version_last_segment() { printf '%s.%s' "${prefix}" "$((last + 1))" } +docker_embed_git_available() { + local root + root="$(docker_embed_root)" + git -C "${root}" rev-parse --is-inside-work-tree >/dev/null 2>&1 +} + +docker_embed_baseline_version() { + local template="$1" + local root manifest_rel tmp version + + root="$(docker_embed_root)" + manifest_rel="internal/templates/embed/${template}/agent.toml" + if ! docker_embed_git_available; then + return 1 + fi + if ! git -C "${root}" cat-file -e "HEAD:${manifest_rel}" 2>/dev/null; then + return 1 + fi + + tmp="$(mktemp)" + git -C "${root}" show "HEAD:${manifest_rel}" > "${tmp}" + version="$(read_agent_toml_version "${tmp}")" + rm -f "${tmp}" + if [ -z "${version}" ]; then + return 1 + fi + printf '%s' "${version}" +} + +# Prints -1 when left < right, 0 when equal, 1 when left > right. +embed_template_version_compare() { + local left="$1" + local right="$2" + local -a left_parts right_parts + local i max len left_val right_val + + if [ "${left}" = "${right}" ]; then + printf '%s' "0" + return 0 + fi + + IFS=. read -r -a left_parts <<< "${left}" + IFS=. read -r -a right_parts <<< "${right}" + len="${#left_parts[@]}" + if [ "${#right_parts[@]}" -gt "${len}" ]; then + len="${#right_parts[@]}" + fi + + for ((i = 0; i < len; i++)); do + left_val=$((10#${left_parts[i]:-0})) + right_val=$((10#${right_parts[i]:-0})) + if ((left_val > right_val)); then + printf '%s' "1" + return 0 + fi + if ((left_val < right_val)); then + printf '%s' "-1" + return 0 + fi + done + + printf '%s' "0" +} + +# Exit 0 when csgclaw-cli sources differ from HEAD (shared docker embed input). +docker_embed_cli_inputs_changed_vs_head() { + local root + + root="$(docker_embed_root)" + if ! docker_embed_git_available; then + return 0 + fi + if [ -n "$(git -C "${root}" status --porcelain -- cmd/csgclaw-cli/ 2>/dev/null || true)" ]; then + return 0 + fi + return 1 +} + +# Exit 0 when the template Dockerfile differs from HEAD (includes untracked). +docker_embed_dockerfile_changed_vs_head() { + local template="$1" + local root dockerfile_rel + + root="$(docker_embed_root)" + dockerfile_rel="internal/templates/embed/${template}/Dockerfile" + if ! docker_embed_git_available; then + return 0 + fi + if [ -n "$(git -C "${root}" status --porcelain -- "${dockerfile_rel}" 2>/dev/null || true)" ]; then + return 0 + fi + return 1 +} + +# Exit 0 when inputs that affect the sandbox image changed relative to git HEAD. +# Workspace and agent.toml metadata are excluded (they ship via go:embed, not the image). +docker_embed_image_inputs_changed_vs_head() { + local template="$1" + + if docker_embed_cli_inputs_changed_vs_head; then + return 0 + fi + if docker_embed_dockerfile_changed_vs_head "${template}"; then + return 0 + fi + return 1 +} + +# Exit 0 when this template's image should be docker-built locally. +# Independent from version bump: repeated builds are allowed while version stays put. +should_build_docker_embed_image() { + local template="$1" + + if [ "${DOCKER_EMBED_FORCE_BUILD:-}" = "1" ]; then + return 0 + fi + if ! docker_embed_git_available; then + return 0 + fi + if docker_embed_cli_inputs_changed_vs_head; then + return 0 + fi + if docker_embed_dockerfile_changed_vs_head "${template}"; then + return 0 + fi + case "${template}" in + picoclaw-*) + if [ -n "${PICOCLAW_BASE_IMAGE:-}" ]; then + return 0 + fi + ;; + openclaw-*) + if [ -n "${OPENCLAW_BASE_IMAGE:-}" ]; then + return 0 + fi + ;; + esac + return 1 +} + +# Exit 0 when version bump should be skipped (sync-only path). +should_skip_docker_embed_bump() { + local template="$1" + local manifest current baseline order + + if [ "${DOCKER_EMBED_FORCE_BUMP:-}" = "1" ]; then + return 1 + fi + if ! docker_embed_git_available; then + return 1 + fi + + manifest="$(docker_embed_manifest_path "${template}")" + current="$(read_agent_toml_version "${manifest}")" + baseline="$(docker_embed_baseline_version "${template}" || true)" + if [ -z "${baseline}" ]; then + return 1 + fi + + if ! docker_embed_image_inputs_changed_vs_head "${template}"; then + return 0 + fi + + order="$(embed_template_version_compare "${current}" "${baseline}")" + if [ "${order}" -gt 0 ]; then + return 0 + fi + if [ "${order}" -eq 0 ]; then + return 1 + fi + return 0 +} + embed_image_ref_env_key() { local template="$1" local key="EMBED_IMAGE_REF_${template}" @@ -283,6 +456,15 @@ bump_agent_toml_version_and_ref() { fi current="$(read_agent_toml_version "${manifest}")" + if should_skip_docker_embed_bump "${template}"; then + if ! docker_embed_manifest_is_current "${template}"; then + sync_agent_toml_version_and_ref "${template}" + return 0 + fi + echo "skip bump ${manifest}: image inputs unchanged or already bumped (version=${current})" + return 0 + fi + if [ -z "${current}" ]; then next="0.1.0" else diff --git a/scripts/docker-embed-build_test.sh b/scripts/docker-embed-build_test.sh new file mode 100755 index 00000000..3b0d3730 --- /dev/null +++ b/scripts/docker-embed-build_test.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Tests for selective docker embed image build selection. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMMON="${ROOT}/scripts/docker-embed-agent-toml-common.sh" +TEMPLATE="test-embed" +OTHER="other-embed" + +# shellcheck source=scripts/docker-embed-agent-toml-common.sh +. "${COMMON}" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +setup_fixture() { + FIXTURE="$(mktemp -d)" + export DOCKER_EMBED_ROOT="${FIXTURE}" + mkdir -p "${FIXTURE}/internal/templates/embed/${TEMPLATE}" + mkdir -p "${FIXTURE}/internal/templates/embed/${OTHER}" + mkdir -p "${FIXTURE}/cmd/csgclaw-cli" + echo 'FROM scratch' > "${FIXTURE}/internal/templates/embed/${TEMPLATE}/Dockerfile" + echo 'FROM scratch' > "${FIXTURE}/internal/templates/embed/${OTHER}/Dockerfile" + echo 'package main; func main() {}' > "${FIXTURE}/cmd/csgclaw-cli/main.go" + git -C "${FIXTURE}" init -q + git -C "${FIXTURE}" config user.email "test@example.com" + git -C "${FIXTURE}" config user.name "Test" + git -C "${FIXTURE}" add -A + git -C "${FIXTURE}" commit -q -m "baseline" +} + +cleanup_fixture() { + if [ -n "${FIXTURE:-}" ] && [ -d "${FIXTURE}" ]; then + rm -rf "${FIXTURE}" + fi +} + +assert_build() { + local template="$1" want="$2" msg="$3" + local got=no + if should_build_docker_embed_image "${template}"; then + got=yes + fi + if [ "${got}" != "${want}" ]; then + fail "${msg}: got '${got}', want '${want}'" + fi +} + +test_clean_tree_skips_build() { + setup_fixture + assert_build "${TEMPLATE}" no "clean tree" + assert_build "${OTHER}" no "clean tree other template" + cleanup_fixture +} + +test_dockerfile_change_builds_one() { + setup_fixture + echo '# tweak' >> "${FIXTURE}/internal/templates/embed/${TEMPLATE}/Dockerfile" + assert_build "${TEMPLATE}" yes "dockerfile changed" + assert_build "${OTHER}" no "other template unchanged" + cleanup_fixture +} + +test_cli_change_builds_all() { + setup_fixture + echo '// tweak' >> "${FIXTURE}/cmd/csgclaw-cli/main.go" + assert_build "${TEMPLATE}" yes "cli changed" + assert_build "${OTHER}" yes "cli changed other template" + cleanup_fixture +} + +test_workspace_change_skips_build() { + setup_fixture + mkdir -p "${FIXTURE}/internal/templates/embed/${TEMPLATE}/workspace" + echo 'note' > "${FIXTURE}/internal/templates/embed/${TEMPLATE}/workspace/NOTE.md" + assert_build "${TEMPLATE}" no "workspace only" + cleanup_fixture +} + +test_force_build() { + setup_fixture + export DOCKER_EMBED_FORCE_BUILD=1 + assert_build "${TEMPLATE}" yes "force build" + unset DOCKER_EMBED_FORCE_BUILD + cleanup_fixture +} + +main() { + test_clean_tree_skips_build + test_dockerfile_change_builds_one + test_cli_change_builds_all + test_workspace_change_skips_build + test_force_build + echo "OK: docker embed build selection tests passed" +} + +main "$@" diff --git a/scripts/docker-embed-bump_test.sh b/scripts/docker-embed-bump_test.sh new file mode 100755 index 00000000..4f4e2e5a --- /dev/null +++ b/scripts/docker-embed-bump_test.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# Tests for conditional docker embed version bump (image inputs only). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMMON="${ROOT}/scripts/docker-embed-agent-toml-common.sh" +TEMPLATE="test-embed" +ACR_REGISTRY="registry.example.com" + +# shellcheck source=scripts/docker-embed-agent-toml-common.sh +. "${COMMON}" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_eq() { + local got="$1" want="$2" msg="$3" + if [ "${got}" != "${want}" ]; then + fail "${msg}: got '${got}', want '${want}'" + fi +} + +setup_fixture() { + FIXTURE="$(mktemp -d)" + export DOCKER_EMBED_ROOT="${FIXTURE}" + mkdir -p "${FIXTURE}/internal/templates/embed/${TEMPLATE}/workspace" + mkdir -p "${FIXTURE}/cmd/csgclaw-cli" + cat > "${FIXTURE}/internal/templates/embed/${TEMPLATE}/Dockerfile" <<'EOF' +FROM scratch +EOF + cat > "${FIXTURE}/cmd/csgclaw-cli/main.go" <<'EOF' +package main +func main() {} +EOF + git -C "${FIXTURE}" init -q + git -C "${FIXTURE}" config user.email "test@example.com" + git -C "${FIXTURE}" config user.name "Test" +} + +write_agent_toml() { + local version="$1" + local ref="$2" + cat > "${FIXTURE}/internal/templates/embed/${TEMPLATE}/agent.toml" < "${FIXTURE}/internal/templates/embed/${TEMPLATE}/workspace/NOTE.md" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.3" \ + "workspace-only change should not bump image version" + cleanup_fixture +} + +test_dockerfile_change_bumps_once() { + setup_fixture + write_agent_toml "0.1.3" "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:0.1.3" + commit_all "baseline" + echo "# tweak" >> "${FIXTURE}/internal/templates/embed/${TEMPLATE}/Dockerfile" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.4" \ + "first bump with Dockerfile change" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.4" \ + "second bump should stay at 0.1.4" + cleanup_fixture +} + +test_commit_then_dockerfile_change_bumps_again() { + setup_fixture + write_agent_toml "0.1.3" "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:0.1.3" + commit_all "baseline" + echo "# tweak" >> "${FIXTURE}/internal/templates/embed/${TEMPLATE}/Dockerfile" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.4" \ + "initial bump" + commit_all "bumped" + echo "# more" >> "${FIXTURE}/internal/templates/embed/${TEMPLATE}/Dockerfile" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.5" \ + "bump after commit and new Dockerfile change" + cleanup_fixture +} + +test_version_ref_only_change_skips_bump() { + setup_fixture + write_agent_toml "0.1.3" "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:0.1.3" + commit_all "baseline" + write_agent_toml "0.1.4" "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:0.1.4" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.4" \ + "version/ref-only change should not bump again" + cleanup_fixture +} + +test_cli_change_bumps_once() { + setup_fixture + write_agent_toml "0.1.3" "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:0.1.3" + commit_all "baseline" + echo "// change" >> "${FIXTURE}/cmd/csgclaw-cli/main.go" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.4" \ + "cli change should bump" + run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.4" \ + "second cli bump should stay at 0.1.4" + cleanup_fixture +} + +test_force_bump() { + setup_fixture + write_agent_toml "0.1.3" "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:0.1.3" + commit_all "baseline" + DOCKER_EMBED_FORCE_BUMP=1 run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.4" \ + "force bump on clean tree" + DOCKER_EMBED_FORCE_BUMP=1 run_bump + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.5" \ + "force bump again" + cleanup_fixture +} + +test_no_changes_syncs_ref() { + setup_fixture + write_agent_toml "0.1.3" "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:wrong-tag" + commit_all "baseline" + run_bump + assert_eq "$(read_agent_toml_image_ref "$(docker_embed_manifest_path "${TEMPLATE}")")" \ + "${ACR_REGISTRY}/opencsghq/${TEMPLATE}:0.1.3" \ + "out-of-sync ref should be synced without bump" + assert_eq "$(read_agent_toml_version "$(docker_embed_manifest_path "${TEMPLATE}")")" "0.1.3" \ + "version unchanged during sync" + cleanup_fixture +} + +main() { + test_no_changes_skips_bump + test_workspace_change_skips_bump + test_dockerfile_change_bumps_once + test_commit_then_dockerfile_change_bumps_again + test_version_ref_only_change_skips_bump + test_cli_change_bumps_once + test_force_bump + test_no_changes_syncs_ref + echo "OK: docker embed bump tests passed" +} + +main "$@"