diff --git a/.base/.github/workflows/multi-distro-build-worker.yaml b/.base/.github/workflows/multi-distro-build-worker.yaml new file mode 100644 index 0000000..21787bf --- /dev/null +++ b/.base/.github/workflows/multi-distro-build-worker.yaml @@ -0,0 +1,169 @@ +name: Multi-distro build dispatcher + +# Two-job dispatcher reusable workflow (#325 B-1). Resolves a per-event +# distro subset and fans it out across `build-worker.yaml` matrix shards, +# so multi-distro callers (`env/ros_distro`, `env/ros2_distro`, +# `app/ros1_bridge`) don't have to copy-paste a +# `${{ github.event_name == 'pull_request' && fromJSON('[...]') || ... }}` +# expression into every main.yaml. +# +# Pull-request events use `pr_distros`; tag pushes, main pushes, and +# workflow_dispatch use `tag_distros` (the full validation matrix). +# Callers express the policy once per repo via two JSON-array inputs. +# +# A `ci-passed` rollup job aggregates the matrix result for branch +# protection — matches the existing rollup naming used by env/ros_distro +# / env/ros2_distro per CLAUDE.md's status-check table, so downstream +# branch protection contexts don't need to change when adopting this +# dispatcher. + +on: + workflow_call: + # === Multi-distro specific === + inputs: + pr_distros: + required: true + type: string + description: | + JSON-array string of distro values to build on `pull_request` + events. Typically a subset of `tag_distros` to keep PR CI + fast — e.g. `'["humble"]'` while `tag_distros: '["humble", + "jazzy"]'` for `app/ros1_bridge`. Tag-time still validates the + full matrix. + tag_distros: + required: true + type: string + description: | + JSON-array string of distro values to build on tag push, main + push, and `workflow_dispatch`. This is the "release-validation" + matrix — everything that ships needs to pass here. + distro_input_name: + required: true + type: string + description: | + Name of the Dockerfile `ARG` (and corresponding `build_args` + key) that consumes the per-shard distro value. The dispatcher + emits `=` as the first + build_args line for each shard. Example values: `ROS_DISTRO` + (ROS 1), `ROS2_DISTRO` (ROS 2 / bridge), `ROS_DISTRO` etc. + # === Forwarded to build-worker.yaml (mirror keep build-worker + # defaults so existing semantics are preserved when caller omits) === + image_name: + required: true + type: string + description: | + Base image name. The dispatcher derives each shard's image + name as `-` so multi-distro callers + don't have to template that expression themselves. E.g. + caller sets `image_name: ros1_bridge`, dispatcher passes + `ros1_bridge-humble` / `ros1_bridge-jazzy` to build-worker — + matching the existing org convention (`app/ros1_bridge`'s + pre-dispatcher main.yaml shipped `ros1_bridge-${distro}`). + extra_build_args: + required: false + type: string + default: "" + description: | + Multi-line KEY=VALUE build_args appended after the + dispatcher-emitted `=` line. + Forwarded as-is to build-worker.yaml's `build_args` input. + build_runtime: + required: false + type: boolean + default: true + test_tools_version: + required: false + type: string + default: "latest" + platforms: + required: false + type: string + default: "linux/amd64" + context_path: + required: false + type: string + default: "." + dockerfile_path: + required: false + type: string + default: "" + build_contexts: + required: false + type: string + default: "" + +jobs: + resolve-matrix: + runs-on: ubuntu-latest + outputs: + distros: ${{ steps.r.outputs.distros }} + steps: + - id: r + env: + EVENT_NAME: ${{ github.event_name }} + PR_DISTROS: ${{ inputs.pr_distros }} + TAG_DISTROS: ${{ inputs.tag_distros }} + # PR event -> pr_distros subset. Every other event + # (push to main, push of a v* tag, workflow_dispatch) -> + # tag_distros full matrix. Tag pushes specifically benefit + # from full validation since they cut releases; main pushes + # benefit because the same code path is what tag-cut will see. + run: | + set -euo pipefail + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + echo "distros=${PR_DISTROS}" >> "${GITHUB_OUTPUT}" + else + echo "distros=${TAG_DISTROS}" >> "${GITHUB_OUTPUT}" + fi + + call-build: + needs: resolve-matrix + strategy: + fail-fast: false + matrix: + distro: ${{ fromJSON(needs.resolve-matrix.outputs.distros) }} + uses: ./.github/workflows/build-worker.yaml + with: + # Per-shard image_name includes the distro so the GHCR tag + # disambiguates multi-distro builds from the same caller repo + # (e.g. ros1_bridge-humble vs ros1_bridge-jazzy). Hyphen + # matches the existing org convention; env/ros{,2}_distro use + # a single-image-multi-variant shape that is out of scope for + # this 1D dispatcher (tracked at #344 for 2D extension). + image_name: ${{ inputs.image_name }}-${{ matrix.distro }} + build_args: | + ${{ inputs.distro_input_name }}=${{ matrix.distro }} + ${{ inputs.extra_build_args }} + build_runtime: ${{ inputs.build_runtime }} + test_tools_version: ${{ inputs.test_tools_version }} + platforms: ${{ inputs.platforms }} + context_path: ${{ inputs.context_path }} + dockerfile_path: ${{ inputs.dockerfile_path }} + build_contexts: ${{ inputs.build_contexts }} + # Per-distro buildx cache scope so e.g. humble and jazzy shards + # don't evict each other's cache. Matches the `cache_variant` + # contract added in #272 for env/ros{,2}_distro's variant matrix. + cache_variant: ${{ matrix.distro }} + + ci-passed: + name: ci-passed + needs: call-build + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Aggregate matrix results + env: + NEEDS_RESULT: ${{ needs.call-build.result }} + # Rollup gate for downstream branch protection. The matrix + # `result` is `success` only when every shard finished with + # `conclusion: success`; any failure or cancellation surfaces + # here. `if: always()` ensures the rollup runs even when one + # matrix shard failed, otherwise branch protection would see a + # missing required check rather than a failed one. + run: | + set -euo pipefail + if [[ "${NEEDS_RESULT}" != "success" ]]; then + echo "call-build matrix did not all succeed: ${NEEDS_RESULT}" + exit 1 + fi + echo "All distro shards passed." diff --git a/.base/.github/workflows/self-test.yaml b/.base/.github/workflows/self-test.yaml index 875b117..ad2a020 100644 --- a/.base/.github/workflows/self-test.yaml +++ b/.base/.github/workflows/self-test.yaml @@ -90,7 +90,11 @@ jobs: else echo "code_changed=true" >> "${GITHUB_OUTPUT}" fi - # behavioural_relevant: block-list (Q5 in #317). + # behavioural_relevant: block-list (Q5 in #317; #317 P3 gotcha-5 + # extends it to setup.sh + lib/** + i18n.sh + prune.sh — each + # of these affects .env / compose.yaml generation or wrapper + # behaviour that the behavioural docker.sock-mounted compose + # service exercises end-to-end). if git diff --quiet "${BASE}"...HEAD \ -- 'script/entrypoint.sh' \ 'compose.yaml' \ @@ -100,6 +104,10 @@ jobs: 'script/docker/run.sh' \ 'script/docker/exec.sh' \ 'script/docker/stop.sh' \ + 'script/docker/prune.sh' \ + 'script/docker/setup.sh' \ + 'script/docker/i18n.sh' \ + 'script/docker/lib/**' \ 'test/behavioural/**' \ 'init.sh' 'upgrade.sh' \ '.github/workflows/**'; then @@ -312,8 +320,12 @@ jobs: behavioural: name: Behavioural Test (runtime-test smoke gate, #249) needs: [actionlint, classify] - # P1 (#317) gates on code_changed only; P3 will tighten to behavioural_relevant. - if: needs.classify.outputs.code_changed == 'true' + # P3 (#317): gate on the narrower `behavioural_relevant` output so + # PRs that change pure lint / unit-test / doc paths (already covered + # by `test`) don't burn the docker.sock-mounted compose run. The + # block-list driving this output is in the `classify` job above; + # gotcha-5 extends it to setup.sh + lib/** + i18n.sh + prune.sh. + if: needs.classify.outputs.behavioural_relevant == 'true' runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.base/.version b/.base/.version index 46e8233..678c94d 100644 --- a/.base/.version +++ b/.base/.version @@ -1 +1 @@ -v0.28.2 +v0.29.2 diff --git a/.base/doc/changelog/CHANGELOG.md b/.base/doc/changelog/CHANGELOG.md index 689ad4d..14218be 100644 --- a/.base/doc/changelog/CHANGELOG.md +++ b/.base/doc/changelog/CHANGELOG.md @@ -7,6 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [v0.29.2] - 2026-05-14 + +Patch release bundling 4 small-bug closures since v0.29.1: #334 (Dockerfile.example WORKDIR collapse), #335 (exec.sh -t non-devel precheck), #341 (stop.sh skips profile-gated services), #345 (stop.sh -v no-op). No breaking changes from v0.29.1. Per CLAUDE.md's "MAJOR.MINOR.PATCH = bug fix; RC not required" rule, tagged directly on the merge commit. + +The #334 and #335 entries below were originally drafted into [Unreleased] when their respective PRs (#350 / #351) landed, but a rebase conflict-resolution mistake during the v0.29.1 -> v0.29.2 cycle placed them under the already-tagged [v0.29.1] heading. Moved here for accuracy: those fixes shipped in v0.29.2, not v0.29.1. + +### Fixed + +- `dockerfile/Dockerfile.example`: add explicit `ENV HOME="/home/${USER_NAME}"` after the `USER` directive in the `devel` stage. `WORKDIR` is a Docker directive that interpolates only build-time `ARG` / `ENV`, not shell-time `$HOME`, so without this the `WORKDIR "${HOME}/work"` directive on the next line silently collapsed to `WORKDIR /work`. Effects: BuildKit emitted `WARN: UndefinedVar: Usage of undefined variable '$HOME'` on every build; `docker inspect --format '{{.Config.WorkingDir}}'` returned `/work` instead of `/home//work`; non-interactive `docker exec` without `--workdir` landed in `/work` instead of the workspace mount. Interactive `./exec.sh` paths were unaffected because bash resets `$HOME` from the passwd entry, masking the bug. The same `ENV HOME` was added to the commented runtime-stage example (line ~373) for consistency. New integration test in `init_new_repo_spec.bats` asserts the `ENV HOME` appears before the `WORKDIR "${HOME}/work"` directive. Closes #334. + +- `script/docker/exec.sh`: precheck for "is the target container running?" now derives the container name from `-t/--target` instead of hardcoding the `devel`-flavoured name. Before this fix the precheck at line 299 was `${USER_NAME}-${IMAGE_NAME}${INSTANCE_SUFFIX}` regardless of target, so any `./exec.sh -t ...` invocation against a running `headless` / `gui` / `test` stage container (auto-emitted via #215 with `container_name: ${USER_NAME}-${IMAGE_NAME}-${INSTANCE_SUFFIX:-}`) aborted with "'' is not running" even though `docker compose exec ${TARGET}` would have worked. The fix mirrors the compose.yaml convention: `devel` -> no stage suffix, anything else -> `-${TARGET}` suffix between `${IMAGE_NAME}` and `${INSTANCE_SUFFIX}`. 4 new unit cases in `exec_sh_spec.bats` lock the precheck name shape across all four combinations (devel / non-devel x with-instance / without-instance). Closes #335. + +- `script/docker/stop.sh`: `_down_one` now exports `COMPOSE_PROFILES='*'` (compose v2.32+ wildcard) and passes `--remove-orphans` when invoking `docker compose down`. Without these, profile-gated services (the auto-emitted `headless` / `gui` / `test` stages introduced via #215) were silently skipped because `docker compose down` only acts on services in currently-active profiles; running `./run.sh -t headless -d` started the container correctly, but `./stop.sh` left it running with no output and `exit 0`. `--remove-orphans` additionally reclaims containers from prior compose.yaml shapes the current file no longer declares. The same env + flag pair is threaded through every `_down_one` invocation (default, `--instance`, `--all`). 2 new unit cases in `stop_sh_spec.bats` lock the `--remove-orphans` propagation including across `--all`. Closes #341. + +- `script/docker/stop.sh`: `-v` / `--verbose` is no longer a no-op. Previously it only exported `BUILDKIT_PROGRESS=plain`, which has zero effect on `compose down` because compose down doesn't build anything; the flag accepted, produced no extra output, and users were left confused. The new behaviour lists the containers belonging to the compose project (name + state, via `docker ps -a --filter "label=com.docker.compose.project="`) before tearing them down, giving the stop flow a real visible signal in parity with `build.sh -v` / `run.sh -v` / `exec.sh -v`. When no containers match, an explicit "No containers found for project <name>" line is printed instead. `-vv` continues to add `set -x` wrapper trace on top. 3 new unit cases in `stop_sh_spec.bats` cover the populated / empty / default (no -v) output. Usage text updated in all 4 languages. Closes #345. + +## [v0.29.1] - 2026-05-14 + +Patch release (no RC) correcting the v0.29.0 dispatcher's per-shard `image_name` separator from `_` to `-` before any downstream adoption. Per CLAUDE.md's "MAJOR.MINOR.PATCH = bug fix; RC not required" rule (see v0.12.1 / v0.12.2 / v0.12.3 precedent), tagged directly on the merge commit. + +### Fixed + +- `.github/workflows/multi-distro-build-worker.yaml`: per-shard `image_name` separator changed from `_` (v0.29.0) to `-` to match the existing org convention. `app/ros1_bridge`'s pre-dispatcher `main.yaml` shipped `ros1_bridge-${distro}` (hyphen); v0.29.0's initial dispatcher used `_${distro}` (underscore) which would have forced a registry tag rename on adoption. No consumer had adopted v0.29.0's dispatcher yet — this fix corrects the separator before the first downstream migration (planned for `app/ros1_bridge`). `env/ros{,2}_distro` use a single-image-multi-variant shape (no distro suffix on image_name) that this 1D dispatcher doesn't fit; their migration is tracked at #344 (2D dispatcher extension). + +## [v0.29.0] - 2026-05-14 + +Promoted from `v0.29.0-rc1` (#343). rc1 tag CI green: `Self Test` + `Release test-tools image to GHCR` both completed/success. The `:main` rolling tag bootstrapped on the rc1 multi-arch publish. + +Bundles all rc1 content; no further changes between rc1 and stable. + +Downstream propagation queued separately: + +- `app/ros1_bridge` — migrate `main.yaml` to use `multi-distro-build-worker.yaml@v0.29.0` (1D matrix; B-1 ready as-shipped). +- `env/ros_distro` + `env/ros2_distro` — defer until base#344 ships the 2D dispatcher extension (distro × variant). +- 13 single-target downstream repos — `/batch-template-upgrade v0.29.0` when ready; nothing in v0.29.0 is breaking for them. + +## [v0.29.0-rc1] - 2026-05-14 + +Release Candidate for v0.29.0 minor feature release. Bundles two themes since v0.28.2: + +- **#317 self-test CI optimization plan completed** — all four P-phases shipped: P1 (#318, buildx GHA cache + doc-only classifier; pre-session) + P1 follow-up (#329, classifier fail-open + base ref fetch for fork PRs) + P2 (#332 + #336 hotfix, `:main` rolling tag with 3-layer Obtain fallback + integration-e2e env passthrough + paths-filtered main-push trigger on release-test-tools.yaml) + P3 (#342, `behavioural` job tightened to `behavioural_relevant` gate + block-list extended with `setup.sh` / `i18n.sh` / `lib/**` / `prune.sh`). Net effect: typical PR shaves ~3-5 min wall-time; doc-only PRs land in seconds with `test` short-circuited and `integration-e2e` / `behavioural` skipped; bootstrap-window of fresh `:main` tag absorbed by the 3-layer fallback. + +- **#325 B-1 dispatcher reusable workflow** (#339) — new `.github/workflows/multi-distro-build-worker.yaml` lets multi-distro callers (`app/ros1_bridge`, eventually `env/ros_distro` and `env/ros2_distro` after 2D extension lands) pass `pr_distros` / `tag_distros` JSON-array inputs plus a `distro_input_name`. The dispatcher resolves the per-event distro subset and fans it across `build-worker.yaml` matrix shards with a `ci-passed` rollup matching CLAUDE.md's status-check table contract. Replaces the previous "copy-paste the `github.event_name == 'pull_request' && fromJSON(...) || fromJSON(...)` expression into every multi-distro main.yaml" pattern (Path A, rejected per locked decision). + +Plus a documentation clarification (#331) on the v0.28.1 `${USER_NAME}-` container_name prefix's relationship to `${DOCKER_HUB_USER}` / `INSTANCE_SUFFIX` namespacing. + +No breaking changes from v0.28.2. All changes are internal CI plumbing or net-additive reusable workflow surfaces; downstream Dockerfile contracts and `build-worker.yaml` / `release-worker.yaml` input signatures unchanged. + +Downstream propagation is partial in this release: the B-1 dispatcher consumer migrations (`app/ros1_bridge`) follow as separate PRs after v0.29.0 stable lands. `env/*_distro` migrations defer to the 2D-matrix dispatcher extension tracked separately. + +### Changed + +- `.github/workflows/self-test.yaml`: `behavioural` job's job-level `if:` tightens from `needs.classify.outputs.code_changed == 'true'` to `needs.classify.outputs.behavioural_relevant == 'true'` (#317 P3). P1 already emitted the narrower `behavioural_relevant` output but routed only `code_changed` to the `behavioural` gate; P3 wires the existing output to its intended consumer so PRs that change pure lint / unit-test / Codecov-relevant paths (covered by `test`) no longer burn the docker.sock-mounted compose run. The `classify` job's behavioural block-list is extended with `script/docker/setup.sh` + `script/docker/i18n.sh` + `script/docker/lib/**` + `script/docker/prune.sh` (#317 gotcha-5): each affects `.env` / `compose.yaml` generation or wrapper behaviour that the behavioural compose service exercises end-to-end, so changes there must invalidate the behavioural-skip optimization. Closes the last P-phase of #317. + +### Added + +- `.github/workflows/multi-distro-build-worker.yaml`: new dispatcher reusable workflow (#325 B-1). Multi-distro callers (`env/ros_distro`, `env/ros2_distro`, `app/ros1_bridge`) pass `pr_distros` / `tag_distros` JSON-array inputs plus a `distro_input_name`; the dispatcher resolves the per-event distro subset (`pull_request` -> `pr_distros`; everything else -> `tag_distros`) and fans the subset across `build-worker.yaml` matrix shards. Each shard derives `image_name` as `_`, passes `=` as the first build_args line, and shards buildx GHA cache via `cache_variant: ${{ matrix.distro }}` (#272 contract reuse). A `ci-passed` rollup job satisfies branch protection — same name used by env/ros_distro / env/ros2_distro per CLAUDE.md's status-check table, so downstream protection contexts don't change on adoption. Solves the maintenance drift caused by the previous "copy-paste the `github.event_name == 'pull_request' && fromJSON('[...]') || fromJSON('[...]')` expression into every multi-distro `main.yaml`" pattern (#325 Path A, explicitly rejected in favour of B-1 dispatcher per the locked decision). + ## [v0.28.2] - 2026-05-14 Patch release for SSH X11 forwarding follow-up (#321 hotfix #333) bundled with CI infrastructure improvements (#317 P2 rolling `:main` tag + 3-layer Obtain fallback, #336 integration-e2e driver fix, #317 P1 follow-up classify-job hardening) and documentation clarifying the v0.28.1 naming-scheme change (#322 follow-up). No breaking changes from v0.28.1; users on v0.28.1 should upgrade to pick up the SSH X11 fix (the v0.28.1 cookie-rewrite path silently produced empty cookies under common `~/.Xauthority` lock contention). diff --git a/.base/doc/test/TEST.md b/.base/doc/test/TEST.md index 2b24094..343b988 100644 --- a/.base/doc/test/TEST.md +++ b/.base/doc/test/TEST.md @@ -1,6 +1,6 @@ # TEST.md -Template self-tests: **1253 tests** total (1197 unit + 56 integration). +Template self-tests: **1278 tests** total (1221 unit + 57 integration). > Counted scope is the `make -f Makefile.ci test` self-test suite — > what runs in the `Self Test` CI job. The 36 shared smoke tests under @@ -215,10 +215,10 @@ on doc-only PRs). | #272 GHA buildx cache: `cache_variant` input declared with empty default, `Compute cache scope` step emits `id: cache` + scope key into `GITHUB_OUTPUT`, 4 build steps set `cache-from: type=gha,scope=...`, 4 build steps set `cache-to: ...,mode=max`, default preserves zero-diff for single-call callers | 5 | | #273 doc-only PR fast-pass (Phase 1 + Phase 2 shell rewrite): `path-filter` job declared, classifier is pure shell (`git diff --name-only base...head` + `case` glob; no `dorny/paths-filter` dependency), reads EVENT_NAME / BASE_SHA / HEAD_SHA from env: keys so the case body stays portable, non-PR event short-circuits before git diff (BASE_SHA / HEAD_SHA empty on push / tag / workflow_dispatch), 6-path allowlist (`**/*.md`, `doc/**`, `LICENSE`, `.gitignore`, `.github/CODEOWNERS`, `.github/dependabot.yml`) in a single `case` arm, `compute-matrix` + `build` jobs gated on `code_changed == 'true'` (2 occurrences), `docker-build` aggregator handles `code_changed == 'false'` short-circuit + `needs: [path-filter, build]`, non-PR triggers always set `code_changed=true` | 8 | -### test/unit/self_test_yaml_spec.bats (25) +### test/unit/self_test_yaml_spec.bats (26) Structural assertions for `.github/workflows/self-test.yaml`. Locks -four cumulative invariants: +five cumulative invariants: 1. **#305 actionlint gate** — `actionlint` job declared, runs `rhysd/actionlint` via Docker pinned to an explicit version @@ -267,6 +267,20 @@ four cumulative invariants: test` so the wrapper script skips its own internal test-tools build, reusing the image populated by the Obtain step. +5. **#317 P3 behavioural conditional + block-list expansion** — + `behavioural` job's job-level `if:` tightens from + `code_changed == 'true'` (P1) to `behavioural_relevant == + 'true'` (the narrower output P1 already emitted but didn't + consume). PRs that change pure lint / unit-test paths + covered by `test` now skip the docker.sock-mounted compose + run, saving ~3-5 min per such PR. The behavioural block-list + in `classify` is extended with `script/docker/setup.sh` + + `script/docker/i18n.sh` + `script/docker/lib/**` + + `script/docker/prune.sh` (gotcha-5): each affects `.env` / + `compose.yaml` generation or wrapper behaviour that the + compose service exercises end-to-end, so they must invalidate + the behavioural-skip optimization. + | Category | Tests | |----------|-------| | `actionlint` job declared | 1 | @@ -275,13 +289,14 @@ four cumulative invariants: | `classify` doc-only allow-list + behavioural block-list + non-PR default | 3 | | `test`/`integration-e2e`/`behavioural` declare `needs: [actionlint, classify]` | 3 | | `test` doc-only short-circuit + real-step `code_changed == 'true'` gate | 2 | -| `integration-e2e` + `behavioural` job-level `if: code_changed == 'true'` | 2 | +| `integration-e2e` job-level `if: code_changed == 'true'` + `behavioural` job-level `if: behavioural_relevant == 'true'` (#317 P3 tightens) | 2 | | `test` + `behavioural` use `docker/build-push-action@v6` with `scope=test-tools` GHA cache | 2 | | `classify` fail-open (`set -uo pipefail`) + pre-fetch base ref (#317 gotcha-1/2) | 2 | | `test` Obtain step pulls `:main` with 3-layer fallback + Build step gated on `build_local` (#317 P2) | 2 | | `integration-e2e` Obtain step + `TEST_TOOLS_IMAGE` env passthrough + no `driver: docker` pin (#317 P2) | 2 | | `behavioural` Obtain step with 3-layer fallback (#317 P2) | 1 | | Obtain steps pre-fetch base ref (4 occurrences: classify + 3 jobs, #317 P2 reuses P1 gotcha-2 fix) | 1 | +| `classify` behavioural block-list extends to `setup.sh` + `i18n.sh` + `lib/**` + `prune.sh` (#317 P3 gotcha-5) | 1 | ### test/unit/release_test_tools_yaml_spec.bats (10) @@ -313,6 +328,52 @@ which would leave a freshly-pushed `:main` unverified). | Smoke step pulls trigger's tag via `steps.tags.outputs.smoke` (#317 P2) | 1 | | Build step pushes multi-arch (amd64 + arm64) + declares `packages: write` permission | 2 | +### test/unit/multi_distro_build_worker_yaml_spec.bats (14) + +Structural assertions for `.github/workflows/multi-distro-build-worker.yaml` +(#325 B-1 dispatcher). The dispatcher fans a per-event distro +subset across `build-worker.yaml` matrix shards so multi-distro +caller `main.yaml`s (`env/ros_distro`, `env/ros2_distro`, +`app/ros1_bridge`) stop copy-pasting a +`${{ github.event_name == 'pull_request' && ... || ... }}` +expression. Three jobs: + +1. **`resolve-matrix`** — pure-shell selector emitting a `distros` + JSON-array output. `pull_request` -> `pr_distros` (subset); + anything else (tag push, main push, `workflow_dispatch`) -> + `tag_distros` (release validation matrix). + +2. **`call-build`** — strategy.matrix job invoking the local + `build-worker.yaml` per distro shard. Derives per-shard + `image_name` as `_`, passes + `=` as the first `build_args` line, + and shards buildx GHA cache by distro via + `cache_variant: ${{ matrix.distro }}` (reuses #272's per-variant + scope contract). `fail-fast: false` so one shard's failure + doesn't cancel siblings. + +3. **`ci-passed`** — rollup gate for branch protection. Matches the + existing `ci-passed` rollup naming used by env/ros_distro / + env/ros2_distro per CLAUDE.md's status-check table, so + downstream branch-protection contexts don't change on adoption. + +| Category | Tests | +|----------|-------| +| Declares `workflow_call` | 1 | +| Required inputs: `pr_distros`, `tag_distros`, `distro_input_name`, `image_name` | 1 | +| Passthrough inputs mirror build-worker (build_runtime / test_tools_version / platforms / context_path / dockerfile_path / build_contexts) | 1 | +| Defines `extra_build_args` passthrough | 1 | +| `resolve-matrix` emits `distros` output | 1 | +| `resolve-matrix` branches on `github.event_name == 'pull_request'` | 1 | +| `call-build` `uses: ./.github/workflows/build-worker.yaml` | 1 | +| `call-build` matrix `fromJSON(needs.resolve-matrix.outputs.distros)` | 1 | +| `call-build` per-shard `image_name: _` | 1 | +| `call-build` `build_args` line `=` | 1 | +| `call-build` `cache_variant: ${{ matrix.distro }}` (per-distro cache scope) | 1 | +| `call-build` `fail-fast: false` | 1 | +| `ci-passed` rollup depends on `call-build`, runs with `if: always()` | 1 | +| `ci-passed` declares `name: ci-passed` to satisfy branch protection contract | 1 | + ### test/unit/build_sh_spec.bats (51) Unit tests for `build.sh` argument handling and control flow. Uses a @@ -364,7 +425,7 @@ value-required and directory guards, usage help mention), and **`-v` / `--verbose` / `-vv` / `--very-verbose` flag** (#311: same export + trace pattern as build.sh, parity across wrappers). -### test/unit/exec_sh_spec.bats (32) +### test/unit/exec_sh_spec.bats (36) Unit tests for `exec.sh` argument parsing, the container-running precheck, and i18n. Sandbox tree mirrors build_sh_spec.bats; @@ -390,7 +451,7 @@ guards, usage help mention), and **`-v` / `--verbose` / `-vv` / `docker exec` itself does not build, but flag is accepted and `-vv` enables wrapper trace). -### test/unit/stop_sh_spec.bats (29) +### test/unit/stop_sh_spec.bats (34) Unit tests for `stop.sh` argument parsing, the `--all` multi-instance teardown, and i18n. `docker ps -a` output is PATH-shimmed via @@ -874,7 +935,7 @@ Unit tests for `template/script/docker/lib/gitignore.sh` — the canonical | `_untrack_canonical_in_repo: idempotent — second run succeeds without error` | Re-run safety | | `_untrack_canonical_in_repo: untracks all canonical entries that match` | Multi-entry sweep | -### test/integration/init_new_repo_spec.bats (38) +### test/integration/init_new_repo_spec.bats (39) End-to-end verification that `init.sh` produces a complete repo skeleton in an empty directory. **Level 1** (file generation only, no Docker). The diff --git a/.base/dockerfile/Dockerfile.example b/.base/dockerfile/Dockerfile.example index e149c4a..4a99f35 100644 --- a/.base/dockerfile/Dockerfile.example +++ b/.base/dockerfile/Dockerfile.example @@ -162,6 +162,13 @@ COPY --chmod=0755 .base/dockerfile/setup "${SETUP_DIR}" USER "${USER}" +# WORKDIR is a Docker directive: it interpolates build-time ARG / ENV +# only, not shell-time $HOME. Without this explicit ENV, the +# `WORKDIR "${HOME}/work"` below silently collapses to /work (BuildKit +# emits `WARN: UndefinedVar`) and `docker inspect` reports the wrong +# WorkingDir. Refs #334. +ENV HOME="/home/${USER_NAME}" + # Setup pip packages (build-time scaffolding from SETUP_DIR, #261). RUN "${SETUP_DIR}"/pip/setup.sh @@ -364,6 +371,9 @@ RUN bats /smoke_test/ # # COPY --chmod=0755 script/entrypoint.sh /entrypoint.sh # USER "${USER}" +# # WORKDIR interpolates only build-time ARG / ENV (not shell $HOME); +# # see #334 for why the explicit ENV is required. +# ENV HOME="/home/${USER_NAME}" # WORKDIR "${HOME}/work" # ENTRYPOINT ["/entrypoint.sh"] # CMD ["bash"] diff --git a/.base/script/docker/exec.sh b/.base/script/docker/exec.sh index fb5c48c..8ce3814 100755 --- a/.base/script/docker/exec.sh +++ b/.base/script/docker/exec.sh @@ -294,9 +294,19 @@ main() { # Precheck: refuse with a friendly hint if the target container is not # running. Skipped under --dry-run since the user is asking what *would* run. - # Container name mirrors compose.yaml's `container_name:`, including the - # ${USER_NAME} prefix added in #322 for multi-user host disambiguation. - local _container_name="${USER_NAME}-${IMAGE_NAME}${INSTANCE_SUFFIX}" + # Container name mirrors compose.yaml's `container_name:`: + # - devel: ${USER_NAME}-${IMAGE_NAME}${INSTANCE_SUFFIX} + # - non-devel stage: ${USER_NAME}-${IMAGE_NAME}-${TARGET}${INSTANCE_SUFFIX} + # The ${USER_NAME} prefix landed in #322 (multi-user host + # disambiguation); the per-stage ${TARGET} suffix is the convention + # auto-emitted for headless / gui / test stages (#215). Refs #335 -- + # before this fix, the precheck always grepped for the devel-flavoured + # name and aborted any ./exec.sh -t invocation. + local _container_name="${USER_NAME}-${IMAGE_NAME}" + if [[ "${TARGET}" != "devel" ]]; then + _container_name="${_container_name}-${TARGET}" + fi + _container_name="${_container_name}${INSTANCE_SUFFIX}" if [[ "${DRY_RUN}" != true ]] \ && ! docker ps --format '{{.Names}}' | grep -qx "${_container_name}"; then # Compose the error + matching hint into a single multi-line _log_err diff --git a/.base/script/docker/stop.sh b/.base/script/docker/stop.sh index add9cb3..3277e6a 100755 --- a/.base/script/docker/stop.sh +++ b/.base/script/docker/stop.sh @@ -85,8 +85,8 @@ usage() { buildx cache / volumes)請用 ./prune.sh。 --lang LANG 設定訊息語言(預設: en) --dry-run 只印出將執行的 docker 指令,不實際執行 - -v, --verbose 設定 BUILDKIT_PROGRESS=plain(與其他 wrapper 對齊;stop 本身 - 不會 build,但保持 flag 一致便於肌肉記憶)。 + -v, --verbose 先列出該 compose project 下的 container(名稱 + 狀態), + 再執行 down。讓 stop 流程的可見訊號跟 build/run/exec 對齊。 -vv, --very-verbose -v 再加 wrapper 本身的 bash trace(set -x)。 EOF @@ -107,8 +107,8 @@ EOF buildx cache / volumes)请用 ./prune.sh。 --lang LANG 设置消息语言(默认: en) --dry-run 只打印将执行的 docker 命令,不实际执行 - -v, --verbose 设置 BUILDKIT_PROGRESS=plain(与其他 wrapper 对齐;stop 本身 - 不会 build,但保持 flag 一致便于肌肉记忆)。 + -v, --verbose 先列出该 compose project 下的 container(名称 + 状态), + 再执行 down。让 stop 流程的可见信号跟 build/run/exec 对齐。 -vv, --very-verbose -v 再加 wrapper 本身的 bash trace(set -x)。 EOF @@ -129,8 +129,8 @@ EOF (buildx cache / volume 含む)は ./prune.sh を使用。 --lang LANG メッセージ言語を設定(デフォルト: en) --dry-run 実行される docker コマンドを表示するのみ(実行はしない) - -v, --verbose BUILDKIT_PROGRESS=plain を設定(他の wrapper と整合; - stop 自体は build しないが、フラグの一貫性のため)。 + -v, --verbose compose project 配下のコンテナ(名前と状態)を down の + 前に出力。stop の可視シグナルを build/run/exec と揃える。 -vv, --very-verbose -v に加え wrapper 自体の bash trace(set -x)。 EOF @@ -153,10 +153,10 @@ Options: cache / volumes), use ./prune.sh. --lang LANG Set message language (default: en) --dry-run Print the docker commands that would run, but do not execute - -v, --verbose Export BUILDKIT_PROGRESS=plain for parity with build/run/exec. - `docker compose down` itself does not build, but keeping the - flag available across all four wrappers gives one - muscle-memory knob to reach for. + -v, --verbose List the containers belonging to this compose project (name + + state) before tearing them down. Gives the stop flow a + real visible signal in parity with build/run/exec, instead + of the previous no-op BUILDKIT_PROGRESS=plain export. -vv, --very-verbose -v plus bash trace (set -x) on the wrapper itself. EOF @@ -170,12 +170,38 @@ PASSTHROUGH=() # _down_one tears down a single instance. _compute_project_name sets and # exports INSTANCE_SUFFIX so compose.yaml resolves the matching container_name. # +# COMPOSE_PROFILES='*' (compose v2.32+ wildcard) activates every profile in +# compose.yaml so `compose down` actually visits profile-gated services +# (#215 auto-emitted headless / gui / test stages). Without this, profile- +# gated containers are silently skipped and remain running. --remove-orphans +# additionally catches containers from prior compose.yaml shapes that the +# current file no longer declares. Refs #341. +# +# -v / --verbose: list the containers belonging to the compose project +# before tearing them down. This gives the user a real signal instead of +# the previous no-op (the BUILDKIT_PROGRESS=plain env had zero effect on +# `compose down`, see #345). +# # Args: # $1: instance name (empty for the default instance) _down_one() { local instance="${1}" _compute_project_name "${instance}" - _compose_project down "${PASSTHROUGH[@]}" + if [[ "${VERBOSE:-}" == true ]]; then + local _project_name + _project_name="${PROJECT_NAME:-${DOCKER_HUB_USER}-${IMAGE_NAME}${INSTANCE_SUFFIX}}" + local _matches + _matches="$(docker ps -a \ + --filter "label=com.docker.compose.project=${_project_name}" \ + --format '{{.Names}} ({{.State}})' 2>/dev/null || true)" + if [[ -n "${_matches}" ]]; then + _log_info stop "Tearing down containers in project ${_project_name}:" + printf '%s\n' "${_matches}" | sed 's/^/ /' >&2 + else + _log_info stop "No containers found for project ${_project_name}" + fi + fi + COMPOSE_PROFILES='*' _compose_project down --remove-orphans "${PASSTHROUGH[@]}" } main() { @@ -231,14 +257,16 @@ main() { shift ;; -v|--verbose) - # BUILDKIT_PROGRESS=plain — symmetry with build/run/exec (#311). - # No-op for `docker compose down` itself but harmless; keeps the - # flag available across all four wrappers for muscle-memory - # consistency. + # Print the container list belonging to the compose project + # before tearing it down. BUILDKIT_PROGRESS=plain (the old #311 + # behaviour) had zero effect on `docker compose down`; replaced + # with real verbose output. Refs #345. + VERBOSE=true export BUILDKIT_PROGRESS=plain shift ;; -vv|--very-verbose) + VERBOSE=true export BUILDKIT_PROGRESS=plain set -x shift diff --git a/.base/test/integration/init_new_repo_spec.bats b/.base/test/integration/init_new_repo_spec.bats index 2435a74..d22977c 100644 --- a/.base/test/integration/init_new_repo_spec.bats +++ b/.base/test/integration/init_new_repo_spec.bats @@ -227,6 +227,22 @@ teardown() { } } +@test "Dockerfile.example declares ENV HOME before WORKDIR \${HOME}/work (#334)" { + local _df="/source/dockerfile/Dockerfile.example" + [[ -f "${_df}" ]] || skip "Dockerfile.example not present in /source" + # WORKDIR is a Docker directive that interpolates build-time ARG / + # ENV, not shell-time $HOME. Without an explicit ENV HOME, the + # `WORKDIR "${HOME}/work"` collapses to /work and BuildKit emits + # `WARN: UndefinedVar`. The ENV must appear BEFORE the WORKDIR. + run grep -nF 'ENV HOME="/home/${USER_NAME}"' "${_df}" + assert_success + local _env_line _workdir_line + _env_line="$(grep -nF 'ENV HOME="/home/${USER_NAME}"' "${_df}" | head -1 | cut -d: -f1)" + _workdir_line="$(grep -nF 'WORKDIR "${HOME}/work"' "${_df}" | grep -v '^[0-9]*:#' | head -1 | cut -d: -f1)" + [[ -n "${_env_line}" && -n "${_workdir_line}" ]] + (( _env_line < _workdir_line )) +} + @test "Dockerfile.example sets up bashrc.d drop-in directory (template#254)" { local _df="/source/dockerfile/Dockerfile.example" [[ -f "${_df}" ]] || skip "Dockerfile.example not present in /source" diff --git a/.base/test/unit/exec_sh_spec.bats b/.base/test/unit/exec_sh_spec.bats index 0737b14..09507a0 100644 --- a/.base/test/unit/exec_sh_spec.bats +++ b/.base/test/unit/exec_sh_spec.bats @@ -149,6 +149,41 @@ teardown() { assert_output --partial "exec" } +# ── -t precheck container name (issue #335) ────────────────────── + +@test "exec.sh -t : precheck name suffixes the target stage (#335)" { + # Before #335, the precheck always grepped `tester-mockimg` regardless of + # -t, so any non-devel target aborted with "not running". After the fix: + # -t devel -> tester-mockimg + # -t headless -> tester-mockimg-headless + run bash "${SANDBOX}/exec.sh" -t headless + assert_failure + assert_output --partial "tester-mockimg-headless" + refute_output --partial "'tester-mockimg' is not running" +} + +@test "exec.sh -t devel: precheck name has no stage suffix (parity, #335)" { + run bash "${SANDBOX}/exec.sh" -t devel + assert_failure + assert_output --partial "tester-mockimg" + refute_output --partial "tester-mockimg-devel" +} + +@test "exec.sh -t headless --instance foo: precheck name carries both suffixes (#335)" { + # Order in compose.yaml: ${USER_NAME}-${IMAGE_NAME}-${TARGET}${INSTANCE_SUFFIX} + # INSTANCE_SUFFIX is `-${INSTANCE}` when --instance set. + run bash "${SANDBOX}/exec.sh" -t headless --instance foo + assert_failure + assert_output --partial "tester-mockimg-headless-foo" +} + +@test "exec.sh -t : precheck passes when matching container is running (#335)" { + echo "tester-mockimg-headless" > "${DOCKER_PS_FILE}" + run bash "${SANDBOX}/exec.sh" -t headless --dry-run + assert_success + assert_output --partial "exec" +} + # ── -- flag/CMD separator (issue #289) ────────────────────────────────────── @test "exec.sh -- separator: standalone -- is consumed, CMD flows through (#289)" { diff --git a/.base/test/unit/multi_distro_build_worker_yaml_spec.bats b/.base/test/unit/multi_distro_build_worker_yaml_spec.bats new file mode 100644 index 0000000..eda6e52 --- /dev/null +++ b/.base/test/unit/multi_distro_build_worker_yaml_spec.bats @@ -0,0 +1,145 @@ +#!/usr/bin/env bats +# +# multi_distro_build_worker_yaml_spec.bats — structural assertions for +# `.github/workflows/multi-distro-build-worker.yaml` (#325 B-1 dispatcher). +# +# The dispatcher is a two-job reusable workflow on top of +# build-worker.yaml: +# +# 1. `resolve-matrix` — pure-shell selector that emits a `distros` JSON +# array output based on `github.event_name`. `pull_request` -> +# `pr_distros` (subset); everything else (tag push, main push, +# workflow_dispatch) -> `tag_distros` (full release matrix). +# +# 2. `call-build` — strategy.matrix job invoking +# `./.github/workflows/build-worker.yaml` per distro shard. Derives +# per-shard `image_name` as `_` so GHCR tags +# disambiguate across distros, and passes +# `=` as the first `build_args` line. +# Per-distro `cache_variant: ${{ matrix.distro }}` so buildx GHA +# cache shards by distro (matches #272's per-variant scope pattern). +# +# 3. `ci-passed` — rollup aggregating the matrix result for branch +# protection. Matches the existing rollup naming used by +# env/ros_distro / env/ros2_distro per CLAUDE.md's status-check +# table, so downstream branch-protection contexts don't change when +# adopting this dispatcher. + +bats_require_minimum_version 1.5.0 + +setup() { + load "${BATS_TEST_DIRNAME}/test_helper" + WF="/source/.github/workflows/multi-distro-build-worker.yaml" + [[ -f "${WF}" ]] || skip "multi-distro-build-worker.yaml not at expected path" +} + +# ── workflow_call interface ───────────────────────────────────────── + +@test "multi-distro-build-worker.yaml: declares workflow_call (#325 B-1)" { + run grep -E '^\s+workflow_call:' "${WF}" + assert_success +} + +@test "multi-distro-build-worker.yaml: required inputs include pr_distros + tag_distros + distro_input_name + image_name (#325 B-1)" { + run awk '/^on:/{flag=1} /^jobs:/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'pr_distros:' + assert_output --partial 'tag_distros:' + assert_output --partial 'distro_input_name:' + assert_output --partial 'image_name:' +} + +@test "multi-distro-build-worker.yaml: passthrough inputs mirror build-worker (build_runtime / test_tools_version / platforms / context_path / dockerfile_path / build_contexts) (#325 B-1)" { + run awk '/^on:/{flag=1} /^jobs:/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'build_runtime:' + assert_output --partial 'test_tools_version:' + assert_output --partial 'platforms:' + assert_output --partial 'context_path:' + assert_output --partial 'dockerfile_path:' + assert_output --partial 'build_contexts:' +} + +@test "multi-distro-build-worker.yaml: defines extra_build_args passthrough (caller can append KEY=VALUE after the dispatcher's distro arg) (#325 B-1)" { + run awk '/^on:/{flag=1} /^jobs:/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'extra_build_args:' +} + +# ── resolve-matrix job ─────────────────────────────────────────────── + +@test "multi-distro-build-worker.yaml: resolve-matrix job emits distros output (#325 B-1)" { + run awk '/^ resolve-matrix:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'distros: ${{ steps.r.outputs.distros }}' +} + +@test "multi-distro-build-worker.yaml: resolve-matrix branches on github.event_name == pull_request (#325 B-1)" { + run awk '/^ resolve-matrix:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'EVENT_NAME: ${{ github.event_name }}' + assert_output --partial '"${EVENT_NAME}" == "pull_request"' + assert_output --partial 'distros=${PR_DISTROS}' + assert_output --partial 'distros=${TAG_DISTROS}' +} + +# ── call-build matrix job ──────────────────────────────────────────── + +@test "multi-distro-build-worker.yaml: call-build uses local build-worker via ./.github/workflows/build-worker.yaml (#325 B-1)" { + run awk '/^ call-build:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'uses: ./.github/workflows/build-worker.yaml' +} + +@test "multi-distro-build-worker.yaml: call-build matrix is fromJSON(needs.resolve-matrix.outputs.distros) (#325 B-1)" { + run awk '/^ call-build:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'distro: ${{ fromJSON(needs.resolve-matrix.outputs.distros) }}' +} + +@test "multi-distro-build-worker.yaml: call-build derives per-shard image_name as - (hyphen, v0.29.1 fix matches org convention)" { + # Hyphen separator chosen to match the existing org pattern (e.g. + # app/ros1_bridge's pre-dispatcher main.yaml shipped + # `ros1_bridge-${distro}`). Underscore was used in the v0.29.0 + # initial implementation but never adopted by any consumer; v0.29.1 + # corrects it before the first downstream migration lands. + run awk '/^ call-build:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'image_name: ${{ inputs.image_name }}-${{ matrix.distro }}' + refute_output --partial 'image_name: ${{ inputs.image_name }}_${{ matrix.distro }}' +} + +@test "multi-distro-build-worker.yaml: call-build passes = as build_args (#325 B-1)" { + run awk '/^ call-build:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial '${{ inputs.distro_input_name }}=${{ matrix.distro }}' +} + +@test "multi-distro-build-worker.yaml: call-build splits buildx cache by distro via cache_variant: matrix.distro (#272 reuse, #325 B-1)" { + run awk '/^ call-build:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'cache_variant: ${{ matrix.distro }}' +} + +@test "multi-distro-build-worker.yaml: call-build has fail-fast: false so one shard's failure doesn't cancel siblings (#325 B-1)" { + run awk '/^ call-build:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'fail-fast: false' +} + +# ── ci-passed rollup ───────────────────────────────────────────────── + +@test "multi-distro-build-worker.yaml: ci-passed rollup job exists, depends on call-build, runs even if matrix failed (#325 B-1)" { + run awk '/^ ci-passed:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'needs: call-build' + assert_output --partial 'if: ${{ always() }}' + assert_output --partial 'NEEDS_RESULT' + assert_output --partial 'needs.call-build.result' +} + +@test "multi-distro-build-worker.yaml: ci-passed job has explicit name: ci-passed (matches existing multi-distro rollup contract) (#325 B-1)" { + run awk '/^ ci-passed:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial 'name: ci-passed' +} diff --git a/.base/test/unit/self_test_yaml_spec.bats b/.base/test/unit/self_test_yaml_spec.bats index 1466b79..203d0e8 100644 --- a/.base/test/unit/self_test_yaml_spec.bats +++ b/.base/test/unit/self_test_yaml_spec.bats @@ -157,10 +157,29 @@ setup() { assert_output --partial "if: needs.classify.outputs.code_changed == 'true'" } -@test "self-test.yaml: behavioural job-level if: gates on code_changed (#317 P1)" { +@test "self-test.yaml: behavioural job-level if: gates on behavioural_relevant (#317 P3)" { + # P1 shipped this with `code_changed` while the behavioural_relevant + # output was emitted-but-unused; P3 tightens to the narrower output so + # PRs that change pure lint / unit-test paths (already covered by + # `test`) don't burn the docker.sock-mounted compose run. run awk '/^ behavioural:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" assert_success - assert_output --partial "if: needs.classify.outputs.code_changed == 'true'" + assert_output --partial "if: needs.classify.outputs.behavioural_relevant == 'true'" + refute_output --partial "if: needs.classify.outputs.code_changed == 'true'" +} + +@test "self-test.yaml: classify behavioural block-list extends to setup.sh + i18n.sh + lib/** + prune.sh (#317 P3 gotcha-5)" { + # setup.sh / lib/** drive .env + compose.yaml generation; i18n.sh + # gates wrapper message output (smoke regressions surface in compose + # logs); prune.sh is part of the wrapper family. All four indirectly + # affect what the docker.sock-mounted compose service does, so they + # must invalidate the behavioural-skip optimization. + run awk '/^ classify:/{flag=1; next} /^ [a-z]/{flag=0} flag' "${WF}" + assert_success + assert_output --partial "'script/docker/setup.sh'" + assert_output --partial "'script/docker/i18n.sh'" + assert_output --partial "'script/docker/lib/**'" + assert_output --partial "'script/docker/prune.sh'" } # ── buildx GHA cache on test-tools builds (#317) ────────────────────── diff --git a/.base/test/unit/stop_sh_spec.bats b/.base/test/unit/stop_sh_spec.bats index cdd8d04..7b190c6 100644 --- a/.base/test/unit/stop_sh_spec.bats +++ b/.base/test/unit/stop_sh_spec.bats @@ -96,6 +96,55 @@ teardown() { assert_output --partial "down" } +# ── --remove-orphans + COMPOSE_PROFILES='*' for profile-gated services (#341) ─ + +@test "stop.sh passes --remove-orphans to compose down (#341)" { + # Profile-gated services (#215 auto-emitted headless / gui / test stages) + # are silently skipped by a bare `compose down`. --remove-orphans catches + # containers from prior compose.yaml shapes the current file no longer + # declares; COMPOSE_PROFILES='*' (env, not argv) activates every profile. + run bash "${SANDBOX}/stop.sh" --dry-run + assert_success + assert_output --partial "--remove-orphans" +} + +@test "stop.sh --all also threads --remove-orphans through each down (#341)" { + printf 'mockuser-mockimg-foo_default\nmockuser-mockimg-bar_default\n' > "${DOCKER_PS_A_FILE}" + run bash "${SANDBOX}/stop.sh" --dry-run --all + assert_success + # --all calls down once per instance; each invocation must carry the flag. + local _count + _count="$(printf '%s\n' "${output}" | grep -c -- '--remove-orphans' || true)" + [[ "${_count}" -ge 2 ]] +} + +# ── -v / --verbose lists project containers before down (#345) ──────────────── + +@test "stop.sh -v lists project containers before down (#345)" { + # Seed the docker stub so _down_one's ps filter returns a non-empty list. + printf 'mockuser-mockimg (running)\n' > "${DOCKER_PS_A_FILE}" + run bash "${SANDBOX}/stop.sh" -v --dry-run + assert_success + assert_output --partial "Tearing down containers in project" + assert_output --partial "mockuser-mockimg (running)" +} + +@test "stop.sh -v with no matching containers prints empty-project hint (#345)" { + : > "${DOCKER_PS_A_FILE}" + run bash "${SANDBOX}/stop.sh" -v --dry-run + assert_success + assert_output --partial "No containers found for project" + refute_output --partial "Tearing down containers in project" +} + +@test "stop.sh without -v does NOT emit the verbose container listing (#345 default)" { + printf 'mockuser-mockimg (running)\n' > "${DOCKER_PS_A_FILE}" + run bash "${SANDBOX}/stop.sh" --dry-run + assert_success + refute_output --partial "Tearing down containers in project" + refute_output --partial "No containers found for project" +} + @test "stop.sh --instance foo stops named instance" { run bash "${SANDBOX}/stop.sh" --dry-run --instance foo assert_success diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1d0a2c4..4b49394 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -36,7 +36,7 @@ jobs: # symlink in the context, exercising the Dockerfile-internal # fallback to `config/demo_bridge.yaml` (closes #65). This keeps # the fallback path under continuous regression coverage. - uses: ycpss91255-docker/base/.github/workflows/multi-distro-build-worker.yaml@v0.29.1 + uses: ycpss91255-docker/base/.github/workflows/multi-distro-build-worker.yaml@v0.29.2 with: image_name: ros1_bridge pr_distros: '["humble"]' @@ -75,6 +75,6 @@ jobs: fail-fast: false matrix: ros2_distro: [humble, jazzy] - uses: ycpss91255-docker/base/.github/workflows/release-worker.yaml@v0.29.1 + uses: ycpss91255-docker/base/.github/workflows/release-worker.yaml@v0.29.2 with: archive_name_prefix: ros1_bridge-${{ matrix.ros2_distro }}