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 <recipe>). 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-pythonuv-syncjust-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-nodejust-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 (../<repo>; 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_<TIER> 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.ymlservices: 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.