Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Excluded, and why:
both resolve exactly one upstream clone. Its provenance travels with the
files instead, in that directory's own `NOTICE` — source repository, commit,
file-by-file origin and licence — which is what a reader who finds the
directory will actually look at. `docs/public/logo.png` comes from the same
directory will actually look at. `common/docs/public/logo.png` comes from the same
repository and the same `NOTICE` covers it.
- **`docs/decisions/`** (this repository's own, at the root — not
`common/docs/decisions/`, which ships and is covered below). This is this
Expand All @@ -62,12 +62,12 @@ Excluded, and why:
| `common/.editorconfig` | `.editorconfig` | verbatim | |
| `common/renovate.json` | `renovate.json` | original | was `verbatim` (byte-identical) until this task. Replaced: the upstream file extends `local>immich-app/.github:renovate-config` (unresolvable outside that organisation), matches `machine-learning/**`, `mobile/**`, `ghcr.io/immich-app/*`, none of which exist in a generated project, and has a trailing comma that makes it invalid strict JSON — every generated project has shipped a Renovate config that fails on its first parse since Task 3. Replaced with a minimal, generic config (`$schema` plus the public `config:recommended` preset, no per-project rules). |
| `common/.gitattributes` | `.gitattributes` | original | instructed as a byte-for-byte copy originally, then reversed: upstream's version is almost entirely `mobile/**` and `packages/sdk/**` linguist/generated rules that have no counterpart in a generated project. Kept as one line, `* text=auto eol=lf`. |
| `common/compose.yaml` | `docker/docker-compose.yml` | adapted | one application service instead of server/machine-learning/redis/postgres; digest-pinned postgres image kept; every variable defaults so the file validates before `.env` exists (ADR-0014). |
| `common/compose.dev.yaml` | `docker/docker-compose.dev.yml` (`database` service) | adapted | the rest of upstream's dev compose mounts the whole immich monorepo into containers, which has no counterpart here; only its `database` service (port `5432:5432` exposed, postgres env vars) is the real parallel, simplified to a pinned-digest image with hardcoded local-only credentials. |
| `common/compose.yaml` | `docker/docker-compose.yml` | adapted | one service per application plus the selected database and cache instead of server/machine-learning/redis/postgres, assembled by `scaffold` (ADR-0022, ADR-0019); each database or cache image is pinned by digest in `services/<name>/service.env`, and each application image is tagged `${IMAGE_TAG:-latest}`; every variable defaults so the file validates before `.env` exists (ADR-0014). |
| `common/compose.dev.yaml` | `docker/docker-compose.dev.yml` (`database` service) | adapted | the rest of upstream's dev compose mounts the whole immich monorepo into containers, which has no counterpart here; only its `database` service (a port exposed to the host) is the real parallel. The template ships `services: {}`; `scaffold` merges in each selected service's `compose.dev.fragment.yaml`, with ports bound to `127.0.0.1` and digest-pinned images. |
| `common/compose.test.yaml` | — | original | no `docker-compose.test.yml` exists upstream. Same shape as `compose.dev.yaml` (its sibling in this repo, not an upstream file) with `tmpfs` storage so CI starts from an empty database every run. |
| `common/example.env` | `docker/example.env` | adapted | one image's variables instead of immich's per-service set; same "copy to `.env` and edit" framing and a placeholder `DB_PASSWORD` an operator must change (ADR-0014). |
| `common/install.sh` | `install.sh` | adapted | one image instead of several; never overwrites an existing `.env`; generates the database password from `/dev/urandom` instead of asking the operator to supply one. Rationale recorded inline in the file. |
| `common/.github/workflows/build.yml`, `ci.yml`, `docs.yml`, `release.yml`, `security.yml` | `.github/workflows/test.yml`, `docker.yml`, `docs-build.yml`, `codeql-analysis.yml`, `static_analysis.yml` | adapted | immich's per-repository workflows collapsed into five thin call sites that each delegate to `you/.github` (ADR-0005); `GITHUB_TOKEN` instead of a minted GitHub App token (ADR-0010); one docs workflow instead of three (ADR-0009); build and release split so an ordinary merge never waits on the standing release PR (ADR-0015). |
| `common/.github/workflows/build.yml`, `ci.yml`, `docs.yml`, `release.yml`, `security.yml` | `.github/workflows/test.yml`, `docker.yml`, `docs-build.yml`, `codeql-analysis.yml`, `static_analysis.yml` | adapted | immich's per-repository workflows collapsed into five thin call sites that each delegate to `you/.github` (ADR-0005); `GITHUB_TOKEN` instead of a minted GitHub App token, except Release Please's optional app token (ADR-0010); one docs workflow instead of three (ADR-0009); build and release split so an ordinary merge never waits on the standing release PR (ADR-0015). |
| `common/docs/` | `docs/` | adapted | VitePress instead of Docusaurus; one docs workflow instead of three (ADR-0009). `common/docs/scripts/check-adrs.mjs` and `check-paths.mjs` have no upstream equivalent — immich has no ADR process — and are original, not adapted. |
| `common/lefthook.yml` | — | original | immich runs no git hooks (ADR-0007). |
| `common/.gitignore`, `common/CODEOWNERS`, `common/CONTRIBUTING.md`, `common/README.md`, `common/SECURITY.md`, `common/commitlint.config.js`, `common/.git-blame-ignore-revs`, `common/release-please-config.json`, `common/.release-please-manifest.json`, `common/mise.root.toml`, `common/pnpm-workspace.yaml`, `common/AGENTS.md`, `common/packages-types/` | (various — e.g. `.gitignore`, `CODEOWNERS`, `CONTRIBUTING.md`, `README.md`, `pnpm-workspace.yaml`) | original | conventional files any GitHub/pnpm project carries. Checked against immich's own copies of each — `diff` shows no shared content beyond the two both being, say, a `.gitignore` — so these were written for this project, not adapted from immich's. |
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ Measured on `scaffold new demo --api nestjs --web nextjs --db postgres`: 101 tra
| Day one | This page, then [01-toolchain](tour/01-toolchain.md) through [03-ci](tour/03-ci.md) |
| First week | [04-guardrails](tour/04-guardrails.md) through [09-wizard](tour/09-wizard.md); ADR-0001, ADR-0003, ADR-0011 |
| On demand | The [runbook](runbook/) that names the situation |
| Before a project's first push | [publish-a-project](runbook/publish-a-project.md) |
| When the toolbox changed under an existing project | [update-a-project](runbook/update-a-project.md) |

## Diagrams

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ application per stack.

Invoke each framework's own generator and overlay four files.

> **Amended 2026-09-17.** `copy_adapter_files` in `lib/adapter.sh` overlays
> every file in the adapter directory except `adapter.env` and
> `lefthook.fragment.yml` (`ADAPTER_INTERNAL_FILES`). Four is the minimum
> `scaffold lint` requires (`REQUIRED_ADAPTER_FILES` in `lib/contract.sh`).

**Amendment, 2026-09-06 (0021).** The boundary was "an adapter overlays
configuration, it never writes application code." It narrows to: no
application code except the health routes 0021's deploy gate requires — one
Expand Down
4 changes: 4 additions & 0 deletions docs/decisions/0005-share-ci-through-reusable-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ one per entrypoint — that only name the reusable workflow and pass its
inputs. Projects reference `you/.github` by the moving tag `v1`, not a
commit or a fixed minor version.

> **Amended 2026-09-17.** `you/.github` now has six `workflow_call` files.
> The sixth, `app-targets.yml`, resolves the image matrix for `app-build.yml`
> and `app-release.yml` (ADR-0022). No call site in `common/` names it.

## Consequences

- A fix to CI reaches every project that calls `v1` the next time it runs,
Expand Down
9 changes: 9 additions & 0 deletions docs/decisions/0010-github-token-over-a-github-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Status: Accepted
Date: 2026-08-28
Superseded in part by ADR-0024 (the Release Please token)

## Context

Expand All @@ -23,6 +24,14 @@ Use the built-in `GITHUB_TOKEN` everywhere, scoped down per job with
`permissions:` (every workflow starts `permissions: {}` and grants only what
that job needs — `contents: read`, `packages: write`, and so on).

> **Amended 2026-09-17.** One exception exists. `app-release.yml`'s
> `release-please` job mints a GitHub App token when the `RELEASE_APP_ID` and
> `RELEASE_APP_PRIVATE_KEY` secrets are set, and falls back to `GITHUB_TOKEN`
> when they are not. `common/.github/workflows/release.yml` passes both, and
> `scaffold publish` sets them from the environment (ADR-0024). A pull request
> opened with `GITHUB_TOKEN` starts no workflow, so its checks wait for
> approval. Every other job still uses `GITHUB_TOKEN`.

## Consequences

- No secret to provision, rotate, or document per project — `GITHUB_TOKEN`
Expand Down
4 changes: 4 additions & 0 deletions docs/decisions/0012-tiered-adapter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ its own directory changes plus weekly — 25 minutes is far past what anyone
will wait on before pushing. Tier C has no automated verification and is
allowed to rot.

> **Amended 2026-09-17.** `flask` joined Tier A (`adapters/flask/adapter.env`).
> Tier B also runs on a manual `workflow_dispatch` of
> `.github/workflows/adapters.yml` (`scripts/adapter-matrix.sh`).

Tier membership lives in one place: each adapter's own `ADAPTER_TIER`
(`adapter.env`). Workflows read it (`scripts/adapter-matrix.sh`, backed by
`scaffold list`) rather than repeating the adapter list in YAML, so the tier
Expand Down
12 changes: 11 additions & 1 deletion docs/decisions/0014-deployment-deferred-with-seams.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Status: Accepted
Date: 2026-08-27
Superseded in part by 0021 (seams 1 and 4)
Superseded in part by 0021 (seams 1 and 4) and 0022 (seam 1's single `app` service)

## Context

Expand Down Expand Up @@ -53,6 +53,13 @@ deploy target plugs into later without restructuring anything above it:
reusable release workflow will carry a `deploy` job that does nothing
until a client sets that variable.

> **Amended 2026-09-17.** Seams 1 and 4 name one `app` service and a
> postgres `database`. Since ADR-0022, `compose.yaml` runs one service per
> application, each on `ghcr.io/<owner>/<project>-<app>`. Since ADR-0019, the
> `database` service and its healthcheck come from the selected
> service's `compose.fragment.yaml` (`pg_isready` for postgres), and only an
> `api` or `app` service waits on it.

**Update, 2026-09-06 (0021).** Seams 1 and 4 as written above are now
false, and the record stays rather than being rewritten: seam 4's premise
was that php-fpm speaks FastCGI and no HTTP check is possible, so no check
Expand Down Expand Up @@ -83,6 +90,9 @@ adapter still fills in a body rather than restructuring anything, and
by hand, on the target host, after cloning nothing more than the two files a
release publishes (`compose.yaml`, `example.env`).

> **Amended 2026-09-17.** A release publishes three files: `app-release.yml`'s
> `assets` job uploads `compose.yaml`, `example.env` and `install.sh`.

**Update, 2026-09-07.** "The one deploy mechanism that exists today" held
only for a public project. Measured against a real private repository, a
release asset's browser download URL 404s even with a Bearer token — the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ Split publishing into two reusable workflows that never gate each other:
'true'` — i.e. only on the merge that closes a release PR — and publish
semver tags (`1.4.0`, `1.4`) plus `latest`.

> **Amended 2026-09-17.** `app-build.yml`'s `build` job and `app-release.yml`'s
> `image` job also require `needs.targets.outputs.matrix != '[]'`: a project
> with no application builds nothing. The release `image` job also publishes
> `sha-<commit>`. Each job builds one image per application (ADR-0022).

Both call sites (`common/.github/workflows/build.yml` and `release.yml`)
are separate files with separate triggers, not two jobs in one workflow, so
one failing does not block the other from being invoked at all.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ whether it clears the immediate failure:
`allowBuilds: { esbuild: true }` entry for the identical reason, kept in
sync by hand since the two files serve different projects and are never
both present at once.

> **Amended 2026-09-17.** `common/pnpm-workspace.yaml` is no longer deleted.
> For a project that is not all-typescript, `keep_apps_standalone` in
> `lib/pnpm.sh` removes only its `packages:` key and keeps `allowBuilds`.
> `join_typescript_workspace` deletes the `docs` pair instead.

- **`confirmModulesPurge` does not ship in `common/pnpm-workspace.yaml`.**
It is set instead as `env = { npm_config_confirm_modules_purge = "false" }`
on the `install` and `ci-unit` mise tasks, in every adapter's
Expand All @@ -90,6 +96,11 @@ whether it clears the immediate failure:
ambient environment variables to the tool it launches — a fact this
decision had to design around, not rely on, for `record_release_age_exceptions`
below).

> **Amended 2026-09-17.** Only the pnpm-based files set it:
> `adapters/nestjs/mise.toml`, `adapters/nextjs/mise.toml`,
> `common/packages-types/mise.toml` and `common/docs/mise.toml`.

- **`minimumReleaseAge: 0` does not ship anywhere, ever.** Lowering it
permanently, silently, for every client project this toolbox will ever
generate is exactly the outcome this decision exists to prevent — a
Expand Down Expand Up @@ -179,6 +190,13 @@ whether it clears the immediate failure:
relaxation ships to the client on every *successful* `scaffold add`;
without `cmd_add_cleanup`'s strip, it survives every *failed* one.

> **Amended 2026-09-17.** `relax_pnpm_workspace` in `lib/pnpm.sh` appends
> three lines, `PNPM_RELAXATIONS`: `confirmModulesPurge: false`,
> `frozenLockfile: false` and `minimumReleaseAge: 0`. Both sites strip them
> with `restore_pnpm_workspace`: on success from `settle_added_app`, before
> `trap - EXIT`; on failure from `cmd_add_cleanup`. The known gap below
> applies to all three lines.

**Known gap, not fixed as part of this decision**: both removal sites
match the line blindly (`sed -i
'/^confirmModulesPurge: false$/d'`), with no way to tell "the line
Expand All @@ -205,6 +223,11 @@ whether it clears the immediate failure:
minimum-release-age recording the first round of apps got from
`scaffold new`.

> **Amended 2026-09-17.** Only an app joining a shared workspace
> (`joins_shared_workspace`) gets `sync_workspace_lockfile`. A standalone
> app gets `record_release_age_exceptions` alone
> (`reconcile_standalone_app`).

## Alternatives considered

- **Ship `minimumReleaseAge: 0` in `common/pnpm-workspace.yaml`.** Rejected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ every requested adapter's `ADAPTER_LANGUAGE` is `typescript`,
(`rm -rf "${target}/packages-types" "${target}/pnpm-workspace.yaml"`) —
see `scaffold`'s `cmd_new`.

> **Amended 2026-09-17.** The decision now lives in `settle_workspace_shape`
> in `scaffold`. A mixed project loses `packages-types`, but keeps
> `pnpm-workspace.yaml` without its `packages:` key (`keep_apps_standalone`
> in `lib/pnpm.sh`).

`scaffold add` installs one more adapter into a project that already made
that decision. Its choice of adapter can contradict the inputs the
decision was originally made from in either direction:
Expand Down Expand Up @@ -87,6 +92,11 @@ retroactively when an added adapter happens to be typescript.
the next time someone runs `pnpm install`, with no extra step from
`scaffold add`.

> **Amended 2026-09-17.** `scaffold add` does run the install. A typescript
> app joining a shared workspace gets `reconcile_shared_workspace`
> (`sync_workspace_lockfile`, then `record_release_age_exceptions`), and the
> file gains and loses `PNPM_RELAXATIONS` during the call (ADR-0017).

## Alternatives considered

- **Recompute and remove `packages/types` when the project becomes
Expand Down
5 changes: 5 additions & 0 deletions docs/decisions/0023-a-project-records-what-generated-it.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ comment change. After the patch, `sync_ci_roots` runs and the build targets are
rebuilt from the manifest — only when the array actually came back empty, so a
project whose targets survived is untouched.

> **Amended 2026-09-17.** `common/.github/workflows/ci.yml` carries
> `roots: '["docs"]'`, not `roots: '[]'`, so a verbatim patch checks only
> `docs`. `resync_derived_files` in `lib/update.sh` runs `sync_ci_roots` on
> every update and rebuilds the image targets only when `images` is `"[]"`.

**It refuses a dirty working tree**, because `git diff` afterwards is the only
review this gets and it has to show one run's changes alone. It never commits.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ guardrails that is a repository setting rather than a file, and the one most
likely to be skipped because nothing in the project records whether it was
applied.

> **Amended 2026-09-17.** ADR-0004 lists no guardrails. The four layers — git
> hooks, CI, Renovate and the ruleset — are listed in
> `docs/tour/04-guardrails.md`.

A runbook step that is required, has no in-repo trace, and fails somewhere
else is a step that should not be a runbook step.

Expand Down
Loading