{"id":47695756,"url":"https://github.com/ycpss91255-docker/template","last_synced_at":"2026-05-06T05:01:40.760Z","repository":{"id":347404225,"uuid":"1193890190","full_name":"ycpss91255-docker/template","owner":"ycpss91255-docker","description":"Shared template for Docker container repos: scripts, tests, CI workflows","archived":false,"fork":false,"pushed_at":"2026-05-04T09:31:59.000Z","size":1135,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T09:40:01.604Z","etag":null,"topics":["bash","docker","template","tool"],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ycpss91255-docker.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-27T17:29:26.000Z","updated_at":"2026-05-04T09:31:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ycpss91255-docker/template","commit_stats":null,"previous_names":["ycpss91255-docker/docker_template","ycpss91255-docker/template"],"tags_count":66,"template":false,"template_full_name":null,"purl":"pkg:github/ycpss91255-docker/template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Ftemplate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Ftemplate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Ftemplate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Ftemplate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ycpss91255-docker","download_url":"https://codeload.github.com/ycpss91255-docker/template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Ftemplate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32679444,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-06T02:33:58.958Z","status":"ssl_error","status_checked_at":"2026-05-06T02:33:39.611Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash","docker","template","tool"],"created_at":"2026-04-02T16:24:54.655Z","updated_at":"2026-05-06T05:01:40.752Z","avatar_url":"https://github.com/ycpss91255-docker.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# template\n\n[![Self Test](https://github.com/ycpss91255-docker/template/actions/workflows/self-test.yaml/badge.svg)](https://github.com/ycpss91255-docker/template/actions/workflows/self-test.yaml)\n[![codecov](https://codecov.io/gh/ycpss91255-docker/template/branch/main/graph/badge.svg)](https://codecov.io/gh/ycpss91255-docker/template)\n\n![Language](https://img.shields.io/badge/Language-Bash-blue?style=flat-square)\n![Testing](https://img.shields.io/badge/Testing-Bats-orange?style=flat-square)\n![ShellCheck](https://img.shields.io/badge/ShellCheck-Compliant-brightgreen?style=flat-square)\n![Coverage](https://img.shields.io/badge/Coverage-Kcov-blueviolet?style=flat-square)\n[![License](https://img.shields.io/badge/License-GPL--3.0-yellow?style=flat-square)](./LICENSE)\n\nShared template for Docker container repos in the [ycpss91255-docker](https://github.com/ycpss91255-docker) organization.\n\n**[English](README.md)** | **[繁體中文](doc/readme/README.zh-TW.md)** | **[简体中文](doc/readme/README.zh-CN.md)** | **[日本語](doc/readme/README.ja.md)**\n\n---\n\n## Table of Contents\n\n- [TL;DR](#tldr)\n- [Overview](#overview)\n- [Quick Start](#quick-start)\n- [CI Reusable Workflows](#ci-reusable-workflows)\n- [Running Template Tests](#running-template-tests)\n- [Tests](#tests)\n- [Directory Structure](#directory-structure)\n\n---\n\n## TL;DR\n\n```bash\n# New repo from scratch: init + first commit + subtree + init.sh\nmkdir \u003crepo_name\u003e \u0026\u0026 cd \u003crepo_name\u003e\ngit init\ngit commit --allow-empty -m \"chore: initial commit\"\ngit subtree add --prefix=template \\\n    https://github.com/ycpss91255-docker/template.git main --squash\n./template/init.sh\n\n# Upgrade to latest\nmake upgrade-check   # check\nmake upgrade         # pull + update version + workflow tag\n\n# Run CI\nmake test            # ShellCheck + Bats + Kcov\nmake help            # show all commands\n```\n\n## Overview\n\nThis repo consolidates shared scripts, tests, and CI workflows used across all Docker container repos. Instead of maintaining identical files in 15+ repos, each repo pulls this template as a **git subtree** and uses symlinks.\n\n### Architecture\n\n```mermaid\ngraph TB\n    subgraph template[\"template (shared repo)\"]\n        scripts[\".hadolint.yaml / Makefile.ci / compose.yaml\"]\n        smoke[\"test/smoke/\u003cbr/\u003escript_help.bats\u003cbr/\u003edisplay_env.bats\"]\n        config[\"config/\u003cbr/\u003ebashrc / tmux / terminator / pip\"]\n        mgmt[\"script/docker/\u003cbr/\u003ebuild.sh / run.sh / exec.sh / stop.sh / setup.sh\"]\n        workflows[\"Reusable Workflows\u003cbr/\u003ebuild-worker.yaml\u003cbr/\u003erelease-worker.yaml\"]\n    end\n\n    subgraph consumer[\"Docker Repo (e.g. ros_noetic)\"]\n        symlinks[\"build.sh → template/script/docker/build.sh\u003cbr/\u003erun.sh → template/script/docker/run.sh\u003cbr/\u003eexec.sh / stop.sh / .hadolint.yaml\"]\n        dockerfile[\"Dockerfile\u003cbr/\u003ecompose.yaml\u003cbr/\u003e.env.example\u003cbr/\u003escript/entrypoint.sh\"]\n        repo_test[\"test/smoke/\u003cbr/\u003eros_env.bats (repo-specific)\"]\n        main_yaml[\"main.yaml\u003cbr/\u003e→ calls reusable workflows\"]\n    end\n\n    template -- \"git subtree\" --\u003e consumer\n    scripts -. symlink .-\u003e symlinks\n    smoke -. \"Dockerfile COPY\" .-\u003e repo_test\n    workflows -. \"@tag reference\" .-\u003e main_yaml\n```\n\n### CI/CD Flow\n\n```mermaid\nflowchart LR\n    subgraph local[\"Local\"]\n        build_test[\"./build.sh test\"]\n        make_test[\"make test\"]\n    end\n\n    subgraph ci_container[\"CI Container (kcov/kcov)\"]\n        shellcheck[\"ShellCheck\"]\n        hadolint[\"Hadolint\"]\n        bats[\"Bats smoke tests\"]\n    end\n\n    subgraph github[\"GitHub Actions\"]\n        build_worker[\"build-worker.yaml\u003cbr/\u003e(from template)\"]\n        release_worker[\"release-worker.yaml\u003cbr/\u003e(from template)\"]\n    end\n\n    build_test --\u003e ci_container\n    make_test --\u003e|\"script/ci/ci.sh\"| ci_container\n    shellcheck --\u003e hadolint --\u003e bats\n\n    push[\"git push / PR\"] --\u003e build_worker\n    build_worker --\u003e|\"docker build test\"| ci_container\n    tag[\"git tag v*\"] --\u003e release_worker\n    release_worker --\u003e|\"tar.gz + zip\"| release[\"GitHub Release\"]\n```\n\n### What's included\n\n| File | Description |\n|------|-------------|\n| `build.sh` | Build containers (TTY-aware `--setup` launches `setup_tui.sh`, else runs `setup.sh`) |\n| `run.sh` | Run containers (X11/Wayland support; same `--setup` semantics as `build.sh`; `--build` opt-in pre-flight ./build.sh test for fresh-clone CI parity) |\n| `exec.sh` | Exec into running containers |\n| `stop.sh` | Stop and remove containers |\n| `setup_tui.sh` | Interactive setup.conf editor (dialog / whiptail front-end) |\n| `script/docker/setup.sh` | Auto-detect system parameters and generate `.env` + `compose.yaml` |\n| `script/docker/_tui_backend.sh` | dialog/whiptail wrapper functions used by `setup_tui.sh` |\n| `script/docker/_tui_conf.sh` | INI validators + read/write for `setup_tui.sh` and `setup.sh` writeback |\n| `script/docker/_lib.sh` | Shared helpers (`_load_env`, `_compose`, `_compose_project`, ...) |\n| `script/docker/i18n.sh` | Shared language detection (`_detect_lang`, `_LANG`) |\n| `config/` | Container-internal shell configs (bashrc, tmux, terminator, pip) |\n| `setup.conf` | Single per-repo runtime configuration (image / build / deploy / gui / network / volumes) |\n| `test/smoke/` | Shared smoke tests + runtime assertion helpers (see below) |\n| `test/unit/` | Template self-tests (bats + kcov) |\n| `test/integration/` | Level-1 `init.sh` end-to-end tests |\n| `.hadolint.yaml` | Shared Hadolint rules |\n| `Makefile` | Repo entry (`make build`, `make run`, `make stop`, etc.) |\n| `Makefile.ci` | Template CI entry (`make test`, `make -f Makefile.ci lint`, etc.) |\n| `init.sh` | First-time symlink setup + new-repo scaffolding |\n| `upgrade.sh` | Subtree version upgrade |\n| `script/ci/ci.sh` | CI pipeline (local + remote) |\n| `dockerfile/Dockerfile.example` | Multi-stage Dockerfile template for new repos |\n| `dockerfile/Dockerfile.test-tools` | Pre-built lint/test tools image (shellcheck, hadolint, bats, bats-mock) |\n| `.github/workflows/` | Reusable CI workflows (build + release) |\n\n### Dockerfile stages (convention)\n\nDownstream repos follow a standard multi-stage layout, defined in\n`dockerfile/Dockerfile.example`. All stages share a common base image\nparameterized by `ARG BASE_IMAGE`.\n\n| Stage | Parent | Purpose | Shipped? |\n|-------|--------|---------|----------|\n| `sys` | `${BASE_IMAGE}` | User/group, sudo, timezone, locale, APT mirror | intermediate |\n| `base` | `sys` | Development tools and language packages | intermediate |\n| `devel` | `base` | App-specific tools + `entrypoint.sh` + PlotJuggler (env repos) | **yes** (primary artifact) |\n| `test` | `devel` | Ephemeral: ShellCheck + Hadolint + Bats smoke (all from `test-tools:local`) | no (discarded) |\n| `runtime-base` (optional) | `sys` | Minimal runtime deps (sudo, tini) | intermediate |\n| `runtime` (optional) | `runtime-base` | Slim runtime image (application repos only) | yes, when enabled |\n\nNotes:\n- Repos that only ship a developer image (`env/*`) skip `runtime-base` /\n  `runtime` — the section stays commented in `Dockerfile.example`.\n- `test` is always built from `devel`, so runtime assertions inside\n  `test/smoke/\u003crepo\u003e_env.bats` see the same binaries / files a user would\n  find after `docker run ... \u003crepo\u003e:devel`.\n- `Dockerfile.test-tools` builds the lint/test tool bundle (bats + shellcheck +\n  hadolint). The downstream `test` stage consumes it through an `ARG\n  TEST_TOOLS_IMAGE` build arg — defaults to `test-tools:local` (matches the\n  local `./build.sh` flow that builds `Dockerfile.test-tools` into the host\n  Docker daemon). CI overrides it to\n  `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z` (pre-built multi-arch image\n  pushed by `.github/workflows/release-test-tools.yaml` on every tag) so\n  buildx pulls the arch-correct binaries over the wire instead of rebuilding\n  them per run, and sidesteps the cross-step image-store isolation that\n  `docker-container` buildx drivers enforce.\n\n#### Adding extra stages (#215)\n\nAny `FROM \u003cbase\u003e AS \u003cstage\u003e` outside the baseline blocklist\n`{sys, base, devel, test}` is auto-emitted as a compose service that\n`extends: devel` (inherits volumes / network / GPU / GUI / cap_add /\nadditional_contexts) and overrides only `build.target` / `image` /\n`container_name` / `stdin_open` / `tty` / `profiles`. Use case:\nentrypoint variants like NVIDIA Isaac Sim's `headless` + `gui` on top\nof `devel`.\n\nUser flow:\n\n```dockerfile\n# Add to Dockerfile (no setup.conf change needed)\nFROM devel AS headless\nENTRYPOINT [\"/isaac-sim/runheadless.sh\"]\nCMD [\"-v\"]\n\nFROM devel AS gui\nENTRYPOINT [\"/isaac-sim/runapp.sh\"]\n```\n\n```bash\n./build.sh                    # regenerates compose.yaml, builds all stages\n./run.sh -t headless          # runs the headless variant\n./run.sh -t gui               # runs the gui variant\n./exec.sh -t headless bash    # exec into running headless container\n```\n\nConstraints:\n\n- Stage names must match `^[a-z][a-z0-9_-]*$` — uppercase / leading\n  digit / dot etc. are rejected (WARN + skip; the rest of the parse\n  continues).\n- Names colliding with the baseline (`sys` / `base` / `devel` / `test`)\n  are a hard error from `setup.sh apply`. So are names colliding with\n  the template-controlled image-tag namespace (`latest`, `v[0-9]*`).\n- Per-stage diff (different volumes / GPU / network than `devel`) is\n  out of scope — declare via Dockerfile `ARG` + conditional `RUN`\n  instead. The `extends` baseline is the same for every emitted stage.\n- Adding / removing a stage triggers `setup.sh check-drift` (via\n  `SETUP_DOCKERFILE_HASH` in `.env`), so wrappers auto-regenerate\n  `compose.yaml` on the next invocation. Unrelated `RUN apt-get\n  install` edits do **not** trigger drift.\n\n### Smoke test helpers (for downstream repos)\n\n`test/smoke/test_helper.bash` (loaded by every smoke spec via\n`load \"${BATS_TEST_DIRNAME}/test_helper\"`) ships a small set of runtime\nassertion helpers. Downstream repos should prefer these over ad-hoc\n`[ -f ... ]` / `command -v` checks so failures produce decorated\ndiagnostics pointing at the missing artifact.\n\n| Helper | Usage |\n|--------|-------|\n| `assert_cmd_installed \u003ccmd\u003e` | Fails unless `\u003ccmd\u003e` is on `PATH` |\n| `assert_cmd_runs \u003ccmd\u003e [flag]` | Fails unless `\u003ccmd\u003e \u003cflag\u003e` exits 0 (default flag: `--version`) |\n| `assert_file_exists \u003cpath\u003e` | Fails unless `\u003cpath\u003e` is a regular file |\n| `assert_dir_exists \u003cpath\u003e` | Fails unless `\u003cpath\u003e` is a directory |\n| `assert_file_owned_by \u003cuser\u003e \u003cpath\u003e` | Fails unless `\u003cpath\u003e`'s owner is `\u003cuser\u003e` |\n| `assert_pip_pkg \u003cpkg\u003e` | Fails unless `pip show \u003cpkg\u003e` returns 0 |\n\n### What stays in each repo (not shared)\n\n- `Dockerfile`\n- `compose.yaml`\n- `.env.example`\n- `script/entrypoint.sh`\n- `doc/` and `README.md`\n- Repo-specific smoke tests\n\n## Per-repo runtime configuration\n\nEach downstream repo drives its runtime config — GPU reservation, GUI\nenv/volumes, network mode, extra volume mounts — through a single\n`setup.conf` INI file. `setup.sh` reads it (plus system detection) and\nregenerates both `.env` and `compose.yaml`; users never hand-edit those\ntwo derived artifacts.\n\n### One conf, six sections\n\n```\n[image]    rules = prefix:docker_, suffix:_ws, @default:unknown\n[build]    apt_mirror_ubuntu, apt_mirror_debian            # Dockerfile build args\n[deploy]   gpu_mode (auto|force|off), gpu_count, gpu_capabilities\n[gui]      mode (auto|force|off)\n[network]  mode (host|bridge|none), ipc, privileged\n[volumes]  mount_1 (workspace, auto-populated on first run)\n           mount_2..mount_N (extra host mounts; devices via /dev path)\n```\n\nTemplate default lives at `template/setup.conf`; per-repo overrides go\nat `\u003crepo\u003e/setup.conf`. Section-level **replace** strategy: a section\npresent in the per-repo file fully replaces the template's section;\nomitted sections fall back to template.\n\nOn first `setup.sh` run (no per-repo setup.conf yet), the template file\nis copied to the repo and the detected workspace is written to\n`[volumes] mount_1`. Subsequent runs read `mount_1` as source of truth\n— clear it to opt out of mounting a workspace. Edit via:\n\n```bash\n./setup_tui.sh                      # interactive dialog/whiptail editor\n./setup_tui.sh volumes              # jump directly to one section\n./build.sh --setup            # launches setup_tui.sh under TTY; setup.sh otherwise\n./template/init.sh --gen-conf # plain copy of template/setup.conf to repo root\n```\n\n### Interactive TUI\n\n`./setup_tui.sh` opens the main menu and lets you edit values across all sections; the backend is `dialog` or `whiptail` (when both are missing it prints a `sudo apt install dialog` hint and exits). Cancel / Esc leaves without saving; saving auto-invokes `setup.sh` to regenerate `.env` + `compose.yaml`.\n\n### When setup.sh runs\n\n`setup.sh` runs only when explicitly triggered — it is not re-run on\nevery build or launch:\n\n- **`./template/init.sh`** runs it once after the skeleton lands\n- **`make upgrade` / `./template/upgrade.sh`** re-runs it via init.sh\n  after the subtree pull, so an upgrade always lands with `.env` /\n  `compose.yaml` regenerated against the new baseline\n- **`./build.sh --setup` / `./run.sh --setup`** (or `-s`) re-runs it on demand\n- **First-time bootstrap**: `./build.sh` / `./run.sh` auto-run setup.sh\n  the very first time (when `.env` is missing, e.g. after a fresh CI\n  clone) — no manual `--setup` needed\n\n\u003e **Fresh-clone lint coverage (#216)**: `./run.sh` on a clone with no\n\u003e image cached locally triggers Compose's auto-build, which only walks\n\u003e `target: devel` (or whatever `-t` says) and **skips** the `target:\n\u003e test` stage that runs ShellCheck / Hadolint / Bats smoke. `run.sh`\n\u003e prints an informational `[run] INFO:` block when this is about to\n\u003e happen (TTY only). Pass `--build` to pre-flight `./build.sh test`\n\u003e first if you want full local-CI parity in one command:\n\u003e\n\u003e ```bash\n\u003e ./build.sh test           # explicit lint + smoke pass\n\u003e ./run.sh --build          # same, then compose up\n\u003e ./run.sh                  # default — fast path, lint/smoke skipped\n\u003e ```\n\n`setup.sh apply` rewrites `compose.yaml` from scratch every time but\npreserves `WS_PATH` / `APT_MIRROR_UBUNTU` / `APT_MIRROR_DEBIAN` from any\nexisting `.env`, so a hand-tuned workspace path or apt mirror survives\nupgrades.\n\n### Drift detection\n\n`setup.sh` stores `SETUP_CONF_HASH`, `SETUP_GUI_DETECTED`, and\n`SETUP_TIMESTAMP` in `.env`. On every `./build.sh` / `./run.sh`,\nstored values are compared against the current setup.conf hash + system\ndetection; a `[WARNING]` is printed (non-blocking) when any of the\nfollowing changed since last setup:\n\n- `setup.conf` contents (conf hash)\n- GPU / GUI detection\n- `USER_UID` (user identity change)\n\nRe-run with `--setup` to regenerate `.env` + `compose.yaml`.\n\n### setup.sh subcommands (v0.11.0+)\n\n`setup.sh` is a git-style backend with explicit subcommands. The build / run / TUI scripts call it for you; invoke directly for scripted / non-interactive use:\n\n| Subcommand | Use |\n|---|---|\n| `apply` | Regenerate `.env` + `compose.yaml` from setup.conf + system detection |\n| `check-drift` | Exit 0 in-sync / 1 drifted (drift descriptions on stderr) |\n| `set \u003csection\u003e.\u003ckey\u003e \u003cvalue\u003e` | Write a single key |\n| `show \u003csection\u003e[.\u003ckey\u003e]` | Read single key or whole section |\n| `list [\u003csection\u003e]` | INI-style dump |\n| `add \u003csection\u003e.\u003clist\u003e \u003cvalue\u003e` | Append to list-style section (`mount_*` / `env_*` / `port_*` / …); reuses next empty slot or `max+1` |\n| `remove \u003csection\u003e.\u003ckey\u003e` / `\u003csection\u003e.\u003clist\u003e \u003cvalue\u003e` | Delete by exact key, or by value match |\n| `reset [-y\\|--yes]` | Restore template default; archives prior `setup.conf` → `setup.conf.bak`, prior `.env` → `.env.bak` |\n\nTyped keys validate against `_tui_conf.sh` validators (the same ones the TUI uses). `set` / `add` / `remove` / `reset` do **not** regenerate `.env` — chain `apply` afterwards, or `build.sh` / `run.sh` will trigger drift-regen on next invocation.\n\n#### Migration from v0.10.x (BREAKING)\n\n`setup.sh` (no args) and `setup.sh --base-path X --lang Y` (no subcommand) used to silently fall through to `apply`. v0.11.0 removes that fall-through:\n\n| Invocation | Pre-v0.11 | v0.11+ |\n|---|---|---|\n| `setup.sh` | runs apply | prints help, exits 0 |\n| `setup.sh --base-path X --lang Y` | runs apply | exit 1 \"Unknown subcommand\" |\n| `setup.sh apply [...]` | runs apply | runs apply (unchanged) |\n\nIf a downstream repo has custom scripts invoking `setup.sh` directly, prepend `apply`. The bundled `build.sh` / `run.sh` / `init.sh` / `setup_tui.sh` are already updated.\n\n### Derived artifacts (gitignored)\n\n- `.env` — runtime variable values + `SETUP_*` drift metadata\n- `compose.yaml` — full compose with baseline + conditional blocks\n\nOpen `compose.yaml` anytime to inspect the repo's current effective\nconfiguration. Both files are regenerated on every `make upgrade`\n(init.sh re-runs `setup.sh apply` after the subtree pull) — never\nhand-edit them; put your overrides in `setup.conf` instead.\n\n## Quick Start\n\n### Adding to a new repo\n\n```bash\n# 1. Initialize empty repo (skip if you already have one with at least one commit)\nmkdir \u003crepo_name\u003e \u0026\u0026 cd \u003crepo_name\u003e\ngit init\ngit commit --allow-empty -m \"chore: initial commit\"\n\n# 2. Add subtree\ngit subtree add --prefix=template \\\n    https://github.com/ycpss91255-docker/template.git main --squash\n\n# 3. Initialize symlinks (one command; runs setup.sh under the hood)\n./template/init.sh\n```\n\n\u003e `git subtree add` requires `HEAD` to exist. On a freshly `git init`-ed repo with no commits, it fails with `ambiguous argument 'HEAD'` and `working tree has modifications`. The empty commit creates `HEAD` so subtree can merge into it.\n\n### Updating\n\nPrerequisites: `git config user.name` / `user.email` must be set, and\nthe working tree can't be mid-merge / rebase / cherry-pick / revert —\nupgrade.sh fails fast with an actionable message instead of half-pulling.\n\n```bash\n# Check if update available\nmake upgrade-check\n\n# Upgrade to latest (subtree pull + version file + workflow tag)\nmake upgrade\n\n# Or pin a specific version\nmake upgrade VERSION=v0.3.0\n# Pinning to a version OLDER than the current local pin (e.g. rolling\n# from v0.12.0-rc1 back to v0.11.0) is refused as an implicit downgrade\n# per SemVer §11. Edit template/.version manually if intentional.\n\n# Fallback if make is unavailable\n./template/upgrade.sh v0.3.0\n```\n\n`upgrade.sh` handles the full cycle in one go:\n\n1. `git subtree pull --prefix=template ... --squash`\n2. Post-pull integrity check — `git reset --hard` rollback if subtree\n   markers (`template/.version`, `template/init.sh`,\n   `template/script/docker/setup.sh`) are missing (catches the\n   destructive fast-forward seen on older `git-subtree.sh`)\n3. `./template/init.sh` re-runs to: resync root symlinks\n   (`build.sh` / `run.sh` / `Makefile` …), sync `.gitignore` against\n   the canonical entry set, `git rm --cached` any tracked-but-now-derived\n   files (`.env`, `compose.yaml`, …), and call `setup.sh apply` to\n   regenerate `.env` + `compose.yaml`\n4. `sed` rewrites `.github/workflows/main.yaml`'s\n   `build-worker.yaml@vX.Y.Z` / `release-worker.yaml@vX.Y.Z` refs\n\nYour per-repo files are never overwritten: `\u003crepo\u003e/setup.conf` stays\nas-is, and `\u003crepo\u003e/config/` (bashrc / tmux / terminator …) is left\nalone — if upstream `template/config/` moved since the last pull,\nupgrade.sh prints a `diff -ruN template/config config` hint so you can\nreconcile manually.\n\nDon't `git subtree pull` by hand — the integrity check, init.sh\nresync, and sed steps are easy to forget.\n\n#### Automated version bumps (optional)\n\nDownstream repos can let Dependabot open PRs whenever a new `template` tag\nships. Add `.github/dependabot.yml`:\n\n```yaml\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n```\n\nDependabot notices the `uses: ycpss91255-docker/template/...@vX.Y.Z` refs in\n`main.yaml`, compares against the template's latest tag, and files a PR. You\nstill run `make upgrade VERSION=vX.Y.Z` locally to sync the subtree itself —\nDependabot only bumps the workflow refs.\n\n## CI Reusable Workflows\n\nRepos replace local `build-worker.yaml` / `release-worker.yaml` with calls to this repo's reusable workflows:\n\n```yaml\n# .github/workflows/main.yaml\njobs:\n  call-docker-build:\n    uses: ycpss91255-docker/template/.github/workflows/build-worker.yaml@v1\n    with:\n      image_name: ros_noetic\n      build_args: |\n        ROS_DISTRO=noetic\n        ROS_TAG=ros-base\n        UBUNTU_CODENAME=focal\n\n  call-release:\n    needs: call-docker-build\n    if: startsWith(github.ref, 'refs/tags/')\n    uses: ycpss91255-docker/template/.github/workflows/release-worker.yaml@v1\n    with:\n      archive_name_prefix: ros_noetic\n```\n\n### build-worker.yaml inputs\n\n| Input | Type | Required | Default | Description |\n|-------|------|----------|---------|-------------|\n| `image_name` | string | yes | - | Container image name |\n| `build_args` | string | no | `\"\"` | Multi-line KEY=VALUE build args |\n| `build_runtime` | boolean | no | `true` | Whether to build runtime stage |\n| `platforms` | string | no | `\"linux/amd64\"` | Comma-separated target platforms; each runs as a parallel native-runner shard (`linux/amd64` → ubuntu-latest, `linux/arm64` → ubuntu-24.04-arm) |\n| `test_tools_version` | string | no | `\"latest\"` | Tag for `ghcr.io/ycpss91255-docker/test-tools:\u003ctag\u003e` build-arg; pin to the template release you upgraded from for reproducibility |\n\n### release-worker.yaml inputs\n\n| Input | Type | Required | Default | Description |\n|-------|------|----------|---------|-------------|\n| `archive_name_prefix` | string | yes | - | Archive name prefix |\n| `extra_files` | string | no | `\"\"` | Space-separated extra files |\n\n## Running Template Tests\n\nUsing `Makefile.ci` (from template root):\n```bash\nmake -f Makefile.ci test        # Full CI (ShellCheck + Bats + Kcov) via docker compose\nmake -f Makefile.ci lint        # ShellCheck only\nmake -f Makefile.ci clean       # Remove coverage reports\nmake help        # Show repo targets\nmake -f Makefile.ci help  # Show CI targets\n```\n\nOr directly:\n```bash\n./script/ci/ci.sh          # Full CI via docker compose\n./script/ci/ci.sh --ci     # Run inside container (used by compose)\n```\n\n## Tests\n\nSee [TEST.md](doc/test/TEST.md) for details.\n\n## Directory Structure\n\n```\ntemplate/\n├── init.sh                           # Initialize repo (new or existing)\n├── upgrade.sh                        # Upgrade template subtree version\n├── script/\n│   ├── docker/                       # Docker operation scripts (symlinked by repos)\n│   │   ├── build.sh\n│   │   ├── run.sh\n│   │   ├── exec.sh\n│   │   ├── stop.sh\n│   │   ├── setup.sh                  # .env generator\n│   │   ├── _lib.sh                   # Shared helpers (_load_env, _compose, _compose_project)\n│   │   ├── i18n.sh                   # Shared language detection (_detect_lang, _LANG)\n│   │   └── Makefile\n│   └── ci/\n│       └── ci.sh                     # CI pipeline (local + remote)\n├── dockerfile/\n│   ├── Dockerfile.test-tools         # Pre-built lint/test tools image\n│   └── Dockerfile.example            # Dockerfile template for new repos (sys → base → devel → test → [runtime])\n├── setup.conf                        # Single runtime config (per-repo override mirror: \u003crepo\u003e/setup.conf)\n├── config/                           # Container-internal shell/tool configs\n│   ├── image_name.conf               # Default IMAGE_NAME detection rules\n│   ├── pip/\n│   │   ├── setup.sh\n│   │   └── requirements.txt\n│   └── shell/\n│       ├── bashrc\n│       ├── terminator/\n│       │   ├── setup.sh\n│       │   └── config\n│       └── tmux/\n│           ├── setup.sh\n│           └── tmux.conf\n├── test/\n│   ├── smoke/                        # Shared smoke tests + runtime assertion helpers\n│   │   ├── test_helper.bash          #  → assert_cmd_installed / _runs / file / dir / owned_by / pip_pkg\n│   │   ├── script_help.bats\n│   │   └── display_env.bats\n│   ├── unit/                         # Template self-tests (bats + kcov)\n│   │   ├── test_helper.bash\n│   │   ├── bashrc_spec.bats\n│   │   ├── ci_spec.bats              # ci.sh _install_deps\n│   │   ├── lib_spec.bats             # _lib.sh\n│   │   ├── pip_setup_spec.bats\n│   │   ├── setup_spec.bats\n│   │   ├── smoke_helper_spec.bats    # Runtime assertion helpers\n│   │   ├── template_spec.bats\n│   │   ├── terminator_config_spec.bats\n│   │   ├── terminator_setup_spec.bats\n│   │   ├── tmux_conf_spec.bats\n│   │   └── tmux_setup_spec.bats\n│   └── integration/\n│       └── init_new_repo_spec.bats   # Level-1 init.sh end-to-end\n├── Makefile.ci                       # Template CI entry (make test/lint/...)\n├── compose.yaml                      # Docker CI runner\n├── .hadolint.yaml                    # Shared Hadolint rules\n├── codecov.yml\n├── .github/workflows/\n│   ├── self-test.yaml                # Template CI\n│   ├── build-worker.yaml             # Reusable build workflow\n│   └── release-worker.yaml           # Reusable release workflow\n├── doc/\n│   ├── readme/                       # README translations (zh-TW / zh-CN / ja)\n│   ├── test/TEST.md                  # Test catalog (spec tables)\n│   └── changelog/CHANGELOG.md        # Release notes\n├── .gitignore\n├── LICENSE\n└── README.md\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fycpss91255-docker%2Ftemplate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fycpss91255-docker%2Ftemplate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fycpss91255-docker%2Ftemplate/lists"}