diff --git a/README.md b/README.md index 6e36f15..0f75ffd 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,169 @@ # scaffold +**Set up client projects with CI, containers, hooks, and releases in one command.** + +scaffold is a bash toolbox for engineers who start client projects often. One command +generates a monorepo that is ready for its first pull request, and every project it makes is +built, checked and released the same way. + [![CI](https://github.com/ttncode/scaffold/actions/workflows/ci.yml/badge.svg)](https://github.com/ttncode/scaffold/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -Generate a client project that is ready for its first pull request: a monorepo -of apps, an optional database and cache in Docker Compose, git hooks, CI, -releases and a docs site, all wired together and committed. +A client project is generated, published, developed, checked by shared CI, released and run from install.sh. scaffold update brings later toolbox changes in, and every app answers one nine-task contract. -scaffold is a bash toolbox for engineers who start client projects often and -want every one of them built, checked and released the same way. It is -pre-1.0; versions are git tags. +scaffold is pre-1.0, and its versions are git tags. -## What a generated project gets +--- -- **Apps** from the adapters you pick: `--web`, `--api` or `--app`, each in its own directory with its own `mise.toml`. -- **One task contract.** Every app answers the same nine `mise` tasks (`install`, `format`, `lint`, `test`, `build`, `checklist`, …), so CI runs one command per app and never learns the language. -- **CI** as five thin workflows that call shared reusable workflows at `@v1` ([ADR-0005](docs/decisions/0005-share-ci-through-reusable-workflows.md)). -- **Guardrails:** lefthook runs prettier, gitleaks and commitlint locally; Renovate opens dependency bumps. -- **Releases:** Release Please from Conventional Commits, container images, and an `install.sh` that runs the released stack with Docker Compose. -- **A VitePress docs site** checked in CI like any app. -- **`.scaffold.toml`**, recording the toolbox commit that generated it, so `scaffold update` can bring later toolbox changes in. +## Commands -## Requirements +| What you're doing | Command | Key principle | +|---|---|---| +| Start a project | `scaffold new ` | One command, one commit | +| Add an app to it | `scaffold add --adapter ` | Staged, never committed for you | +| Bring in toolbox changes | `scaffold update [dir]` | Applied as a patch you review | +| Put it on GitHub | `scaffold publish [dir]` | Private by default, `main` protected | +| See what's available | `scaffold list` | The wizard reads the same list | +| Check adapters and services | `scaffold lint` | Every one meets the contract | -| Needed for | Requirement | -| --- | --- | -| Everything | `git` and [`mise`](https://mise.jdx.dev/getting-started.html); `mise install` supplies `jq`, `yq` and the rest | -| `laravel-api`, `laravel-inertia` | PHP 8.3 or later on the host; mise cannot pin it ([ADR-0016](docs/decisions/0016-php-is-not-pinned-through-mise.md)) | -| `scaffold new` | A GitHub owner for the project: `SCAFFOLD_GITHUB_OWNER`, else the signed-in `gh` user, else `git config github.user` | -| `scaffold publish` | [`gh`](https://cli.github.com/), signed in with `gh auth login` | -| CI in a generated project | A `.github` repository under the project's GitHub owner holding the reusable workflows ([ADR-0005](docs/decisions/0005-share-ci-through-reusable-workflows.md)) | -| Running a release | Docker | +Run `scaffold` with no arguments for a wizard that builds the command for you. Flags and +defaults are in [Commands](docs/README.md#commands). + +--- ## Quick start +You need `git` and [`mise`](https://mise.jdx.dev/getting-started.html). `mise install` +supplies the rest. + ```sh git clone https://github.com/ttncode/scaffold.git cd scaffold mise install export PATH="$PWD:$PATH" -scaffold list ``` -`scaffold list` prints one row per adapter and service. `scaffold` loads its pinned `jq` and `yq` itself. Call `scaffold` by its path or through `PATH`; a symlink to it does not work. - -Generate a project outside the toolbox. `scaffold new` creates it where you run it: +Then generate a project somewhere outside the toolbox: ```sh cd ~/playground scaffold new demo-app --web nextjs --api nestjs --db postgres ``` -The framework generators take several minutes. The result is a directory with one commit, `feat: scaffold project`. +The framework generators take a few minutes. You get a directory with one commit, +`feat: scaffold project`. The full run, from generation to a running release, is in +[Walk through a first project](docs/runbook/first-project-walkthrough.md). -Run `scaffold` with no arguments in a terminal for a wizard that builds the same command. The full end-to-end run, from generation to a running release, is [Walk through a first project](docs/runbook/first-project-walkthrough.md). +
+Other requirements -## Commands +| Needed for | Requirement | +|---|---| +| `laravel-api`, `laravel-inertia` | PHP 8.3 or later on the host ([ADR-0016](docs/decisions/0016-php-is-not-pinned-through-mise.md)) | +| `scaffold new` | A GitHub owner: `SCAFFOLD_GITHUB_OWNER`, the signed-in `gh` user, or `git config github.user` | +| `scaffold publish` | [`gh`](https://cli.github.com/), signed in | +| CI in a generated project | A `.github` repository under that owner, holding the reusable workflows ([ADR-0005](docs/decisions/0005-share-ci-through-reusable-workflows.md)) | +| Running a release | Docker | -| Command | Does | -| --- | --- | -| `scaffold new ` | Generates and commits a project | -| `scaffold add --adapter ` | Adds an app to an existing project and stages it | -| `scaffold update [dir]` | Applies toolbox changes since the project was generated; never commits | -| `scaffold publish [dir]` | Creates the GitHub repository (private by default) and protects `main` | -| `scaffold list` | Prints adapters with role and tier, services with kind | -| `scaffold lint` | Checks every adapter and service against the contract | +
-Flags, defaults, environment variables and the decision behind each command: [Commands](docs/README.md#commands). +--- -## Adapters +## What a generated project gets -Each adapter's tier is `ADAPTER_TIER` in its `adapter.env` ([ADR-0012](docs/decisions/0012-tiered-adapter-support.md)). +| Piece | What It Does | +|---|---| +| **Apps** | One directory per adapter you pick, each with its own `mise.toml` | +| **Task contract** | Every app answers the same nine `mise` tasks, so CI runs one command per app | +| **CI** | Five thin workflows that call shared reusable workflows at `@v1` | +| **Guardrails** | lefthook runs prettier, gitleaks and commitlint; Renovate opens dependency bumps | +| **Releases** | Release Please, container images, and an `install.sh` that runs the stack with Docker Compose | +| **Docs site** | VitePress, checked in CI like any app | +| **`.scaffold.toml`** | Records the toolbox commit that generated it, for `scaffold update` | -| Adapter | Role | Tier | -| --- | --- | --- | -| `nextjs` | web | A | -| `nestjs` | api | A | -| `laravel-api` | api | A | -| `flask` | api | A | -| `laravel-inertia` | app | B | +--- -| Tier | CI runs it | Guarantee | -| --- | --- | --- | -| A | every pull request, and nightly | stays green through every dependency bump | -| B | a pull request that changes `adapters/laravel-inertia/`, weekly, or on manual dispatch | verified regularly, not on every push | -| C | not automatically verified | none; no adapter is tier C today | +## Adapters and services -## Services +| Adapter | Flag | Tier | +|---|---|---| +| `nextjs` | `--web` | A | +| `nestjs` | `--api` | A | +| `laravel-api` | `--api` | A | +| `flask` | `--api` | A | +| `laravel-inertia` | `--app` | B | -A database or cache is a directory under `services/`, not an adapter ([ADR-0019](docs/decisions/0019-services-are-not-adapters.md)). +Tier A runs on every pull request and stays green through every dependency bump. Tier B is +verified weekly and whenever its adapter changes +([ADR-0012](docs/decisions/0012-tiered-adapter-support.md)). | Flag | Services | Default | -| --- | --- | --- | -| `--db` | `mysql`, `postgres`, `mongodb`, `none` | `mysql` with `--api` or `--app`, otherwise `none` ([ADR-0020](docs/decisions/0020-database-default-is-derived-from-requested-adapters.md)) | +|---|---|---| +| `--db` | `mysql`, `postgres`, `mongodb`, `none` | `mysql` with `--api` or `--app`, otherwise `none` | | `--cache` | `redis`, `none` | `none` | -There is no DynamoDB: every release ships a `compose.yaml` for the client to run ([ADR-0014](docs/decisions/0014-deployment-deferred-with-seams.md)), and the only DynamoDB that fits a compose file is an emulator with no production counterpart. +--- + +## How it works + +- **Overlay, not presets.** Each adapter runs the framework's own generator, then lays + scaffold's files on top ([ADR-0003](docs/decisions/0003-adapter-overlay-instead-of-vendored-presets.md)). +- **Only what you picked.** A project gets `common/` and the adapters you chose, nothing + else ([ADR-0004](docs/decisions/0004-keep-the-toolbox-out-of-generated-projects.md)). +- **No build orchestrator.** `mise` tasks are the only task runner + ([ADR-0001](docs/decisions/0001-use-mise-tasks-as-the-task-runner.md), + [ADR-0002](docs/decisions/0002-no-monorepo-build-orchestrator.md)). +- **The released stack must run.** A release has to start and serve, not just build + ([ADR-0021](docs/decisions/0021-the-released-stack-must-run.md)). + +--- + +## Project structure + +| Path | Purpose | +|---|---| +| `scaffold` | The entry point, one function per command | +| `lib/` | The libraries it sources | +| `adapters/` | One directory per framework | +| `services/` | One directory per database or cache | +| `common/` | Copied into every new project | +| `tests/` | bats suites and fixtures | +| `docs/` | Tour, decisions, runbooks and diagrams | + +--- + +## Why scaffold? + +Client projects start the same way every time, and each hand-made setup drifts a little from +the last. scaffold turns the setup into one command and keeps it consistent: every app speaks +the same task contract, every project shares the same CI, and `scaffold update` carries later +fixes into projects that already exist. + +--- ## Documentation -- [Start here](docs/README.md): repository map, commands, glossary and a [reading path](docs/README.md#reading-path) +- [Start here](docs/README.md): repository map, commands and a reading path - [Tour](docs/tour/): how the pieces fit, in nine pages - [Decisions](docs/decisions/): why they fit that way - [Runbooks](docs/runbook/): what to do when something specific happens -- [Provenance](docs/PROVENANCE.md): what is copied from [immich](https://github.com/immich-app/immich), and where it drifted +- [Provenance](docs/PROVENANCE.md): what is copied from [immich](https://github.com/immich-app/immich) + +--- + +## Contributing + +Setup, test lanes, and how to add an adapter or a service are in +[CONTRIBUTING.md](CONTRIBUTING.md). Report vulnerabilities privately as described in +[SECURITY.md](SECURITY.md). -## Contributing and security +## Team -Setup, test lanes and how to add an adapter or a service: [CONTRIBUTING.md](CONTRIBUTING.md). Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md). +| | Name | GitHub | Role | +|---|------|--------|------| +| Truong Trung Nghia | **Truong Trung Nghia** | [@ttncode](https://github.com/ttncode) | Creator | ## License -MIT, see [LICENSE](LICENSE). A generated project gets no license file: its terms belong to the engagement it was generated for. +MIT, see [LICENSE](LICENSE). A generated project gets no license file, because its terms +belong to the engagement it was generated for. diff --git a/docs/README.md b/docs/README.md index e11b39d..6fc5686 100644 --- a/docs/README.md +++ b/docs/README.md @@ -103,7 +103,7 @@ Measured on `scaffold new demo --api nestjs --web nextjs --db postgres`: 101 tra ## Diagrams -Each `.svg` is exported from the `.html` beside it. +Each `.svg` or `.png` is exported from the `.html` beside it. The README's hero is a `.png`, because GitHub shows README images without their web fonts. | Diagram | Shows | | --- | --- | @@ -113,3 +113,4 @@ Each `.svg` is exported from the `.html` beside it. | [generated-project](diagrams/generated-project.svg) | The tree `scaffold new` produces | | [release-flow](diagrams/release-flow.svg) | Continuous builds and cut releases of a generated project | | [scaffold-update](diagrams/scaffold-update.svg) | How `scaffold update` patches an existing project | +| [scaffold-lifecycle](diagrams/scaffold-lifecycle.png) | A client project from generation to a running release (the README hero) | diff --git a/docs/diagrams/scaffold-lifecycle.html b/docs/diagrams/scaffold-lifecycle.html new file mode 100644 index 0000000..529f85c --- /dev/null +++ b/docs/diagrams/scaffold-lifecycle.html @@ -0,0 +1,60 @@ + + + + +A client project, end to end + + + + + +A client project, end to end +A client project is generated, published, developed, checked by shared CI, released and run from install.sh, with scaffold update bringing later toolbox changes and every app answering one nine-task contract. + + + + + + + + + +SCAFFOLD UPDATE + + +Generate +scaffold new + + +Publish +scaffold publish + + +Develop +lefthook · mise + + +Check +reusable CI @v1 + + +Release +release-please + + +Run +install.sh + +One task contract: every app answers the same nine mise tasks +install · format · format-fix · lint · check · test · build · ci-unit · checklist — CI never learns the language + +LEGEND + +stage of a client project + +later toolbox changes, applied but never committed + +what every app implements + + + diff --git a/docs/diagrams/scaffold-lifecycle.png b/docs/diagrams/scaffold-lifecycle.png new file mode 100644 index 0000000..fcbc9db Binary files /dev/null and b/docs/diagrams/scaffold-lifecycle.png differ