Skip to content

Split a repo into components

A single repository often holds more than one deliverable: an API and a web frontend, a service and its infrastructure module, a family of libraries. Cascade lets each of these be a component with its own version line, its own promotion cadence, and its own hotfix and rollback history, all described in one manifest.

Components are the native shape of schema_version: 1. A manifest with no components: block is one implicit component that spans the whole repository, and it generates byte-identical output to a single-component setup. Nothing about the single-component path changes, and there is nothing to migrate. You reach for components: only when you want more than one independently versioned unit in the same repository.

A component is a named subtree of the repository that versions and ships on its own. Each component declares two required fields:

  • path, the subtree the component owns (for example api/ or web/).
  • tag_prefix, the prefix for that component’s version tags. Each component needs a distinct prefix so their tag namespaces never overlap.

Everything else a component needs, such as its builds, deploys, environment ladder, or breaking-change policy, is inherited from the shared top-level config and overridden per component only where it differs.

When a components: block is present, the top-level config becomes the set of shared defaults every component inherits. Each entry under components: overrides those defaults where it sets a value and inherits them everywhere else.

This manifest ships an api service and a web frontend from one repository. The shared defaults set the CLI pin, the trunk branch, and the default environment ladder. Each component owns its subtree, its tag namespace, and its own build and deploy:

.github/manifest.yaml
ci:
config:
schema_version: 1
trunk_branch: main
cli_version: v0.9.1
environments: [dev, staging, prod]
components:
api:
path: api/
tag_prefix: api-
builds:
- name: api
workflow: .github/workflows/build-api.yaml
triggers: ["api/**"]
deploys:
- name: api
workflow: .github/workflows/deploy-api.yaml
triggers: ["api/**"]
web:
path: web/
tag_prefix: web-
# web ships to a shorter ladder than the shared default.
environments: [dev, prod]
builds:
- name: web
workflow: .github/workflows/build-web.yaml
triggers: ["web/**"]
deploys:
- name: web
workflow: .github/workflows/deploy-web.yaml
triggers: ["web/**"]

Here api inherits the shared [dev, staging, prod] ladder, while web overrides it with a shorter [dev, prod] ladder. Both inherit trunk_branch and cli_version because those are shared, repository-wide settings that a component cannot override. For the exact list of which fields a component may override and which stay repository-wide, see the components field reference.

Every component computes its own version. Cascade scopes the commit walk to the component’s path, so only changes under api/ bump the api version and only changes under web/ bump the web version. Each component reads and writes tags under its own tag_prefix, and that namespace is strict: api-1.2.3 and web-1.2.3 never cross-match, and a prefix that is a substring of another (such as api- against api-beta-) cannot collide either.

The result is two independent version lines from one trunk. A commit that touches only web/ advances web- tags and leaves the api- line untouched.

The implicit single-component default keeps the permissive historical tag parsing, so a repository without a components: block reads its tags exactly as before. See Per-component versioning for the tag-namespace rules in full.

Each component gets its own promote workflow and its own concurrency lane. Cascade derives a per-component concurrency group so two components never serialize against each other: promoting api from dev to staging does not queue behind or cancel a web promotion. You promote each component on its own schedule, and the release boundary (the prerelease and release markers near the top of each ladder) is evaluated per component against that component’s own environment list.

Because web above declares a two-environment ladder, its promotion chain has a single step, while api promotes across three environments. Each component’s ladder is its own.

Hotfix and rollback operate per component, in separate namespaces, so an operation on one component never reaches into another:

  • State is recorded per component at state.components.<name>.<env>, carrying the full per-environment record (version, SHA, deploy history, and the rollback ring) for each component independently.
  • Hotfix branches are namespaced as env/<component>/<env>, so a hotfix in flight on api in staging lives on its own branch and its single-flight guard only counts that component’s open hotfix pull requests. If you protect these branches with a ruleset, match the nested env/<component>/<env> pattern.
  • Rollback reads the deploy history from that component’s own state ring and enumerates that component’s own environment ladder, so the rollback dropdown for web offers only web environments.
  • Release-candidate cleanup runs within the component’s tag namespace. Publishing api-1.0.1 reaps only api- prerelease tags and never touches web- tags.

The operator walkthroughs in Run a hotfix and Roll back an environment apply per component unchanged; the only difference is that the branch, state, and tag names carry the component name.

Target a subset of the ladder per component

Section titled “Target a subset of the ladder per component”

A component does not have to ride the full shared ladder. Override environments: on the component to give it a shorter chain, as web does above with [dev, prod]. The shared default stays whatever the top-level environments list declares, and each component narrows it where that makes sense. A component’s release and prerelease markers are the last and second-to-last entries of its own resolved ladder.

If you never write a components: block, none of the above applies. The whole repository is one unit, the top-level tag_prefix is the one tag namespace, the top-level environments is the one ladder, and the generated workflows, state serialization, version lines, and tags are byte-identical to a pre-component setup. Components are strictly additive: adopt them when you have a second thing to version, and ignore them entirely until then.

Components are independent by design. Cascade does not sequence one component after another, build a cross-component dependency graph, or add a fan-in barrier across components. When you need one unit to react to another, use the external and notify coordination path described in Coordinate multiple repos, which works across components and repositories alike.

Prerequisite: How Cascade works for the trunk, environment chain, and release boundary each component follows.

Reference: the components field reference for the override matrix and Per-component versioning for the tag-namespace and state rules.