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
FSHTechand usemainas the default branch (release, docs, and sibling clones all targetmain).Pin to
@v1. Consumers reference everything at the moving major tag (…@v1), updated in this repo viagit tag -f v1 && git push -f origin v1.Thin callers. Each repo keeps its own
.github/workflows/*.yml, but those files only own theon:trigger + inputs and delegate to the reusable workflows / composite actions here. Job orchestration that is genuinely repo-specific (thelintjob graph) stays local but is built from these actions.actions/checkoutfirst. 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-python → uv-sync → just-run, plus pre-commit)¶
uv project with a committed
uv.lockat the repo root (thesetup-pythoncache keys on it). Python is pinned to 3.14 unlesspython-versionis overridden.Dependency groups.
dev(tests + general dev) andlint(ruff, zuban, pre-commit, and anything the local hooks import). Jobs pass these touv-syncviaargs:--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-cirecipes (run byjust-run), anddocsif the repo publishes Sphinx docs..pre-commit-config.yamlat the repo root.ruff/ruff-formatare skipped bypre-commit(the lint job covers them); list any other hooks that can’t run in CI via theskipinput (e.g.js-format,check-control-blank-lineswhen that tree isn’t materialised).Coverage configured in
pyproject.toml; thecoverage-cirecipe runscoverage run -m pytest+ a report. DB-backed suites attach apostgresservice in the calling job and passDATABASE_URLvia env.
Node repos (setup-node → just-run)¶
.nvmrcat the repo root pins the Node version.Yarn Classic (v1).
setup-noderunsyarn install --frozen-lockfile; usecache: yarnlayout and a committedyarn.lock.Yarn workspaces with
@fsh/*scoped package names.Justfile exposing the same recipe names (
lint,type-check,build,test) that wrap the underlyingyarn workspace …calls, sojust-rundrives Node the same way it drives Python. (A repo that still callsyarndirectly in its workflow hasn’t adopted the runner yet — give it ajustfile.)LINT_IMPORT_CYCLESis honored by the shared eslint flat-config preset (turns onimport/no-cycle); set it in thelintrecipe 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 JSfile:links are../../<repo>/packages/<pkg>— i.e. siblings sit one directory above$GITHUB_WORKSPACE.clone-siblingsclones 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_consumersentry) and expose them as Actions secrets namedDEPLOY_KEY_<REPO>— repo name uppercased with hyphens turned into underscores (codegen-database→DEPLOY_KEY_CODEGEN_DATABASE). The caller passes them through on the step’senv:.
PR titles (pr-title.yml)¶
PR titles follow Conventional Commits. Each repo passes its allowed
scopes(always includedeps,ci,release).requireScopedefaults 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 emitrelease_created; monorepos emitreleases_created(+paths_releasedfor npm).Optional
RELEASE_TOKENsecret (a PAT so release-please PRs re-trigger CI); falls back toGITHUB_TOKEN. Pass viasecrets: 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_URLfor python,CODEARTIFACT_NPM_REPOSITORY_URLfor npm). The publish job requestsid-token: writeand assumes the publisher role — no static credentials.Build system. Python repos build with
uv build(pyprojectbuild-system); npm repos build only the buildable@fsh/*workspaces (thebuild-commandinput —@fsh/eslint-confighas 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_TOKENsecret (pass viasecrets: inherit) and theCLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_PAGES_PROJECTvariables, with the custom domain bound to themain(production) branch.Python docs:
just docsbuilds Sphinx intodocs/_build/html. Node docs: a Storybook build assembled into a site dir. The caller passes theruntime,build-command, andoutput-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 throughworkflow_call— so it keeps a local docs workflow.
Tooling versions¶
OPA / Regal are pinned (
setup-opadefaults 1.16.2 / 0.34.1) — bump deliberately, since OPA’s--schematypechecker and Regal’s lint rules evolve and can surface new failures.