# Consumer conventions These shared actions and workflows are deliberately thin — they assume the consuming repo follows the FSH conventions below. If a repo diverges, override the relevant input or keep a local definition for that one piece. ## Repo-wide - **Org / default branch.** All repos live under `FSHTech` and use `main` as the default branch (release, docs, and sibling clones all target `main`). - **Pin to an exact release tag.** Consumers reference everything at the exact `vX.Y.Z` tag release-please cuts (`…@v1.3.0`) — there is no moving major tag. Renovate keeps the pins current; bump the tag to adopt a new release. - **Thin callers.** Each repo keeps its own `.github/workflows/*.yml`, but those files only own the `on:` trigger + inputs and delegate to the reusable workflows / composite actions here. Job orchestration that is genuinely repo-specific (the `lint` job graph) stays local but is *built from* these actions. - **`actions/checkout` first.** Every composite action assumes the repo is already checked out and (for lint/test/build actions) the language toolchain action has already run. ## Everything runs through `just` Lint, type-check, test, build and generate all run via `just-run` (`just `). The language-specific part is only the install that runs first (`setup-python` + `uv-sync`, or `setup-node`). So **every repo exposes its CI steps as `just` recipes in a root `justfile`** — the canonical names are `lint`, `type-check`, `coverage-ci` (CI test gate), `docs`, and for app/template repos `gen` / `bootstrap` / `validate`. This is what lets one generic runner serve both the Python and the Node repos. ## Python repos (`setup-python` → `uv-sync` → `just-run`, plus `pre-commit`) - **uv project** with a committed `uv.lock` at the repo root (the `setup-python` cache keys on it). Python is pinned to 3.14 unless `python-version` is overridden. - **Dependency groups.** `dev` (tests + general dev) and `lint` (ruff, zuban, pre-commit, and anything the local hooks import). Jobs pass these to `uv-sync` via `args`: `--group dev --group lint` (lint), `--group lint` (pre-commit's default sync), `--group dev` (tests). Monorepos add `--all-packages`. - **Justfile** with at least `lint`, `type-check`, `coverage-ci` recipes (run by `just-run`), and `docs` if the repo publishes Sphinx docs. - **`.pre-commit-config.yaml`** at the repo root. `ruff` / `ruff-format` are skipped by `pre-commit` (the lint job covers them); list any other hooks that can't run in CI via the `skip` input (e.g. `js-format`, `check-control-blank-lines` when that tree isn't materialised). - **Coverage** configured in `pyproject.toml`; the `coverage-ci` recipe runs `coverage run -m pytest` + a report. DB-backed suites attach a `postgres` service in the calling job and pass `DATABASE_URL` via env. ## Node repos (`setup-node` → `just-run`) - **`.nvmrc`** at the repo root pins the Node version. - **Yarn Classic (v1).** `setup-node` runs `yarn install --frozen-lockfile`; use `cache: yarn` layout and a committed `yarn.lock`. - **Yarn workspaces** with `@fsh/*` scoped package names. - **Justfile** exposing the same recipe names (`lint`, `type-check`, `build`, `test`) that wrap the underlying `yarn workspace …` calls, so `just-run` drives Node the same way it drives Python. (A repo that still calls `yarn` directly in its workflow hasn't adopted the runner yet — give it a `justfile`.) - **`LINT_IMPORT_CYCLES`** is honored by the shared eslint flat-config preset (turns on `import/no-cycle`); set it in the `lint` recipe or the job env. ## Sibling deps from CodeArtifact (`codeartifact-login`) FSH libraries (`fsh-lib`, `codegen`, `codegen-database`, `@fsh/*`) are published to CodeArtifact and resolved as ordinary pinned dependencies — no cloning. CI runs `codeartifact-login` (assumes the read role via OIDC, then exports `UV_INDEX_URL` and writes `~/.netrc` for uv/pip + `~/.npmrc` for `@fsh` npm). The calling job needs `permissions: id-token: write` and the `CODEARTIFACT_*` repo variables (provisioned by the infra source-code stack). **Local-dev editable override.** Repos that want live sibling editing keep `[tool.uv.sources]` path overrides (`../`; JS `file:` links) so a local `uv sync` resolves siblings from the checkout next door. CI sets `UV_NO_SOURCES=1` (workflow-level `env:`) so it *ignores* those overrides and resolves the published package from CodeArtifact instead. The committed `uv.lock` is the registry (no-sources) form — `UV_NO_SOURCES` can't override an editable entry baked into the lock, so generate it with `uv lock --no-sources`; a local `uv sync` re-resolves to the editable form (don't commit that). **`just` modules are the exception.** Recipe modules are vendored from the `just-packages` git repo by `just-vendor` over per-repo deploy keys (`DEPLOY_KEY_JUST_PACKAGES`, `DEPLOY_KEY_JUST_PACKAGE_MANAGER`) — there's no CodeArtifact equivalent for `just` files, so those deploy keys stay. ## PR titles (`pr-title.yml`) - PR titles follow Conventional Commits. Each repo passes its allowed `scopes` (always include `deps`, `ci`, `release`). `requireScope` defaults to false. ## Releases (`release-python.yml`, `release-npm.yml`) - **release-please** configured in-repo: `release-please-config.json` + `.release-please-manifest.json`. Single-package repos emit `release_created`; monorepos emit `releases_created` (+ `paths_released` for npm). - **Optional `RELEASE_TOKEN`** secret (a PAT so release-please PRs re-trigger CI); falls back to `GITHUB_TOKEN`. Pass via `secrets: inherit`. - **CodeArtifact via OIDC** — infra provides these as **repo/org variables**: `CODEARTIFACT_PUBLISH_ROLE_ARN`, `CODEARTIFACT_REGION`, `CODEARTIFACT_DOMAIN`, `CODEARTIFACT_DOMAIN_OWNER`, and the repo URL (`CODEARTIFACT_PYPI_REPOSITORY_URL` for python, `CODEARTIFACT_NPM_REPOSITORY_URL` for npm). The publish job requests `id-token: write` and assumes the publisher role — no static credentials. - **Build system.** Python repos build with `uv build` (pyproject `build-system`); npm repos build only the buildable `@fsh/*` workspaces (the `build-command` input — `@fsh/eslint-config` has no build script and publishes source as-is). ## Deploys (`deploy-ecs.yml`) App repos deploy by calling the reusable `deploy-ecs.yml` workflow with a single `tier` input (`prod` | `staging`). The thin caller maps branch → tier (`main` → prod, `staging` → staging) plus a `workflow_dispatch` escape hatch. - **Mutable branch tags drive deploys.** ECS pins `branch-main` (prod) / `branch-staging` (staging), so a deploy is: re-push the tag for each image (`ecr-build-push`), run migrations once in the primary region (`ecs-run-migration`), then `force-new-deployment` per region (`ecs-deploy`). Warm-standby regions additionally gate on `ecr-wait-replication` (ECR cross-region replication is async). - **Infra provides the repo variables**: `DEPLOY_ROLE_ARN` (ECS deploy role, restricted to the `main`/`staging` refs), `DEPLOY_ECR_PUSH_ROLE_ARN` (image push, any ref), `DEPLOY_ECR_REGISTRY`, `DEPLOY_ECR_REPO_PREFIX`, and a `DEPLOY_CONFIG_` JSON document per tier (image tag + the per-region cluster/service/migrate-task coordinates). Jobs request `permissions: id-token: write` — no static credentials. - **Checkout is a complete build context.** The generated trees (`_generated/`) are committed, so the build jobs run no codegen. A repo that gitignores them must add a generate step before `ecr-build-push`. - **Dockerfile secret contract.** Images that resolve private deps take CodeArtifact auth as BuildKit secrets: the be image mounts `id=uv_index_url` as `UV_INDEX_URL` (run `codeartifact-login` first, pass `secret-envs: uv_index_url=UV_INDEX_URL`), the render image mounts `id=npmrc` at `/root/.npmrc` (pass `secret-files: npmrc=/home/runner/.npmrc`). ## Docs (`docs.yml`) - A **Cloudflare Pages project is pre-provisioned** by infra, which supplies the `CLOUDFLARE_API_TOKEN` secret (pass via `secrets: inherit`) and the `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_PAGES_PROJECT` variables, with the custom domain bound to the `main` (production) branch. - **Python docs**: `just docs` builds Sphinx into `docs/_build/html`. **Node docs**: a Storybook build assembled into a site dir. The caller passes the `runtime`, `build-command`, and `output-dir`. - A docs build needing a **service container** (codegen-database's Sphinx build talks to postgres) can't use `docs.yml` — `services:` can't pass through `workflow_call` — so it keeps a local docs workflow. ## Tooling versions - **OPA / Regal** are pinned (`setup-opa` defaults 1.16.2 / 0.34.1) — bump deliberately, since OPA's `--schema` typechecker and Regal's lint rules evolve and can surface new failures.