M7-29: Setup wizard install.sh (installation in one command)¶
Status: ready | Depends on: M1-73, M7-10 | Milestone: M7
Goal¶
After this ticket a stranger installs an instance with exactly one documented command (16 1 goal 1, 3.1, R-OP-20): the POSIX shell script infra/compose/install.sh runs the seven steps of 16 3.1 (preflight check, bundle with checksum by download or --bundle <file>, prompts through /dev/tty or options, .env and secrets/, with the edge direct the gateway of the Compose network in BOTHY_TRUSTED_PROXIES, start with readiness, the check of GET /api/v1/instance/discovery through the published port, the first owner, the hints) and obeys its five rules (a second run overwrites neither .env nor secrets/, no sudo, Linux and macOS, every abort with a non-zero code and the step on standard error, the documented options as the single interface of the portability test). make release-build of the component bothy produces the bundle and the separate artifact install.sh as 18 2.1 rules 1 to 4 require: BOTHY_VERSION from the commit, BOTHY_REGISTRY=ghcr.io/<owner> in lower case from GITHUB_REPOSITORY_OWNER, version, SHA-256 and download address https://github.com/<GITHUB_REPOSITORY>/releases/download/v<version>/ built into install.sh, and an abort without output on every placeholder and missing value. make release-prep COMPONENT=bothy SERVER_VERSION=<version> writes the line BOTHY_VERSION= with its three refusals (ADR 0060 item 6). tests/portability/install-options is the one place of the options the portability test passes, held equal to the second block of 16 3.1 by a check in make check. ShellCheck runs in the gate. The trial release of the product (make release-trial COMPONENT=bothy, D-W-127) builds bundle and install.sh against the local trial registry. Why: one command is the promise of the product to instance operators; every rule of 16 3.1 protects them from a script that changes more than its folder or leaks a secret.
Read first¶
docs/spec/16-operations.md: 1, 3 (rule 7: minimum versions), 3.1 (all steps and rules), 4.3 (directandBOTHY_TRUSTED_PROXIES), 5.3, 5.4, 11.1 (interface ofmake portability, rules 1 to 5), 14 (install.ready_timeout), 15 (R-OP-20).docs/spec/18-release.md: 2 (componentbothy), 2.1 (all rules), 5, 6 (signing of files), 13 (R-RL-09).docs/adr/0060-release-automation-in-the-monorepo.md(item 6),docs/operations/install-compose.md(the manual path, sections 1 to 4),docs/operations/release.md2.2,docs/quality/public-switch.md(step 21, V1-03 replacesOWNER).infra/compose/compose.yaml,infra/compose/.env.example,Makefile(release-build,release-pins,portability),tools/ci/components.py(pins),.github/workflows/release.yml(jobbuildfor the componentbothy).docs/spec/08-identity.md4 (bothy setup bootstrap-owner, enrollment code shown once).docs/DECISIONS.md: D-W-100 and its extension to M7 (D-W-125), the trial release (D-W-127), D-W-76, D-W-95, D-W-98, D-W-99; section 10 (format ofD-M7-29-<n>rows).- Briefs (merged names win, D-W-100):
M1-73-compose-smoke.md(infra/compose/smoke.sh,test_release_bundle_excludes_smoke_overlay, overlays underinfra/compose/test),M7-10-server-release-chain.md(release-prep,release-trial, signing of files).
This ticket touches no table, no action and no event.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples use
https://chat.example.com, the owner handlealexand the nameAlex; the repository isOWNER/bothyuntil V1-03; no company, host or personal names. - Timeouts and versions only through the named values of 16 14 and 18 2.1; code comments name the value, never a number.
- Specification first: a change to the steps, options or bundle rules updates
docs/spec/16-operations.md3.1 and 11.1 anddocs/spec/18-release.md2.1 in the same change. The core contract is not edited by this ticket. Everything this ticket adds is English. - No cost (D-W-125).
- Never tag, never create a release, never publish to a registry (D-W-125); the trial uses the local registry of M7-10 only.
Product requirements implemented here (each is a test in Scope):
- ShellCheck without a finding; POSIX
shonly. - Missing or too old Docker or Compose (16 3 rule 7) aborts before any write; a wrong bundle checksum aborts, also with
--bundle; without a terminal and without a required value the script aborts naming the option; the script never callssudo. - A second run overwrites neither
.envnorsecrets/;.envhas mode0600and contains no secret exceptTS_AUTHKEY;secrets/has mode0700; the enrollment code appears only on the terminal, in no file of the target directory. --storage s3sets the profiles3andBOTHY_STORAGE_DRIVER=s3.- With the edge
directthe gateway of the Compose network is inBOTHY_TRUSTED_PROXIESas/32(IPv6/128); a second run leaves a value set by the instance operator in place; if/api/v1/instance/discoverydoes not answer200through the published port, the script aborts with a hint toBOTHY_TRUSTED_PROXIES. BOTHY_VERSIONandBOTHY_REGISTRYin.envare the values of the bundle's.env.example, not the version ofinstall.sh(16 3.1 step 4).- The version in the documented command in
README.mdanddocs/operations/install-compose.mdequalsVERSION;tests/portability/install-optionsholds exactly the options aftersh -s --of the second block of 16 3.1, only--storageand--bundlemay differ; the check compares the options, not the source of the script (both checks inmake check). make release-buildofbothy(18 2.1) against a fixture repository and a registry stub:BOTHY_VERSIONis the value of the commit,BOTHY_REGISTRYequalsghcr.io/<owner>in lower case, every other file equals the commit; one case each aborts without a bundle and names the value for0.0.0-placeholder, a version without SemVer form, a server pre-release in a stable product, a missingGITHUB_REPOSITORY_OWNER,OWNERin the result and an image fromcompose.yamlmissing in the registry; it sets the download address fromGITHUB_REPOSITORYalso when the source namesOWNER, and aborts withoutGITHUB_REPOSITORYand onOWNERor a placeholder left in the finishedinstall.sh; aninstall.shfrom the fixture downloads its bundle from exactly this address (download stub).make release-prep COMPONENT=bothy SERVER_VERSION=<version>writes the lineBOTHY_VERSION=and aborts without a written file and naming the value without the tagserver/v<version>, withoutCHANNEL=betafor a version with a pre-release identifier, and withoutSERVER_VERSIONwhile the line carries the placeholder (18 2.1 rule 1).test_release_bundle_excludes_smoke_overlayof M1-73 stays green against the fixture repository.
Scope¶
infra/compose/install.sh(POSIX shell, steps and rules of 16 3.1, options of 16 3.1 plus--dir,--bundle,--ready-timeout,--yes).- Tests in
infra/compose/test/install/(bats pinned by digest in a container, or POSIX shell tests in a container) with stubs fordocker,docker compose,curl,cosignandsudo(asudostub that fails the test when called). Makefile:release-buildof the componentbothyper 18 2.1 (Design note 2);release-trial COMPONENT=bothyextends the target of M7-10 (Design note 3).tools/ci/release_prep.py: the optionSERVER_VERSIONforbothywith tests intools/ci/tests/.tests/portability/install-options(the only place of the options) and the two checks of requirement 19 (tools/ci/install_options_check.pywith tests, inmake check)..github/workflows/release.yml: the product path builds bundle andinstall.sh, signs both like every artifact (M7-10), and logs in toghcr.ioalso for the componentbothy, so thatrelease-buildcan check the images.- ShellCheck pinned (
tools-lock) and run overinfra/compose/*.shinmake lint. infra/compose/.env.example(comments only, values stay placeholders),README.mdanddocs/operations/install-compose.md(the documented command with the version),docs/spec/16-operations.md3.1 and 11.1 anddocs/spec/18-release.md2.1 as built,CHANGELOG.mdunder[Unreleased],docs/DECISIONS.mdsection 10 rowsD-M7-29-<n>.
Out of scope¶
- The portability test itself (M7-14); replacing
OWNER(V1-03). - Windows outside WSL 2 (16 3.1 rule 3: the script is verified there only as the build host allows; report it).
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M7-29-setup-wizard; one writer per worktree. - Conventional Commits with the component as scope (for example
feat(compose): ...,ci(release): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag, never publish an image or a release, never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules). Script tests use stubs; a real installation (in the trial) publishes only on
127.0.0.1and is removed at the end with its volumes. - Stop only processes and containers you started, by PID or container ID. Temporary directories only via
mktemp -d, removed at the end. - Never read or print secrets; the tests assert that the enrollment code and generated secrets never reach a file outside
secrets/. - Never lower a threshold, disable a gate or delete a test.
- If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M7 by D-W-125). Where the merged code of an earlier ticket names a package, type, function, file or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Script structure (16 3.1). One function per step, each prefixed in its error messages (
install.sh: preflight: ...); state only in the target folder; the prompts read from/dev/ttyonly when it is a terminal; options always win over prompts;--yestakes defaults but never invents the public address, the owner handle or the owner name. -
release-buildofbothy(18 2.1 rules 2 to 4). In order: read and checkBOTHY_VERSIONof the commit; checkGITHUB_REPOSITORY_OWNERandGITHUB_REPOSITORY; list the images ofcompose.yamlwith${BOTHY_VERSION}and check each exists under the registry (docker manifest inspectorcranepinned; the stub in tests); write the bundle with only the registry line changed; renderinstall.shwith version, checksum and address; scan both forOWNERand placeholders; on any failure delete partial output and exit non-zero naming the value. -
Trial of the product (D-W-127).
make release-trial COMPONENT=bothyruns the server trial of M7-10 first, thenrelease-buildwith the trial registry: a variableRELEASE_TRIAL_REGISTRYreplacesghcr.io/<owner>and is accepted only whenGITHUB_ACTIONSis unset (a test asserts the refusal), the trial download address points at a local static server; the result is a bundle and aninstall.shthat install from the local trial registry, used by M7-14 for its local end-to-end run. Record the variable in 18 2.1 as trial-only. -
Rulings for
docs/DECISIONS.md(D-M7-29-<n>): the test framework for the script, the image existence check tool, the trial registry variable (note 3), the WSL 2 verification state.
Tests (write them first)¶
infra/compose/test/install/: one test per item of requirements 13 to 18 (Docker stub, download stub,sudostub, terminal and no terminal, second run, IPv4 and IPv6 gateway, discovery not200).tools/ci/tests/test_release_build_bothy.py(fixture repository, registry stub): every case of requirement 20.tools/ci/tests/test_release_prep.py: the cases of requirement 21.tools/ci/tests/test_install_options_check.py: equal options pass; a changed option fails; differing--storageand--bundlepass; a changed script source alone does not matter.
Commands that must pass, from the repository root:
uv run --project tools --locked pytest -q tools/ci/tests- the script tests (the target the build adds to
make test, documented indocs/quality/gates.md) make release-trial COMPONENT=bothyon the build hostmake lint,make release-pins,make runners-check,make docs,make linkcheckmake check
Acceptance criteria¶
- Every item of requirements 13 to 22 has a passing test.
-
make release-buildofbothywrites bundle andinstall.shexactly per 18 2.1 and aborts on every listed case without output. -
tests/portability/install-optionsexists and the two checks hold it equal to 16 3.1 and the documented command toVERSION. - The trial release of the product produces an
install.shthat installs from the local trial registry. - 16, 18, the install guide, README, CHANGELOG and the rulings are updated.
-
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol and a header comment per shell function (purpose, arguments, exit codes); comments explain why.
docs/operations/install-compose.mdandREADME.mdshow the one command.- Rulings of Design note 4 recorded in
docs/DECISIONS.mdsection 10. CHANGELOG.mdentry under 'Unreleased'.- An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make checkand of the product trial. - Review and red team verdicts and the findings fixed.
- The documented command as it now stands in the README.
- What is not proven (the public
curlpath, WSL 2). - Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.