feat: S2 — local and single-site production installer - #281
Draft
acburdine wants to merge 5 commits into
Draft
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
Implements S2 of docs/ghost-cli-replacement.md: a release-selecting bootstrap and a checkout-owned installer, plus the `.ghost-docker.json` reader and writer that S1 deferred to this step. - bootstrap.sh selects a release in semver order (never lexically), clones it, and execs that checkout's install.sh. Bootstrap logic only. - install.sh installs into its own directory: mode-aware preflight, stable project identity, generated passwords, exact Ghost version resolution from the image's own GHOST_VERSION/GHOST_CONTENT/GHOST_INSTALL, private config files, Caddy rendering, readiness verification through the site's real ingress, and a final summary. - Installation never stops or reconfigures anything already running. A chosen port moves out of the way; an explicitly requested busy port, or an occupied 80/443 in production, is an error naming what holds it. - Prompts read /dev/tty and every required input has a flag or environment variable, so --no-prompt is fully scriptable. --no-start starts nothing. - Options for steps that have not landed (--import, --with supervisor, --image-registry, --ghost-channel, --without) exit 3 naming the step rather than being reported as unknown options. - scripts/lib/meta.sh reads and writes .ghost-docker.json at schema v1, refuses a newer schema rather than misreading it, and treats a missing file as a pre-metadata install rather than a broken site. - scripts/site.sh adds list, check/doctor and info. Doctor degrades to useful host-level output when Docker is unreachable. - Read-only Docker probes have deadlines: a wedged daemon answers nothing rather than returning an error, and the check that exists to report that must not be able to hang on it. - Host tool contract recorded in GD_HOST_UTILITIES and enforced by an install run with a PATH built from exactly that list. Docker access is established by asking the daemon, never from docker-group membership. Adds tests/meta.test.mjs, tests/install.test.mjs (no daemon needed) and tests/install-e2e.test.mjs, which installs local and production sites from a candidate release built out of the working tree, runs two independent local sites, and covers port conflicts, an existing proxy, --no-start and --no-prompt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reconciliation Surfaced by the S2 install e2e suite (now 21/21 against real containers). - env.sh/compose.sh: `((n++))`/`((count++))` return exit 1 on the first iteration (post-increment from 0), aborting under `set -e`. install.sh is the first `set -e` caller to write a multi-line env file, so this latent S1 bug only surfaced now. Use `n=$((n + 1))`. Without this, every install died at "Writing configuration". - install_verify_ingress: a plain http probe straight to the loopback Ghost is 301-redirected to HTTPS by design in production (canonical URL is https), so the direct-loopback check now accepts a 3xx there and sends the site's Host header. Caddy's own checks remain the authoritative production verification. Without this, every production install failed its final verification. - install.sh: a release and its prerelease can share a commit, so `git describe --exact-match` can report a different tag than the one bootstrap pinned. Accept --ref when it is among `git tag --points-at HEAD`. - preflight.sh: record `sysctl` in the host-utility contract (macOS memory probe). Test harness: - Central teardown so two local sites stay running together for the simultaneous-answer and distinct-port assertions; no site is downed while a sibling suite still asserts. - makeCandidateRelease tags each release on its own commit and exposes a file:// URL, so shallow clone is honoured without the local-clone warning and `git describe` is unambiguous. - HTTP-redirect check drives the installer's /dev/tcp helper, since Node fetch forbids overriding Host and Caddy would otherwise redirect to 127.0.0.1. - minimum-tools wraps docker/jq so their own subprocess needs run with the full PATH while the shell's coreutil lookups stay restricted to the contract. - realpathSync for the recorded site.dir (macOS /var -> /private/var). docs/ghost-cli-replacement.md: amend §2.10 with when the manager image lands (S4, first tenant backup/restore) and why env/caddy/preflight stay host shell; note the deferred operation lock and the manager-image timing in the S4 step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… CLI at S4 Corrects the earlier "env/caddy never move into the image" framing. The dividing line is daemon-dependency, not config-vs-stateful: - Move onto the manager CLI at S4 (image + dispatcher exist there anyway): env.sh, meta.sh, and caddy_render — pure functions of on-disk files, a straight bash-deletion substitution. - Stay host shell permanently: bootstrap shim, preflight, site.sh check/doctor (must run without the image), and config validate + caddy validate/reload/ verify (they derive answers by asking `docker compose config`; moving them means bundling the Docker CLI in the manager or reimplementing Compose interpolation — the drift "ask Compose" exists to avoid). Records the accepted split (config get/set in the CLI, validate host shell) and why the move waits for S4 rather than S2: net line count is a wash once the Dockerfile, publish pipeline and privileged dispatcher are counted, so it only pays off riding a PR that builds them anyway. Updates the S4 step accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI's shellcheck (Ubuntu 0.9.0) flags what local 0.11 does not: - site.sh: `(($#)) && shift || true` (SC2015) -> `if (($#)); then shift; fi` - docker_responsive: its SECONDS arg is optional and bare calls are the norm, so silence SC2120/SC2119 at the definition rather than every call site. Verified with koalaman/shellcheck:v0.9.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
acburdine
force-pushed
the
claude/s2-installer
branch
from
September 3, 2026 17:34
a3949d7 to
40518c5
Compare
On the Linux CI runner, `docker info --format '{{.ServerVersion}}'` prints
"Cannot connect to the Docker daemon ..." and still exits 0, so preflight_docker
took the version path and reported the connect-error string as an old engine
version instead of "docker daemon not reachable".
Decide reachability from the output: the server version is a dotted number when
the daemon answers, so extract it and treat its absence as unreachable, whatever
the exit code. Keeps the timeout (124) path for a wedged daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements S2 from
docs/ghost-cli-replacement.md(§2.8, §2.2, §2.10): a release-selecting bootstrap and a checkout-owned installer for fresh local and single-site production installs. Stacked on S1 (#280).What landed
bootstrap.sh— curl-able shim, bootstrap logic only. Resolves the newest release on a channel by full semver order (prereleases included), clones at that tag, andexecs that checkout's installer.install.sh— checkout-owned; one checkout is one site. Exact Ghost version resolution (asks the pulled image forGHOST_VERSION/GHOST_CONTENT/GHOST_INSTALL, records the digest), stable project identity, private.env/ghost.env, generated passwords, mode-aware preflight, free-port selection (busy explicit port is an error), optional analytics/activitypub setup, Caddy rendering, readiness + ingress verification, useful summary. Honors--no-prompt/--no-start; prompts read/dev/ttywith a flag equivalent for every required input.--import/supervisor/infra flags exit 3 naming the step, never as unknown options..ghost-docker.jsonreader/writer (scripts/lib/meta.sh) — S1 deferred this here as its first writer. Schema v1 per §2.2; a missing file is "pre-metadata", not broken.scripts/lib/preflight.sh— host-shell preflight/doctor primitives that work when Docker is missing, stopped, or wedged (bounded probes).scripts/lib/install.sh— identity, secrets, image resolution,/dev/ttyprompts, readiness/ingress verification.scripts/site.sh—list,check/doctor,info.Verification
tests/install-e2e.test.mjs(GD_TEST_INSTALL=1): 21/21 against real containers — local via bootstrap, two independent local sites running together, explicit port conflict, existing proxy on 80/443, production HTTPS through Caddy,--no-start, and an install with a PATH of only the recorded minimum tools.Two real bugs surfaced and fixed in the follow-up commit: a
set -eabort from S1's((n++))on the first multi-line env write, and production verification failing on Ghost's by-design http→https loopback redirect. See the commit body.Contract change
Amends §2.10: when the manager image lands (S4, first tenant backup/restore) and why env validation, Caddy generation, preflight and version resolution stay host shell.
🤖 Generated with Claude Code