diff --git a/doc/changelog/CHANGELOG.md b/doc/changelog/CHANGELOG.md index 5106872..1e66bec 100644 --- a/doc/changelog/CHANGELOG.md +++ b/doc/changelog/CHANGELOG.md @@ -8,8 +8,12 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Changed -- Upgrade `template/` subtree to [v0.9.11](https://github.com/ycpss91255-docker/template/releases/tag/v0.9.11). Brings arm64-native CI (multi-arch matrix on `ubuntu-24.04-arm`), Jetson auto-detect for `[deploy] runtime` / `[build] network` (no more manual `network = host` workaround on Jetson), `_sanitize_lang` i18n, `_lib.sh` / `i18n.sh` dedupe, and `upgrade.sh` destructive fast-forward guards. -- Pin `main.yaml` reusable workflows to [`@v0.9.13`](https://github.com/ycpss91255-docker/template/releases/tag/v0.9.13) — picks up the GHCR test-tools migration (D plan, closes template #106) and the associated `build-worker.yaml` fix. Required because this PR's Dockerfile adopts the `ARG TEST_TOOLS_IMAGE` pattern that v0.9.13 introduced. +- Upgrade `template/` subtree to [v0.10.0-rc2](https://github.com/ycpss91255-docker/template/releases/tag/v0.10.0-rc2). Bundles: + - **Compose `runtime` service** auto-emitted by `setup.sh` when Dockerfile declares `FROM … AS runtime` (closes template #108). `./run.sh -t runtime` no longer errors with "no such service". + - **`run.sh` arg realignment** (closes template #118, BREAKING): target is now `-t/--target` flag (default `devel`); positional args become CMD passthrough (empty → Dockerfile CMD, non-empty → override); `-d + cmd` → exit 2 error. `./run.sh` bare unchanged (devel bash). Migration inside this repo: `./run.sh runtime` now written as `./run.sh -t runtime` (auto-runs `parameter_bridge` attached). `./run.sh -t runtime bash` drops into runtime shell for debug. + - **arm64 test-tools** hotfix — `Dockerfile.test-tools` `ARG TARGETARCH=amd64` default used to shadow BuildKit's auto-inject (moby/buildkit#3403), so `:v0.9.13` / `:v0.10.0-rc1` GHCR arm64 variants shipped x86_64 shellcheck / hadolint. v0.10.0-rc2 drops the default; arm64 binaries are now genuinely aarch64 (verified via `docker cp` + `file`). + - Intermediate releases (v0.9.11/12/13, v0.10.0-rc1) are superseded; this PR pins `main.yaml` directly to `@v0.10.0-rc2`. +- Pin `main.yaml` reusable workflows to [`@v0.10.0-rc2`](https://github.com/ycpss91255-docker/template/releases/tag/v0.10.0-rc2). - Rebuild `devel` stage from `ros:foxy-ros-base-focal` (multi-arch) plus the ROS 1 snapshot apt repo instead of the amd64-only `osrf/ros:foxy-ros1-bridge`. Enables Jetson (arm64) support. - `ENV ROS1_DISTRO=noetic` / `ENV ROS2_DISTRO=foxy` now baked into the image so downstream scripts can reference the distro names without hardcoding. - Test stage lint target uses `COPY script/*.sh /lint/` (glob) to pick up new scripts automatically. diff --git a/template/.github/dependabot.yml b/template/.github/dependabot.yml new file mode 100644 index 0000000..0a24199 --- /dev/null +++ b/template/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 diff --git a/template/.github/workflows/build-worker.yaml b/template/.github/workflows/build-worker.yaml index aa3a30c..50e4213 100644 --- a/template/.github/workflows/build-worker.yaml +++ b/template/.github/workflows/build-worker.yaml @@ -116,17 +116,7 @@ jobs: fi - name: Set up Docker Buildx - # docker-container driver is required even on single-platform - # builds now: the downstream Dockerfile's `test` stage - # references `test-tools:local` via `COPY --from=`, and with - # docker-container the tag built in the previous buildx step - # stays in the same builder's internal store, which subsequent - # build-push-action steps can resolve. The legacy `docker` - # driver stores images in the host daemon, which isn't - # accessible from a matrix job's buildx container. uses: docker/setup-buildx-action@v3 - with: - driver: docker-container - name: Generate .env run: | @@ -143,21 +133,30 @@ jobs: EOF mkdir -p /tmp/workspace - - name: Build test-tools image - # Must use buildx (not plain `docker build`) so `test-tools:local` - # lands in buildx's internal image store — subsequent build - # steps on the same builder resolve `COPY --from=test-tools:local` - # from there. docker-container's builder container doesn't see - # host daemon images. - uses: docker/build-push-action@v6 - with: - context: . - file: template/dockerfile/Dockerfile.test-tools - tags: test-tools:local - platforms: ${{ matrix.platform }} - push: false + - name: Resolve template version for test-tools image + # The downstream repo's main.yaml pins this workflow via + # uses: ycpss91255-docker/template/.github/workflows/build-worker.yaml@vX.Y.Z + # GITHUB_WORKFLOW_REF exposes that full ref at runtime. Parse + # out the tag so we can pin the test-tools GHCR image to the + # matching template release (prevents version skew between the + # workflow logic and the test-tools contract it assumes). + id: tplver + shell: bash + run: | + ref="${GITHUB_WORKFLOW_REF##*@}" + case "${ref}" in + refs/tags/v*) ver="${ref##refs/tags/}" ;; + *) ver="latest" ;; + esac + printf 'image=ghcr.io/ycpss91255-docker/test-tools:%s\n' "${ver}" >> "${GITHUB_OUTPUT}" - name: Build test stage (includes smoke tests) + # The downstream Dockerfile consumes TEST_TOOLS_IMAGE as the + # source of `COPY --from=${TEST_TOOLS_IMAGE}`; buildx auto-pulls + # the image from GHCR on `COPY --from=` when the tag + # is not present locally, so no explicit docker-pull step is + # needed here. Public visibility of the GHCR package means + # anonymous pull works, keeping the build-args simple. uses: docker/build-push-action@v6 with: context: . @@ -170,6 +169,7 @@ jobs: UID=1000 GID=1000 HARDWARE=${{ matrix.hardware }} + TEST_TOOLS_IMAGE=${{ steps.tplver.outputs.image }} ${{ inputs.build_args }} - name: Build devel stage diff --git a/template/.github/workflows/release-test-tools.yaml b/template/.github/workflows/release-test-tools.yaml new file mode 100644 index 0000000..b17bd6e --- /dev/null +++ b/template/.github/workflows/release-test-tools.yaml @@ -0,0 +1,73 @@ +name: Release test-tools image to GHCR + +# Multi-arch (linux/amd64 + linux/arm64) build of +# template/dockerfile/Dockerfile.test-tools, pushed to +# ghcr.io/ycpss91255-docker/test-tools tagged with the release version +# and :latest. Downstream repo Dockerfiles consume this via +# `ARG TEST_TOOLS_IMAGE=ghcr.io/ycpss91255-docker/test-tools:`, +# avoiding the cross-step image-store isolation that sank the previous +# `test-tools:local` approach (v0.9.11 pre-D migration). +# +# Triggers: +# push of a vX.Y.Z tag -> pushes : and :latest +# workflow_dispatch -> manual bootstrap; pushes only :latest +# +# Needs packages: write to push to GHCR under the org namespace. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + release-test-tools: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve tags + id: tags + shell: bash + run: | + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + ver="${GITHUB_REF##refs/tags/}" + printf 'tags=ghcr.io/ycpss91255-docker/test-tools:%s,ghcr.io/ycpss91255-docker/test-tools:latest\n' "${ver}" >> "${GITHUB_OUTPUT}" + else + printf 'tags=ghcr.io/ycpss91255-docker/test-tools:latest\n' >> "${GITHUB_OUTPUT}" + fi + + - name: Set up QEMU + # Needed for the arm64 leg; amd64 alone could skip it, but the + # QEMU setup is a no-op on native arch so we keep the workflow + # simple with a single builder. + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multi-arch test-tools image + # This workflow runs inside the template repo, not a downstream + # consumer, so the Dockerfile lives at `dockerfile/...`, not + # `template/dockerfile/...` (the latter is only the subtree path + # downstream repos see and is what build-worker.yaml uses since + # that workflow executes in the downstream's checkout). + uses: docker/build-push-action@v6 + with: + context: . + file: dockerfile/Dockerfile.test-tools + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.tags.outputs.tags }} + push: true diff --git a/template/.version b/template/.version index 9781ff2..4b03ff2 100644 --- a/template/.version +++ b/template/.version @@ -1 +1 @@ -v0.9.11 +v0.10.0-rc2 diff --git a/template/README.md b/template/README.md index 4fd2b77..9aaaa75 100644 --- a/template/README.md +++ b/template/README.md @@ -156,9 +156,16 @@ Notes: - `test` is always built from `devel`, so runtime assertions inside `test/smoke/_env.bats` see the same binaries / files a user would find after `docker run ... :devel`. -- `Dockerfile.test-tools` builds a separate `test-tools:local` image (not - part of the stage chain above) that the `test` stage copies bats / - shellcheck / hadolint binaries from via `COPY --from=test-tools:local`. +- `Dockerfile.test-tools` builds the lint/test tool bundle (bats + shellcheck + + hadolint). The downstream `test` stage consumes it through an `ARG + TEST_TOOLS_IMAGE` build arg — defaults to `test-tools:local` (matches the + local `./build.sh` flow that builds `Dockerfile.test-tools` into the host + Docker daemon). CI overrides it to + `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z` (pre-built multi-arch image + pushed by `.github/workflows/release-test-tools.yaml` on every tag) so + buildx pulls the arch-correct binaries over the wire instead of rebuilding + them per run, and sidesteps the cross-step image-store isolation that + `docker-container` buildx drivers enforce. ### Smoke test helpers (for downstream repos) @@ -289,6 +296,31 @@ make upgrade ./template/upgrade.sh v0.3.0 ``` +`upgrade.sh` handles the full cycle in one go: `git subtree pull --squash`, +post-pull integrity check (rolls back on destructive FF), `./template/init.sh` +to resync root symlinks, and `sed` of `.github/workflows/main.yaml`'s +`build-worker.yaml@vX.Y.Z` / `release-worker.yaml@vX.Y.Z` references. Don't +subtree pull by hand — the sed + init steps are easy to forget. + +#### Automated version bumps (optional) + +Downstream repos can let Dependabot open PRs whenever a new `template` tag +ships. Add `.github/dependabot.yml`: + +```yaml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` + +Dependabot notices the `uses: ycpss91255-docker/template/...@vX.Y.Z` refs in +`main.yaml`, compares against the template's latest tag, and files a PR. You +still run `./template/upgrade.sh vX.Y.Z` locally to sync the subtree itself — +Dependabot only bumps the workflow refs. + ## CI Reusable Workflows Repos replace local `build-worker.yaml` / `release-worker.yaml` with calls to this repo's reusable workflows: diff --git a/template/doc/changelog/CHANGELOG.md b/template/doc/changelog/CHANGELOG.md index 96587b0..d8d7c49 100644 --- a/template/doc/changelog/CHANGELOG.md +++ b/template/doc/changelog/CHANGELOG.md @@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v0.10.0-rc2] - 2026-04-24 + +Second release candidate. Ships the arm64 test-tools hotfix that v0.10.0-rc1 / v0.9.13 both missed — **strongly recommended** over rc1 for any downstream repo enabling the arm64 build matrix. + +### Fixed +- **`Dockerfile.test-tools` `ARG TARGETARCH=amd64` default shadowed BuildKit's per-platform auto-inject** ([moby/buildkit#3403](https://github.com/moby/buildkit/issues/3403)). Every multi-arch build published via `release-test-tools.yaml` (v0.9.13, v0.10.0-rc1) therefore fell back to `amd64` and shipped x86_64 `shellcheck` / `hadolint` binaries inside the arm64 image variant. Symptom downstream: `shellcheck: Exec format error` on arm64 CI (ros1_bridge PR #27 first surfaced it). Fix: declare `ARG TARGETARCH` without default so BuildKit's injected value drives the `case` branch. Regression test added: `Dockerfile.test-tools ARG TARGETARCH has no default value`. Requires a new tag + `release-test-tools.yaml` re-run to reissue `:v0.10.0-rc2` + `:latest` on GHCR. + +## [v0.10.0-rc1] - 2026-04-24 + +Release candidate for v0.10.0. BREAKING: `run.sh` arg semantics realigned. +Validate on `ros1_bridge` (`./run.sh -t runtime` attaches to bridge logs, +`./run.sh -t runtime bash` drops into runtime shell) + at least one +GUI-using env repo before promoting to v0.10.0. + +### Added +- **`runtime` compose service auto-emission (closes #108)**. `setup.sh` now detects a `FROM AS runtime` stage in the sibling Dockerfile and emits a paired `runtime` service that `extends: { service: devel }` (inherits volumes / env / network / GPU / caps), overrides `build.target`, `image` (`:runtime` tag), `container_name` (`-runtime`), and flips `stdin_open: false` / `tty: false` for headless auto-run. Gated by `profiles: [runtime]` so plain `compose up` still scopes to `devel`; `compose run runtime` / `compose up runtime` (and `./run.sh -t runtime`) target it explicitly. Repos without an `AS runtime` stage get no emission (no broken service entry). + +### Changed +- **BREAKING: `./run.sh` arg semantics aligned with `docker run [cmd]` (closes #118).** + - Target is now the explicit `-t TARGET` / `--target TARGET` flag (default `devel`). + - Positional args after options are the CMD to run inside the container, mirroring `exec.sh`. Empty CMD → Dockerfile CMD runs (`devel` = `bash`, `runtime` = its auto-run service). Non-empty CMD → overrides Dockerfile CMD. + - `-d` + CMD → error (exit 2) with a pointer to `./exec.sh` for the detached-container cmd case; `-d` alone is unchanged (`compose up -d TARGET`). + - Migration: `./run.sh runtime` → `./run.sh -t runtime`. `./run.sh test` → `./run.sh -t test`. Plain `./run.sh` still drops into devel bash (unchanged UX). + +## [v0.9.13] - 2026-04-24 + +### Added +- **`.github/workflows/release-test-tools.yaml`** — on every tag push (and manual `workflow_dispatch`), builds multi-arch (amd64 + arm64) `Dockerfile.test-tools` and publishes to `ghcr.io/ycpss91255-docker/test-tools:` + `:latest`. First release triggered by this tag; package visibility should be set to public on first push so downstream Dockerfiles can pull anonymously. +- **`TEST_TOOLS_IMAGE` build-arg** in `Dockerfile.example` — defaults to `test-tools:local` (preserves the local `./build.sh` flow that builds `Dockerfile.test-tools` into the host daemon). Override in CI to `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z` so buildx pulls the arch-correct pre-built image over the wire. + +### Changed +- **BREAKING for downstream repos adopting v0.9.13+ workflows**: `build-worker.yaml` no longer builds `test-tools:local` in-job. Instead it parses the template version from `GITHUB_WORKFLOW_REF` and passes `TEST_TOOLS_IMAGE=ghcr.io/ycpss91255-docker/test-tools:` as a build-arg to the test stage. Downstream Dockerfiles must add `ARG TEST_TOOLS_IMAGE="test-tools:local"` + `FROM ${TEST_TOOLS_IMAGE} AS test-tools-stage` + `COPY --from=test-tools-stage` (the previous `COPY --from=test-tools:local` literal stops working once repos bump their `main.yaml` `@tag` to `v0.9.13`). Existing repos pinned to `@v0.9.12` or earlier remain unaffected until they upgrade. +- `Dockerfile.example` test stage restructured: new `FROM ${TEST_TOOLS_IMAGE} AS test-tools-stage` alias, 4 `COPY --from=test-tools:local` → `COPY --from=test-tools-stage`, top-level comment updated. + +### Fixed +- **CI `COPY --from=test-tools:local` no longer fails with `pull access denied` on downstream repos** (follow-up to v0.9.12 `load: true` attempt, which turned out not to share images between buildx steps — [docker/build-push-action#581](https://github.com/docker/build-push-action/issues/581)). GHCR-backed approach sidesteps the cross-step image-store isolation entirely. +- **`release-test-tools.yaml` Dockerfile path** — was wrongly written as `template/dockerfile/Dockerfile.test-tools` (the downstream subtree path); in the template repo itself the file is at `dockerfile/Dockerfile.test-tools`. Regression test added to assert no subtree-prefixed path leaks back in. + +## [v0.9.12] - 2026-04-24 + +### Added +- `.github/dependabot.yml` — weekly `github-actions` ecosystem scan so template's own consumed actions (`actions/checkout`, `docker/*`, etc.) stay current without manual audits. + +### Changed +- README "Updating" section (4 languages) clarifies that `./template/upgrade.sh` already automates subtree pull + integrity check + `init.sh` resync + `main.yaml` `@vX.Y.Z` sed; hand-rolling `git subtree pull` is discouraged since the sed + init steps are easy to forget. Adds a Dependabot snippet downstream repos can drop into their `.github/dependabot.yml` so template version bumps surface as PRs automatically (Dependabot handles workflow refs only; subtree still needs `upgrade.sh`). + +### Fixed +- **`build-worker.yaml` test-tools build now uses `load: true`.** Without it, `docker/build-push-action@v6` with `push: false` discards the built image, so subsequent `COPY --from=test-tools:local` in the downstream Dockerfile can't resolve the tag. buildx then falls back to registry pull → `docker.io/library/test-tools:local: pull access denied` → CI fail. Surfaced when `ros1_bridge` became the first downstream repo to adopt `test-tools:local` post-v0.9.11 (issue #106 migration PR). Added `test/unit/template_spec.bats` regression test asserting the `load: true` flag is present. + ## [v0.9.11] - 2026-04-24 ### Fixed diff --git a/template/doc/readme/README.ja.md b/template/doc/readme/README.ja.md index 6e9112c..c322961 100644 --- a/template/doc/readme/README.ja.md +++ b/template/doc/readme/README.ja.md @@ -156,10 +156,7 @@ flowchart LR - `test` は常に `devel` を継承するため、`test/smoke/_env.bats` の runtime assertion が確認するバイナリやファイルは、ユーザーが `docker run ... :devel` で目にするものと一致します。 -- `Dockerfile.test-tools` は別途 `test-tools:local` image をビルドし - (上記ステージ連鎖には含まれません)、`test` ステージが - `COPY --from=test-tools:local` で bats / shellcheck / hadolint - バイナリを取り込みます。 +- `Dockerfile.test-tools` は lint/test ツールセット(bats + shellcheck + hadolint)をビルドします。ダウンストリームの `test` ステージは `ARG TEST_TOOLS_IMAGE` build arg で参照します — デフォルト `test-tools:local`(ローカル `./build.sh` フロー、`Dockerfile.test-tools` を host Docker daemon に load)。CI では `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z`(`.github/workflows/release-test-tools.yaml` がタグ push ごとに publish するマルチアーキ image)で override し、buildx が registry からアーキ対応の bats / shellcheck / hadolint binary を直接 pull します。`docker-container` buildx driver の step 間 image store 分離問題を回避。 ### Smoke test ヘルパー(ダウンストリーム repo 用) @@ -299,6 +296,23 @@ make upgrade ./template/upgrade.sh v0.3.0 ``` +`upgrade.sh` は一度に完結します:`git subtree pull --squash`、post-pull 整合性チェック(destructive FF を検出したら自動 rollback)、`./template/init.sh` による root symlinks の再同期、そして `.github/workflows/main.yaml` 内の `build-worker.yaml@vX.Y.Z` / `release-worker.yaml@vX.Y.Z` を sed で更新します。手動で `git subtree pull` しないでください — sed と init の手順を忘れがちです。 + +#### 自動バージョン更新(任意) + +ダウンストリーム repo は、`template` の新しい tag が出るたびに Dependabot が PR を立てるよう設定できます。`.github/dependabot.yml` を追加します: + +```yaml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` + +Dependabot は `main.yaml` 内の `uses: ycpss91255-docker/template/...@vX.Y.Z` ref を見て、template の最新 tag と照合して PR を出します。subtree 自体は引き続きローカルで `./template/upgrade.sh vX.Y.Z` を実行する必要があります — Dependabot が扱うのは workflow ref のみです。 + ## CI Reusable Workflows 各 repo のローカル `build-worker.yaml` / `release-worker.yaml` を、本 repo の reusable workflows 呼び出しに置き換えます: diff --git a/template/doc/readme/README.zh-CN.md b/template/doc/readme/README.zh-CN.md index 1fd3bbc..eae5195 100644 --- a/template/doc/readme/README.zh-CN.md +++ b/template/doc/readme/README.zh-CN.md @@ -155,9 +155,7 @@ flowchart LR - `test` 总是从 `devel` 继承,所以 `test/smoke/_env.bats` 中的 runtime assertion 所看到的二进制与文件,就是用户 `docker run ... :devel` 后会看到的内容。 -- `Dockerfile.test-tools` 另外构建一个 `test-tools:local` image(不在 - 上面的阶段链中),`test` 阶段通过 `COPY --from=test-tools:local` - 把 bats / shellcheck / hadolint 二进制拉进来。 +- `Dockerfile.test-tools` 构建 lint/test 工具集(bats + shellcheck + hadolint)。下游 `test` 阶段通过 `ARG TEST_TOOLS_IMAGE` build arg 引用 — 默认 `test-tools:local`(对应本地 `./build.sh` 流程,把 `Dockerfile.test-tools` 构建到 host Docker daemon)。CI 则覆盖成 `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z`(由 `.github/workflows/release-test-tools.yaml` 在每次 tag 推的预构建 multi-arch image),buildx 直接从 registry 拉对应架构的 bats / shellcheck / hadolint binary,避开 `docker-container` buildx driver 跨 step 不共享 image store 的问题。 ### Smoke test helpers(供下游 repo 使用) @@ -288,6 +286,23 @@ make upgrade ./template/upgrade.sh v0.3.0 ``` +`upgrade.sh` 一次完成:`git subtree pull --squash`、post-pull 完整性检查(检测到 destructive FF 会自动 rollback)、`./template/init.sh` 重整 root symlinks、以及 sed `.github/workflows/main.yaml` 里的 `build-worker.yaml@vX.Y.Z` / `release-worker.yaml@vX.Y.Z`。不要手动 `git subtree pull` — sed 与 init 步骤容易漏掉。 + +#### 自动升版(可选) + +下游 repo 可以让 Dependabot 在 `template` 出新 tag 时自动开 PR。加入 `.github/dependabot.yml`: + +```yaml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` + +Dependabot 会读 `main.yaml` 里的 `uses: ycpss91255-docker/template/...@vX.Y.Z` ref,比对 template 最新 tag 后开 PR。subtree 本身仍需在本地跑 `./template/upgrade.sh vX.Y.Z` — Dependabot 只负责 workflow ref。 + ## CI Reusable Workflows 各 repo 将本地的 `build-worker.yaml` / `release-worker.yaml` 替换为调用此 repo 的 reusable workflows: diff --git a/template/doc/readme/README.zh-TW.md b/template/doc/readme/README.zh-TW.md index faca56c..58c3e4a 100644 --- a/template/doc/readme/README.zh-TW.md +++ b/template/doc/readme/README.zh-TW.md @@ -155,9 +155,7 @@ flowchart LR - `test` 永遠從 `devel` 繼承,所以 `test/smoke/_env.bats` 裡的 runtime assertion 所看到的二進位與檔案,就是使用者 `docker run ... :devel` 後會看到的內容。 -- `Dockerfile.test-tools` 另外建置一個 `test-tools:local` image(不在 - 上面的階段鏈中),`test` 階段透過 `COPY --from=test-tools:local` - 把 bats / shellcheck / hadolint 二進位拉進來。 +- `Dockerfile.test-tools` 建置 lint/test 工具集(bats + shellcheck + hadolint)。下游 `test` 階段透過 `ARG TEST_TOOLS_IMAGE` build arg 引用 — 預設 `test-tools:local`(對應本地 `./build.sh` 流程,把 `Dockerfile.test-tools` 建到 host Docker daemon)。CI 則覆寫成 `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z`(由 `.github/workflows/release-test-tools.yaml` 在每次 tag 推的預建 multi-arch image),buildx 直接從 registry 拉對應架構的 bats / shellcheck / hadolint binary,避開 `docker-container` buildx driver 跨 step 不共享 image store 的問題。 ### Smoke test helpers(供下游 repo 使用) @@ -288,6 +286,23 @@ make upgrade ./template/upgrade.sh v0.3.0 ``` +`upgrade.sh` 一次完成:`git subtree pull --squash`、post-pull 完整性檢查(偵測到 destructive FF 會自動 rollback)、`./template/init.sh` 重整 root symlinks、以及 sed `.github/workflows/main.yaml` 裡的 `build-worker.yaml@vX.Y.Z` / `release-worker.yaml@vX.Y.Z`。不要手動 `git subtree pull` — sed 與 init 步驟很容易漏掉。 + +#### 自動升版(選用) + +下游 repo 可以讓 Dependabot 在 `template` 出新 tag 時自動開 PR。加入 `.github/dependabot.yml`: + +```yaml +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +``` + +Dependabot 會讀 `main.yaml` 裡的 `uses: ycpss91255-docker/template/...@vX.Y.Z` ref,比對 template 最新 tag 後開 PR。subtree 本身仍需在本地跑 `./template/upgrade.sh vX.Y.Z` — Dependabot 只負責 workflow ref。 + ## CI Reusable Workflows 各 repo 將本地的 `build-worker.yaml` / `release-worker.yaml` 替換為呼叫此 repo 的 reusable workflows: diff --git a/template/doc/test/TEST.md b/template/doc/test/TEST.md index d3efe4a..daca168 100644 --- a/template/doc/test/TEST.md +++ b/template/doc/test/TEST.md @@ -1,6 +1,6 @@ # TEST.md -Template self-tests: **654 tests** total (611 unit + 43 integration). +Template self-tests: **675 tests** total (632 unit + 43 integration). ## Test Files @@ -111,7 +111,7 @@ build invocation, and **runtime log-line i18n** (bootstrap / drift-regen / err_no_env messages translate in all four languages via the local `_msg()` table; English remains the default). -### test/unit/run_sh_spec.bats (30) +### test/unit/run_sh_spec.bats (33) Unit tests for `run.sh`. Mirrors the build_sh_spec.bats harness; `docker ps` reads from a controllable stub file so tests can simulate @@ -156,7 +156,7 @@ Chinese / Simplified Chinese / Japanese translations of the no-instances message, `--all` multi-project teardown loop, and fallback `_detect_lang` branches. -### test/unit/compose_gen_spec.bats (38) +### test/unit/compose_gen_spec.bats (45) Covers `generate_compose_yaml` conditional output: AUTO-GENERATED header, baseline workspace volume, network/ipc/privileged env-var @@ -180,8 +180,15 @@ conditional GPU deploy block + GUI env/volumes + extra volumes from | `extra volumes appended after baseline` | volumes list | | `empty extras => no extra mount lines` | empty list | | `with GUI+GPU+extras => all sections present` | fully loaded | +| `emits runtime service when Dockerfile has AS runtime` | #108 auto-emit | +| `skips runtime service when Dockerfile lacks AS runtime` | opt-out by absence | +| `skips runtime service when Dockerfile is absent` | no-Dockerfile guard | +| `runtime service extends devel and overrides target/image/tty/profile` | compose extends shape | +| `runtime service appears between devel and test blocks` | ordering | +| `runtime detection is robust against weird whitespace` | regex tolerance | +| `runtime detection ignores non-runtime stage names` | strict match | -### test/unit/template_spec.bats (105) +### test/unit/template_spec.bats (116) | Test | Description | |------|-------------| @@ -259,6 +266,7 @@ conditional GPU deploy block + GUI env/volumes + extra volumes from | `exec.sh --dry-run skips precheck and prints compose command` | dry-run e2e | | `script/docker/i18n.sh exists` | i18n module exists | | `Dockerfile.test-tools includes bats-mock` | bats-mock available in test image | +| `Dockerfile.test-tools ARG TARGETARCH has no default value (must not shadow BuildKit auto-inject)` | multi-arch build regression | | `i18n.sh defines _detect_lang function` | _detect_lang in i18n.sh | | `build.sh sources _lib.sh` | build.sh uses shared lib | | `run.sh sources _lib.sh` | run.sh uses shared lib | @@ -280,6 +288,16 @@ conditional GPU deploy block + GUI env/volumes + extra volumes from | `upgrade.sh --gen-conf delegates to init.sh --gen-conf` | Delegation | | `upgrade.sh --help mentions --gen-conf` | Help text | | `upgrade.sh updates main.yaml @tag without clobbering release-worker.yaml` | sed regression | +| `build-worker.yaml: no legacy in-job test-tools build step` | v0.9.13 GHCR migration | +| `build-worker.yaml: resolves template version from GITHUB_WORKFLOW_REF` | GHCR tag resolution | +| `build-worker.yaml: test build passes TEST_TOOLS_IMAGE build-arg` | build-arg wiring | +| `Dockerfile.example has ARG TEST_TOOLS_IMAGE with test-tools:local default` | ARG default | +| `Dockerfile.example FROM ${TEST_TOOLS_IMAGE} AS test-tools-stage` | named stage alias | +| `Dockerfile.example test stage copies from test-tools-stage, not test-tools:local` | stage rename migration | +| `release-test-tools.yaml exists and pushes to ghcr.io/ycpss91255-docker/test-tools` | GHCR publisher | +| `release-test-tools.yaml declares packages:write permission` | ghcr auth scope | +| `release-test-tools.yaml builds multi-arch (amd64 + arm64)` | arch coverage | +| `release-test-tools.yaml uses template-repo-local Dockerfile path` | no subtree path confusion | | `run.sh contains XDG_SESSION_TYPE check` | X11/Wayland branch | | `run.sh contains xhost +SI:localuser for wayland` | Wayland xhost | | `run.sh contains xhost +local: for X11` | X11 xhost | diff --git a/template/dockerfile/Dockerfile.example b/template/dockerfile/Dockerfile.example index 9de4ca5..7466629 100644 --- a/template/dockerfile/Dockerfile.example +++ b/template/dockerfile/Dockerfile.example @@ -1,7 +1,13 @@ # Dockerfile.example - Template for new Docker container repos # # Copy this file to your repo root as "Dockerfile" and customize. -# The test-tools image (ShellCheck, Hadolint, Bats) is pre-built by build.sh. +# The test-tools image (ShellCheck, Hadolint, Bats) is consumed via the +# TEST_TOOLS_IMAGE build arg. Default `test-tools:local` works for the +# local `./build.sh` flow (builds Dockerfile.test-tools into the host +# Docker daemon). CI overrides this to +# `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z` so buildx can pull the +# arch-correct pre-built image without the cross-step image-store +# isolation that broke the old test-tools:local CI pattern. # # Stages: # sys - User/group, locale, timezone @@ -11,6 +17,7 @@ # runtime - Minimal runtime image (optional) ARG BASE_IMAGE="ubuntu:24.04" +ARG TEST_TOOLS_IMAGE="test-tools:local" ############################## sys ############################## FROM ${BASE_IMAGE} AS sys @@ -120,13 +127,16 @@ ENTRYPOINT ["/entrypoint.sh"] CMD ["bash"] ############################## test ############################## +# Resolves to test-tools:local (local build.sh) or ghcr.io/.../test-tools:vX.Y.Z (CI). +FROM ${TEST_TOOLS_IMAGE} AS test-tools-stage + FROM devel AS test USER root -# Lint tools (from pre-built test-tools image) -COPY --from=test-tools:local /usr/local/bin/shellcheck /usr/local/bin/shellcheck -COPY --from=test-tools:local /usr/local/bin/hadolint /usr/local/bin/hadolint +# Lint tools (from pre-built test-tools image; see TEST_TOOLS_IMAGE at top) +COPY --from=test-tools-stage /usr/local/bin/shellcheck /usr/local/bin/shellcheck +COPY --from=test-tools-stage /usr/local/bin/hadolint /usr/local/bin/hadolint # Lint: ShellCheck (.sh) + Hadolint (Dockerfile) COPY .hadolint.yaml /lint/.hadolint.yaml @@ -146,9 +156,9 @@ COPY template/script/docker/_lib.sh \ RUN shellcheck -S warning /lint/*.sh RUN cd /lint && hadolint Dockerfile -# Bats (from pre-built test-tools image) -COPY --from=test-tools:local /opt/bats /opt/bats -COPY --from=test-tools:local /usr/lib/bats /usr/lib/bats +# Bats (from pre-built test-tools image; see TEST_TOOLS_IMAGE at top) +COPY --from=test-tools-stage /opt/bats /opt/bats +COPY --from=test-tools-stage /usr/lib/bats /usr/lib/bats RUN ln -sf /opt/bats/bin/bats /usr/local/bin/bats ENV BATS_LIB_PATH="/usr/lib/bats" diff --git a/template/dockerfile/Dockerfile.test-tools b/template/dockerfile/Dockerfile.test-tools index 1865833..a037496 100644 --- a/template/dockerfile/Dockerfile.test-tools +++ b/template/dockerfile/Dockerfile.test-tools @@ -20,10 +20,13 @@ RUN apk add --no-cache git && \ FROM alpine:latest AS lint-tools # TARGETARCH is auto-populated by BuildKit (docker 23+) with "amd64" / -# "arm64" / etc. The default handles the rare case where a legacy -# builder doesn't provide it; override via --build-arg TARGETARCH= -# if you build outside BuildKit. -ARG TARGETARCH=amd64 +# "arm64" / etc. Do NOT set a default — a default value shadows +# BuildKit's auto-inject (moby/buildkit#3403), which caused every +# multi-arch build of this file to fall back to amd64 and ship +# x86_64 shellcheck / hadolint inside the arm64 image variant +# (visible symptom: `shellcheck: Exec format error` on arm64 downstream +# CI). If invoked outside BuildKit, pass `--build-arg TARGETARCH=`. +ARG TARGETARCH RUN apk add --no-cache curl xz && \ case "${TARGETARCH}" in \ amd64) sc_arch="x86_64"; hd_arch="x86_64" ;; \ diff --git a/template/script/docker/run.sh b/template/script/docker/run.sh index 007fe14..c7cc7a8 100755 --- a/template/script/docker/run.sh +++ b/template/script/docker/run.sh @@ -58,74 +58,82 @@ usage() { case "${_LANG}" in zh-TW) cat >&2 <<'EOF' -用法: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] [--lang ] [TARGET] +用法: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] + [--lang ] [-t|--target TARGET] [CMD...] 選項: -h, --help 顯示此說明 - -d, --detach 背景執行(docker compose up -d) + -t, --target T Compose service 名稱(預設: devel;例: runtime) + -d, --detach 背景執行(docker compose up -d,不接受 CMD) -s, --setup 強制重跑 setup.sh 重新生成 .env + compose.yaml (預設:.env 不存在時自動 bootstrap;存在時僅印 drift warning) --dry-run 只印出將執行的 docker 指令,不實際執行 --instance NAME 啟動命名 instance(與預設並行,suffix=-NAME) --lang LANG 設定訊息語言(預設: en) -目標: - devel 開發環境(預設) - runtime 最小化 runtime +CMD: 啟動容器後要執行的指令,對齊 `docker run [cmd]` 語意: + 無 CMD → 跑 Dockerfile 的 CMD(例: devel=bash, runtime=auto-run service) + 有 CMD → 覆蓋 Dockerfile CMD(例: ./run.sh -t runtime bash 進 runtime shell) EOF ;; zh-CN) cat >&2 <<'EOF' -用法: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] [--lang ] [TARGET] +用法: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] + [--lang ] [-t|--target TARGET] [CMD...] 选项: -h, --help 显示此说明 - -d, --detach 后台运行(docker compose up -d) + -t, --target T Compose service 名称(默认: devel;例: runtime) + -d, --detach 后台运行(docker compose up -d,不接受 CMD) -s, --setup 强制重跑 setup.sh 重新生成 .env + compose.yaml (默认:.env 不存在时自动 bootstrap;存在时仅打印 drift warning) --dry-run 只打印将执行的 docker 命令,不实际执行 --instance NAME 启动命名 instance(与默认并行,suffix=-NAME) --lang LANG 设置消息语言(默认: en) -目标: - devel 开发环境(默认) - runtime 最小化 runtime +CMD: 启动容器后要执行的指令,对齐 `docker run [cmd]` 语义: + 无 CMD → 跑 Dockerfile 的 CMD(例: devel=bash, runtime=auto-run service) + 有 CMD → 覆盖 Dockerfile CMD(例: ./run.sh -t runtime bash 进 runtime shell) EOF ;; ja) cat >&2 <<'EOF' -使用法: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] [--lang ] [TARGET] +使用法: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] + [--lang ] [-t|--target TARGET] [CMD...] オプション: -h, --help このヘルプを表示 - -d, --detach バックグラウンドで実行(docker compose up -d) + -t, --target T Compose サービス名(デフォルト: devel;例: runtime) + -d, --detach バックグラウンド実行(docker compose up -d、CMD は受け付けない) -s, --setup setup.sh を強制実行して .env + compose.yaml を再生成 (デフォルト:.env が無ければ自動 bootstrap、あれば drift warning のみ) --dry-run 実行される docker コマンドを表示するのみ(実行はしない) --instance NAME 名前付き instance を起動(デフォルトと並行、suffix=-NAME) --lang LANG メッセージ言語を設定(デフォルト: en) -ターゲット: - devel 開発環境(デフォルト) - runtime 最小化ランタイム +CMD: コンテナ起動後に実行するコマンド。`docker run [cmd]` セマンティクス: + CMD 無し → Dockerfile の CMD を実行(例: devel=bash, runtime=auto-run service) + CMD あり → Dockerfile CMD を上書き(例: ./run.sh -t runtime bash で runtime shell) EOF ;; *) cat >&2 <<'EOF' -Usage: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] [--lang ] [TARGET] +Usage: ./run.sh [-h] [-d|--detach] [-s|--setup] [--dry-run] [--instance NAME] + [--lang ] [-t|--target TARGET] [CMD...] Options: -h, --help Show this help - -d, --detach Run in background (docker compose up -d) + -t, --target T Compose service name (default: devel; e.g. runtime) + -d, --detach Run in background (docker compose up -d; no CMD accepted) -s, --setup Force rerun setup.sh to regenerate .env + compose.yaml (default: auto-bootstrap if .env missing; warn on drift if present) --dry-run Print the docker commands that would run, but do not execute --instance NAME Start a named parallel instance (suffix=-NAME) --lang LANG Set message language (default: en) -Targets: - devel Development environment (default) - runtime Minimal runtime +CMD: Command to run after the container starts; mirrors `docker run [cmd]`: + no CMD → run the Dockerfile CMD (e.g. devel=bash, runtime=auto-run service) + with CMD → override the Dockerfile CMD (e.g. ./run.sh -t runtime bash to shell in) EOF ;; esac @@ -147,6 +155,7 @@ main() { local DETACH=false local TARGET="devel" local INSTANCE="" + local -a CMD_ARGS=() DRY_RUN=false while [[ $# -gt 0 ]]; do @@ -175,14 +184,38 @@ main() { _sanitize_lang _LANG "run" shift 2 ;; + -t|--target) + TARGET="${2:?"-t/--target requires a value (e.g. devel, runtime)"}" + shift 2 + ;; + --) + shift + CMD_ARGS+=("$@") + break + ;; *) - TARGET="$1" + # Positional from here on is the CMD to run inside the container, + # mirroring `docker run [cmd...]` semantics. Empty CMD_ARGS + # means "use the Dockerfile CMD". + CMD_ARGS+=("$1") shift ;; esac done export DRY_RUN + # -d is background `compose up`, which starts the service with its + # compose-level command (for devel: tty/stdin_open keep it alive; for + # runtime: the Dockerfile CMD runs headless). `up` has no slot for an + # override cmd, so -d + CMD is ambiguous — refuse rather than silently + # drop the cmd. + if [[ "${DETACH}" == true ]] && (( ${#CMD_ARGS[@]} > 0 )); then + printf "[run] ERROR: -d/--detach does not accept a CMD (got: %s). " "${CMD_ARGS[*]}" >&2 + printf "Use './exec.sh -t %s %s' to run a command inside a detached container.\n" \ + "${TARGET}" "${CMD_ARGS[*]}" >&2 + exit 2 + fi + local _setup="${FILE_PATH}/template/script/docker/setup.sh" local _tui="${FILE_PATH}/setup_tui.sh" @@ -272,12 +305,25 @@ main() { # Foreground devel: `up -d` + `exec` so a second terminal can join via # `./exec.sh`. Trap auto-`down` on exit to preserve the # "exit shell = container gone" semantic of the previous `compose run`. + # CMD_ARGS passthrough: empty → `bash` (matches Dockerfile CMD for devel); + # non-empty → override (e.g. `./run.sh ls /tmp`). trap _devel_cleanup EXIT _compose_project up -d "${TARGET}" - _compose_project exec "${TARGET}" bash + if (( ${#CMD_ARGS[@]} > 0 )); then + _compose_project exec "${TARGET}" "${CMD_ARGS[@]}" + else + _compose_project exec "${TARGET}" bash + fi else - # Other one-shot stages (test, runtime, ...): keep `compose run --rm`. - _compose_project run --rm "${TARGET}" + # Other one-shot stages (runtime, test, ...): `compose run --rm` with + # CMD passthrough. Empty CMD_ARGS → service's Dockerfile CMD runs + # (e.g. runtime auto-boots parameter_bridge). Non-empty overrides + # (e.g. `./run.sh -t runtime bash` to debug interactively). + if (( ${#CMD_ARGS[@]} > 0 )); then + _compose_project run --rm "${TARGET}" "${CMD_ARGS[@]}" + else + _compose_project run --rm "${TARGET}" + fi fi } diff --git a/template/script/docker/setup.sh b/template/script/docker/setup.sh index a67478c..48fdcae 100755 --- a/template/script/docker/setup.sh +++ b/template/script/docker/setup.sh @@ -693,6 +693,21 @@ generate_compose_yaml() { printf ' runtime: %s\n' "${_runtime}" } + # Detect `FROM … AS runtime` in the sibling Dockerfile — if present, + # emit a dedicated `runtime` compose service that extends `devel`'s + # baseline (same volumes / network / caps / GPU) but with its own + # image tag, container_name, and non-interactive tty settings so + # `./run.sh -t runtime` auto-runs the Dockerfile CMD (e.g. a + # parameter_bridge process). Absent `AS runtime` → skip emission so + # repos without a runtime stage don't get a broken service entry. + # Issue #108. + local _dockerfile _has_runtime=false + _dockerfile="$(dirname -- "${_out}")/Dockerfile" + if [[ -f "${_dockerfile}" ]] \ + && grep -qE '^FROM[[:space:]]+[^[:space:]]+[[:space:]]+AS[[:space:]]+runtime[[:space:]]*$' "${_dockerfile}"; then + _has_runtime=true + fi + # Convert space-separated caps to YAML array form [a, b, c] local -a _caps_arr=() read -ra _caps_arr <<< "${_gpu_caps}" @@ -878,6 +893,33 @@ YAML capabilities: ${_caps_yaml} YAML fi + + # runtime service (when Dockerfile has `AS runtime`): extends devel's + # baseline (volumes, network, GPU, capabilities), overrides target + + # image + container_name, disables tty/stdin_open since runtime is + # auto-run headless (Dockerfile CMD drives). profiles: [runtime] + # keeps plain `compose up` scoped to devel; `compose run runtime` or + # `compose up runtime` still works because explicit-service targeting + # bypasses the profile gate. + if [[ "${_has_runtime}" == true ]]; then + cat < AS runtime`, setup.sh +# emits a dedicated `runtime` compose service alongside `devel`/`test`. +# Absent that stage, emission is skipped so plain-dev repos don't get a +# broken service entry. + +@test "generate_compose_yaml emits runtime service when Dockerfile has AS runtime" { + cat > "${TEMP_DIR}/Dockerfile" <<'DOCK' +FROM ubuntu:24.04 AS devel +CMD ["bash"] + +FROM devel AS runtime +CMD ["/entrypoint.sh"] +DOCK + local _extras=() + generate_compose_yaml "${COMPOSE_OUT}" "myrepo" \ + "false" "false" "0" "gpu" _extras + run grep -E '^ runtime:' "${COMPOSE_OUT}" + assert_success +} + +@test "generate_compose_yaml skips runtime service when Dockerfile lacks AS runtime" { + cat > "${TEMP_DIR}/Dockerfile" <<'DOCK' +FROM ubuntu:24.04 AS devel +CMD ["bash"] +DOCK + local _extras=() + generate_compose_yaml "${COMPOSE_OUT}" "myrepo" \ + "false" "false" "0" "gpu" _extras + run grep -cE '^ runtime:' "${COMPOSE_OUT}" + assert_output "0" +} + +@test "generate_compose_yaml skips runtime service when Dockerfile is absent" { + # No Dockerfile in TEMP_DIR at all. + local _extras=() + generate_compose_yaml "${COMPOSE_OUT}" "myrepo" \ + "false" "false" "0" "gpu" _extras + run grep -cE '^ runtime:' "${COMPOSE_OUT}" + assert_output "0" +} + +@test "runtime service extends devel and overrides target/image/tty/profile" { + cat > "${TEMP_DIR}/Dockerfile" <<'DOCK' +FROM ubuntu:24.04 AS devel +CMD ["bash"] + +FROM devel AS runtime +CMD ["/app"] +DOCK + local _extras=() + generate_compose_yaml "${COMPOSE_OUT}" "myrepo" \ + "false" "false" "0" "gpu" _extras + # extends → devel (compose merges base volumes, env, caps, etc.) + run grep -F 'service: devel' "${COMPOSE_OUT}" + assert_success + # build target override + run grep -F 'target: runtime' "${COMPOSE_OUT}" + assert_success + # image tag is :runtime (not :devel) + run grep -E '^ image:.*:runtime$' "${COMPOSE_OUT}" + assert_success + # container_name uses -runtime suffix + INSTANCE_SUFFIX support + run grep -F 'container_name: myrepo-runtime${INSTANCE_SUFFIX:-}' "${COMPOSE_OUT}" + assert_success + # non-interactive (runtime is headless auto-run, Dockerfile CMD drives) + run grep -E '^ stdin_open: false$' "${COMPOSE_OUT}" + assert_success + run grep -E '^ tty: false$' "${COMPOSE_OUT}" + assert_success + # profiles gate prevents plain `compose up` from starting runtime. + # `--` guards against grep reading the leading `-` as an option. + run grep -F -- '- runtime' "${COMPOSE_OUT}" + assert_success +} + +@test "runtime service appears between devel and test blocks" { + cat > "${TEMP_DIR}/Dockerfile" <<'DOCK' +FROM ubuntu:24.04 AS devel +CMD ["bash"] + +FROM devel AS runtime +CMD ["/app"] +DOCK + local _extras=() + generate_compose_yaml "${COMPOSE_OUT}" "myrepo" \ + "false" "false" "0" "gpu" _extras + local _devel _runtime _test + _devel="$(grep -n '^ devel:' "${COMPOSE_OUT}" | head -1 | cut -d: -f1)" + _runtime="$(grep -n '^ runtime:' "${COMPOSE_OUT}" | head -1 | cut -d: -f1)" + _test="$(grep -n '^ test:' "${COMPOSE_OUT}" | head -1 | cut -d: -f1)" + (( _devel < _runtime )) + (( _runtime < _test )) +} + +@test "runtime detection is robust against weird whitespace" { + cat > "${TEMP_DIR}/Dockerfile" <<'DOCK' +FROM ubuntu:24.04 AS devel +CMD ["bash"] + +FROM devel AS runtime +CMD ["/app"] +DOCK + local _extras=() + generate_compose_yaml "${COMPOSE_OUT}" "myrepo" \ + "false" "false" "0" "gpu" _extras + run grep -E '^ runtime:' "${COMPOSE_OUT}" + assert_success +} + +@test "runtime detection ignores non-runtime stage names" { + cat > "${TEMP_DIR}/Dockerfile" <<'DOCK' +FROM ubuntu:24.04 AS runtime-base +FROM runtime-base AS devel +CMD ["bash"] +DOCK + local _extras=() + generate_compose_yaml "${COMPOSE_OUT}" "myrepo" \ + "false" "false" "0" "gpu" _extras + # "runtime-base" doesn't count as the runtime stage (strict match). + run grep -cE '^ runtime:' "${COMPOSE_OUT}" + assert_output "0" +} diff --git a/template/test/unit/run_sh_spec.bats b/template/test/unit/run_sh_spec.bats index 521714f..f4619a3 100644 --- a/template/test/unit/run_sh_spec.bats +++ b/template/test/unit/run_sh_spec.bats @@ -239,12 +239,39 @@ EOS } @test "run.sh non-devel target routes to 'compose run --rm'" { - run bash "${SANDBOX}/run.sh" --dry-run test + # -t is now the explicit target flag; positional would be treated as CMD. + run bash "${SANDBOX}/run.sh" --dry-run -t test assert_success assert_output --partial "run" assert_output --partial "--rm" } +@test "run.sh positional args after options become CMD passthrough (devel)" { + # New semantics: positionals = cmd, default target = devel. + # Expect exec of `ls /tmp` inside the devel service. + run bash "${SANDBOX}/run.sh" --dry-run ls /tmp + assert_success + assert_output --partial "exec" + assert_output --partial "ls /tmp" +} + +@test "run.sh -t runtime with CMD overrides Dockerfile runtime CMD" { + run bash "${SANDBOX}/run.sh" --dry-run -t runtime bash + assert_success + assert_output --partial "run" + assert_output --partial "--rm" + assert_output --partial "runtime" + assert_output --partial "bash" +} + +@test "run.sh -d combined with CMD is rejected with exit 2" { + run bash "${SANDBOX}/run.sh" --dry-run -d ls /tmp + assert_failure + [ "$status" -eq 2 ] + assert_output --partial "does not accept a CMD" + assert_output --partial "./exec.sh" +} + @test "run.sh --instance is appended to project/container name" { run bash "${SANDBOX}/run.sh" --dry-run --instance foo assert_success diff --git a/template/test/unit/template_spec.bats b/template/test/unit/template_spec.bats index b18d16c..172da17 100644 --- a/template/test/unit/template_spec.bats +++ b/template/test/unit/template_spec.bats @@ -470,6 +470,20 @@ EOF assert_success } +@test "Dockerfile.test-tools ARG TARGETARCH has no default value (must not shadow BuildKit auto-inject)" { + # Regression guard: `ARG TARGETARCH=amd64` with a default shadows + # BuildKit's per-platform auto-inject (moby/buildkit#3403), which + # caused every multi-arch build to fall back to amd64 — arm64 image + # variants shipped x86_64 shellcheck / hadolint binaries. Symptom + # downstream: `shellcheck: Exec format error` on arm64 CI. + run grep -E '^ARG TARGETARCH=' /source/dockerfile/Dockerfile.test-tools + assert_failure + # But the bare declaration must still be there so the stage can + # consume the BuildKit-injected value. + run grep -E '^ARG TARGETARCH$' /source/dockerfile/Dockerfile.test-tools + assert_success +} + @test "Dockerfile.test-tools branches case for amd64 and arm64" { # Must handle both common arches; amd64 → x86_64 binaries, # arm64 → aarch64 (shellcheck) + arm64 (hadolint) binaries. @@ -627,9 +641,12 @@ _stage_lint_layout() { # ════════════════════════════════════════════════════════════════════ @test ".version file exists in template root" { + # Semver with optional pre-release (e.g. v0.10.0-rc1). Accepts plain + # `vX.Y.Z` and `vX.Y.Z-` per semver §9 so the RC release + # workflow doesn't fail on the CHANGELOG self-check. assert [ -f /source/.version ] run cat /source/.version - assert_output --regexp '^v[0-9]+\.[0-9]+\.[0-9]+$' + assert_output --regexp '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$' } @test "upgrade.sh reads version from template/.version" { @@ -702,6 +719,114 @@ EOF rm -rf "${_tmp}" } +# ════════════════════════════════════════════════════════════════════ +# build-worker.yaml: GHCR test-tools migration (D plan) +# ════════════════════════════════════════════════════════════════════ + +@test "build-worker.yaml: no legacy in-job test-tools build step" { + # The old `Build test-tools image` step is replaced by GHCR pull + # via the TEST_TOOLS_IMAGE build-arg. If it reappears, CI will hit + # the cross-step buildx image-store isolation again (v0.9.12 regression). + local _yaml="/source/.github/workflows/build-worker.yaml" + [[ -f "${_yaml}" ]] || skip "build-worker.yaml not present in /source" + run grep -c 'Build test-tools image' "${_yaml}" + assert_output "0" +} + +@test "build-worker.yaml: resolves template version from GITHUB_WORKFLOW_REF" { + local _yaml="/source/.github/workflows/build-worker.yaml" + [[ -f "${_yaml}" ]] || skip "build-worker.yaml not present in /source" + run grep -F 'GITHUB_WORKFLOW_REF' "${_yaml}" + assert_success + # outputs var must carry the ghcr.io tag for downstream build-arg pass-through + run grep -F 'ghcr.io/ycpss91255-docker/test-tools' "${_yaml}" + assert_success +} + +@test "build-worker.yaml: test build passes TEST_TOOLS_IMAGE build-arg" { + local _yaml="/source/.github/workflows/build-worker.yaml" + [[ -f "${_yaml}" ]] || skip "build-worker.yaml not present in /source" + # The test-stage build step must include TEST_TOOLS_IMAGE so the + # downstream Dockerfile's `FROM ${TEST_TOOLS_IMAGE}` stage resolves + # to the GHCR image (not the local fallback tag). + run awk ' + /- name: Build test stage/ { inside = 1 } + inside && /^[[:space:]]*- name:/ && !/Build test stage/ { inside = 0 } + inside { print } + ' "${_yaml}" + assert_success + assert_output --partial 'TEST_TOOLS_IMAGE=' +} + +# ════════════════════════════════════════════════════════════════════ +# Dockerfile.example: TEST_TOOLS_IMAGE ARG + named stage +# ════════════════════════════════════════════════════════════════════ + +@test "Dockerfile.example has ARG TEST_TOOLS_IMAGE with test-tools:local default" { + local _df="/source/dockerfile/Dockerfile.example" + [[ -f "${_df}" ]] || skip "Dockerfile.example not present in /source" + run grep -E '^ARG TEST_TOOLS_IMAGE="test-tools:local"' "${_df}" + assert_success +} + +@test "Dockerfile.example FROM \${TEST_TOOLS_IMAGE} AS test-tools-stage" { + local _df="/source/dockerfile/Dockerfile.example" + [[ -f "${_df}" ]] || skip "Dockerfile.example not present in /source" + run grep -F 'FROM ${TEST_TOOLS_IMAGE} AS test-tools-stage' "${_df}" + assert_success +} + +@test "Dockerfile.example test stage copies from test-tools-stage, not test-tools:local" { + local _df="/source/dockerfile/Dockerfile.example" + [[ -f "${_df}" ]] || skip "Dockerfile.example not present in /source" + # All COPY --from referring to the test-tools image must now use the + # named stage alias. + run grep -c 'COPY --from=test-tools-stage' "${_df}" + # 4 copies expected: shellcheck, hadolint, /opt/bats, /usr/lib/bats + assert_output "4" + # Legacy tag reference must be gone: + run grep -c 'COPY --from=test-tools:local' "${_df}" + assert_output "0" +} + +# ════════════════════════════════════════════════════════════════════ +# release-test-tools.yaml: GHCR publisher workflow +# ════════════════════════════════════════════════════════════════════ + +@test "release-test-tools.yaml exists and pushes to ghcr.io/ycpss91255-docker/test-tools" { + local _yaml="/source/.github/workflows/release-test-tools.yaml" + [[ -f "${_yaml}" ]] || skip "release-test-tools.yaml not present in /source" + run grep -F 'ghcr.io/ycpss91255-docker/test-tools' "${_yaml}" + assert_success +} + +@test "release-test-tools.yaml declares packages:write permission" { + local _yaml="/source/.github/workflows/release-test-tools.yaml" + [[ -f "${_yaml}" ]] || skip "release-test-tools.yaml not present in /source" + run grep -F 'packages: write' "${_yaml}" + assert_success +} + +@test "release-test-tools.yaml builds multi-arch (amd64 + arm64)" { + local _yaml="/source/.github/workflows/release-test-tools.yaml" + [[ -f "${_yaml}" ]] || skip "release-test-tools.yaml not present in /source" + run grep -F 'platforms: linux/amd64,linux/arm64' "${_yaml}" + assert_success +} + +@test "release-test-tools.yaml uses template-repo-local Dockerfile path" { + # Regression: this workflow runs in the template repo, so Dockerfile.test-tools + # path must be `dockerfile/...` (not `template/dockerfile/...` which is the + # downstream subtree path used by build-worker.yaml). + local _yaml="/source/.github/workflows/release-test-tools.yaml" + [[ -f "${_yaml}" ]] || skip "release-test-tools.yaml not present in /source" + run grep -E '^\s*file: dockerfile/Dockerfile\.test-tools$' "${_yaml}" + assert_success + # And must NOT have the subtree-prefixed path: + run grep -c 'file: template/dockerfile/Dockerfile.test-tools' "${_yaml}" + assert_output "0" +} + # ════════════════════════════════════════════════════════════════════ # run.sh: XDG_SESSION_TYPE branching # ════════════════════════════════════════════════════════════════════