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 @v1. Consumers reference everything at the moving major tag (…@v1), updated in this repo via git tag -f v1 && git push -f origin v1.

  • 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 path-deps (clone-siblings)

Why clone instead of installing from CodeArtifact? These deps are published CodeArtifact packages — [tool.uv.sources] (and the JS file: links) just override them to point at sibling source. The integration harnesses (codegen-example-app, sales-pipeline, codegen-targets, the templates) clone siblings so CI validates against HEAD of the whole constellation, not the last release — a breaking change in codegen is caught by the example app’s generate/lint/test gates before it’s published. Pure consumers of stable libs don’t need this and should just resolve from CodeArtifact.

Repos that do use the sibling layout follow the “checked out next to each other” convention:

  • [tool.uv.sources] paths are ../../<repo> and JS file: links are ../../<repo>/packages/<pkg> — i.e. siblings sit one directory above $GITHUB_WORKSPACE. clone-siblings clones into $(dirname $GITHUB_WORKSPACE) to match.

  • Per-repo deploy keys. Each sibling has its own read-only deploy key (provisioned by the infra source-code stack). The consumer must be granted those keys (the stack’s ci_consumers entry) and expose them as Actions secrets named DEPLOY_KEY_<REPO> — repo name uppercased with hyphens turned into underscores (codegen-databaseDEPLOY_KEY_CODEGEN_DATABASE). The caller passes them through on the step’s env:.

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).

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.