Repository navigation
feat(bricks): add-on-demand infrastructure bricks and a nested service - #19
Conversation
The template carries every optional piece (backups, Alloy, Grafana) next to the core, gated by profiles and documented four times over, so a new project inherits all of it and an agent has to read all of it to find one piece. This spec turns optional infrastructure into self-contained bricks under bricks/<name>/ that a single command wires in or out, and nests the service under services/app so other things can sit beside it. Rulings taken while the user slept are listed in section 10 of the spec. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Ten tasks: nest the service, wire .bricks into Make/Gradle/tests, write brick.sh, extract observability and backup, generate every prod password, add the minio brick, verify every brick in CI, rewrite the docs, run the acceptance round trips. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Gradle becomes a multi-project build that discovers services/*; the root keeps plugin versions and formatting. Other things (bricks, later more services) can now sit beside the service instead of inside it. Co-Authored-By: Claude Fable 5.1 <[email protected]>
…e test runner One file lists what is on. Make includes each enabled brick's targets, the service build applies each enabled brick's dependencies, and script tests under bricks/*/tests run regardless so a disabled brick cannot rot. Co-Authored-By: Claude Opus 5 <[email protected]>
Presence of a file in bricks/<name>/ decides what gets wired: compose include lines, env marker blocks, Java copied into the service package, a skill, the lockfile. Both directions are idempotent so a repeated command is harmless. Co-Authored-By: Claude Opus 5 <[email protected]>
… brick Metrics collection, dashboards and alerts are optional per project. They now live in bricks/observability and are wired by `make add BRICK=observability` instead of a compose profile and four documents. Co-Authored-By: Claude Opus 5 <[email protected]>
The sidecar, its targets, tests and runbook now live in bricks/backup and are wired by `make add BRICK=backup`. vps.sh reads .bricks on the host instead of COMPOSE_PROFILES to know whether to report backup freshness. deploy-config.test.sh carries three minio assertions commented out until the minio brick lands. Its per-service awk range ended on its own first line, so it is replaced by a small svc_block helper. Co-Authored-By: Claude Opus 5 <[email protected]>
…p brick The rewrite of deploy-config.test.sh dropped the check that rclone tokens never enter the docker build context, and the sh -n/shellcheck pass on backup.sh. The context is now bricks/backup, so the guards run there again. Also make the all-bricks scratch copy tolerate deleted files and vps.sh read .bricks independent of VPS_APP_DIR spelling. Co-Authored-By: Claude Opus 5 <[email protected]>
A brick (minio first) can ask for a secret by leaving `X_PASSWORD=` empty in its env block; prod-init fills it on the host, so no brick secret is ever typed or committed. Postgres is no longer the only generated password. Co-Authored-By: Claude Opus 5 <[email protected]>
A failing openssl inside a command substitution was silently ignored, so .env.prod could be written with an empty POSTGRES_PASSWORD. The file is now built in memory and written only after every password is generated, and a final line without a trailing newline is no longer dropped. Co-Authored-By: Claude Opus 5 <[email protected]>
First brick that carries Java: proves that compose, env, dependencies, main code, tests and a skill can all come from one folder and leave no residue on removal. The AWS SDK client works unchanged against Hetzner Object Storage. The image is Chainguard's MinIO build because MinIO Inc. no longer publishes minio/minio. Co-Authored-By: Claude Opus 5 <[email protected]>
…rip clean The health probe listed all buckets, which bucket-scoped credentials on managed stores forbid, leaving the app permanently DOWN; it now heads the configured bucket. SDK calls had no timeout, so a hung store could hold a probe past the compose interval. Missing buckets read as 404 and racing bucket creation is tolerated. The lockfile header is regenerated so add/remove leaves no diff, and the deploy-config and init-prod-env script tests no longer break when a brick is enabled (core render comes from a brick-free scratch copy; demo block renamed). Co-Authored-By: Claude Opus 5 <[email protected]>
Bricks are inert until enabled, so a plain build cannot notice one that rotted; adding, building and removing each one on its own runner catches that. The job is deliberately not a gate for image, so push to live stays as fast. Co-Authored-By: Claude Opus 5 <[email protected]>
The root documents shrink to the core; each brick's README is its runbook and bricks/README.md is the contract for writing one, so an agent reads one folder to work on one component. Co-Authored-By: Claude Opus 5 <[email protected]>
… apart A failed lockfile refresh used to leave add/remove half-applied with no hint, so the message now names the recovery commands. Names such as '..' passed the directory check, and a .bricks without a final newline got the name glued on. Co-Authored-By: Claude Opus 5 <[email protected]>
grep -q exits on the first match, git log can die of SIGPIPE, and pipefail then reported no match, skipping a major bump intermittently. Co-Authored-By: Claude Opus 5 <[email protected]>
In a derived project the committed brick made add a no-op and remove delete tracked files, so the clean-tree check failed on every push; the build job already covers an enabled brick. Co-Authored-By: Claude Opus 5 <[email protected]>
SERVICE doubled as both, so bare make prod-logs tailed only app and the help text lied. GRADLE_SERVICE now names the Gradle project; vps.sh no longer passes an empty service name to compose. Co-Authored-By: Claude Opus 5 <[email protected]>
The generated record toString printed secretKey, which CLAUDE.md forbids in logs; it is now masked and a unit test pins that. Co-Authored-By: Claude Opus 5 <[email protected]>
The all-bricks render merges partial app services; losing the Watchtower labels would silently stop deploys, so the test now pins them. Co-Authored-By: Claude Opus 5 <[email protected]>
… upgrade Enabling a brick needs git pull, an env block and make prod-up on the host right after the push, or a Java brick rolls into a host without its service. Also restores the Telegram secrets row, states the include: form brick.sh manages and fixes a garbled checklist step. Co-Authored-By: Claude Opus 5 <[email protected]>
…f copying it Copying Java into services/app meant package rewriting, a Spotless pass on add, and brick code that silently diverged from its source. A brick with bricks/<name>/java/build.gradle.kts is now project 🧱<name> while listed in .bricks, so add/remove touch no service source and the brick is built and tested on its own. Co-Authored-By: Claude Opus 5 <[email protected]>
…ject The brick no longer reaches into the application: beans come from an auto-configuration (each @ConditionalOnMissingBean so an app can override), and a missing key is a ResponseStatusException instead of the app's ApplicationException. Its own build file and lockfile own the AWS SDK and test dependencies, and the integration test boots a minimal context rather than the application's. Co-Authored-By: Claude Opus 5 <[email protected]>
…roject The guard for the Grafana assets belongs to the brick, not to a copy of it in the service, and it needs only JUnit, AssertJ and Jackson. Co-Authored-By: Claude Opus 5 <[email protected]>
Brick Java now has its own Gradle build files and lockfiles under bricks/*/java, which Dependabot never looked at. Co-Authored-By: Claude Opus 5 <[email protected]>
The docs still said brick Java is copied with __PKG__ substituted; authors need the new contract (Gradle project, fixed package, auto-configuration, no app imports) and users need to know a bean can be overridden. Co-Authored-By: Claude Opus 5 <[email protected]>
A Boot/BOM bump refreshed only the app lockfile and broke every enabled brick's locked resolution. The minio README still suggested removing the brick to keep its Java, which now drops the subproject. The rename test fixture now mirrors the real brick layout. Co-Authored-By: Claude Opus 5 <[email protected]>
Pins that the brick registers its beans, binds app.minio.*, and backs off for an S3Client the application defines. Co-Authored-By: Claude Opus 5 <[email protected]>
|
Update: brick Java is now a Gradle subproject, not a copy. A brick with Java has Verified: 🤖 Generated with Claude Code |
Compose < 5 (ubuntu-latest ships 2.38) rejects an included file that
redefines a service of the parent ("services.app conflicts with imported
resource"), which the minio brick did for app. Partials of core services
now live in override(.prod).yml, which the Makefile adds as -f files while
the brick is enabled; -f merging works on every Compose version. The
Makefile is the single place that builds the compose -f list (make
print-prod), and vps.sh evals that on the VPS instead of repeating it.
Co-Authored-By: Claude Opus 5 <[email protected]>
…t BOM Trivy fails CI on fixable HIGH/CRITICAL findings in the service and brick lockfiles. Boot 4.1.0 still manages Jackson 3.1.4 / 2.21.4; the fixes are in 3.1.7 / 2.21.7. Netty 4.2.17, httpcore5 5.4.3 and Spring Framework 7.0.9 were flagged by the same scan, so they are overridden the same way (the bricks import the Boot BOM too). Drop each once the BOM catches up. Co-Authored-By: Claude Opus 5 <[email protected]>
AWS SDK classes are annotated with Jackson 2 @JsonInclude, which the brick did not see, so javac printed "unknown enum constant Include.NON_EMPTY" on every compile. compileOnly is enough: nothing at runtime needs it. Co-Authored-By: Claude Opus 5 <[email protected]>
Summary
services/app(Gradle:app,settings.gradle.ktsdiscoversservices/*).bricks/<name>/, enabled by.bricks, wired bymake add BRICK=<name>/make remove BRICK=<name>(composeinclude:lines, env marker blocks, Java copied with__PKG__substitution, skill install, lockfile refresh).bricks/README.mdis the contract;.claude/skills/bricksthe agent entry point.observability(Alloy, Grafana, Prometheus, dashboard, alerts) andbackup(pg_dump → rclone) from compose profiles into bricks. Adds a newminiobrick (S3 client,ObjectStorageService, health indicator, unit + Testcontainers tests). Nothing is enabled by default.make prod-initfills every empty*PASSWORD=; CI gains abricksmatrix job that builds each brick on a clean checkout without gating the image.next-version.shcould skip a MAJOR bump (git log | grep -qunderpipefail).Spec:
docs/superpowers/specs/2026-10-12-lego-bricks-design.md. Plan:docs/superpowers/plans/2026-10-12-lego-bricks.md.Notes for the reviewer
minio/miniois no longer pullable (Docker Hub, quay, ghcr); the brick usescgr.dev/chainguard/minio:latest(real MinIO, shipsmc;latestonly on Chainguard's free tier).COMPOSE_PROFILESlayout must followdocs/deployment.md→ "Upgrading a host from the COMPOSE_PROFILES layout" beforemake prod-up, or backups stop silently.make addon the laptop, push, then on the VPSgit pull, copy the brick's.env.prodblock,make prod-up(documented indocs/deployment.md).Test plan
make build(unit + integration + JaCoCo) with no bricks and with each brick addedmake test-scripts(11 suites incl. per-brick tests; all-bricks compose render;:?guards still fire)make renameaftermake add BRICK=miniobuilds under the new packagescripts/lint-commits.sh main..HEADcleanbricksmatrix job on GitHub (never executed on a real runner yet)🤖 Generated with Claude Code