Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 63 additions & 51 deletions decisions/0008-branching-model.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 8. Branching Model

- **Status:** PROPOSED
- **Date:** 2026-05-29
- **Deciders:** TBD
- **Status:** ACCEPTED
- **Date:** 2026-07-25
- **Deciders:** ss-o
Comment thread
ss-o marked this conversation as resolved.
- **Supersedes:** None
- **Superseded by:** None

Expand All @@ -16,44 +16,54 @@ inconsistency is real, not cosmetic:
- Others are trunk-based on `main` only (`zd`, packaged `zsh`,
`z-a-meta-plugins`, `zsh-fancy-completions`, `zunit`, `.github`).

The meta-workspace catalog (`workspace/repos.yml`) had drifted from this reality
and had to be reconciled by inspecting live remotes. The root cause is that no
decision says *which class of repo uses a `next` branch and which does not*, so
each repo's model is discovered empirically rather than governed. `zsh-lint`
recently gained a `next` branch during its Go reboot, which re-surfaced the
ambiguity.
An audit on 2026-07-18 found that all listed repositories use `main` as their
GitHub default branch. Only `src`, `wiki`, `zi`, `zsh-lint`, and `zsh-eza` have
a live `next` branch for development or integration. In this ADR, **GitHub
default branch** and **development branch** are therefore separate concepts.

The private meta-workspace catalog (`workspace/repos.yml`) is still out of date. Its
`default_branch` field conflates those concepts, and some entries name `next`
for repositories that do not have that branch. The catalog has not yet been
reconciled; that is a separate meta-workspace change tracked outside this ADR.

`decisions/0007-release-publication-flow.md` already defines four repository
classes by delivery model. Branching should be derived from those classes rather
than decided per repo, so the catalog stops drifting at the source.
classes by delivery model. Those classes constrain publication behavior, but
the live branch inventory shows that class alone does not select a repository's
development branch.

## Decision

The **canonical per-repo table below is the authoritative source** for branch
model, and `workspace/repos.yml` derives from it. The ADR-0007 repository class
is an *input* to the choice (it sets the publication boundary and a default), but
it does **not** by itself determine the branch model — repo churn/scale does.
Reality confirms this: within class 1, `wiki`/`src` run `next` → `main` while
`zd` is trunk-only; within class 2, `zsh-lint` uses `next` while `zunit` is
trunk. So the table, not the class, is binding. There is no ad-hoc per-repo
discretion: a repo's branch model is whatever this table says, and changing it
requires amending this ADR (or a superseding one), not creating a branch.
The **canonical per-repo table below is the authoritative source** for the
development branch and branch model. The ADR-0007 repository class is an
_input_ to the choice because it sets the publication boundary and a default,
but it does **not** by itself determine the branch model — repo churn/scale
does. Within class 1, `wiki`/`src` use `next` → `main` while `zd` is
trunk-only; within class 2, `zsh-lint` uses `next` while `zunit` is trunk.
Changing a repository's assigned model requires amending this ADR (or a
superseding one), not merely creating or deleting a branch.

### Canonical branch model

| Repo | Class | Branch model | Development branch | Publication boundary |
| ---------------------- | ----- | ---------------- | ------------------ | --------------------------- |
| `wiki` | 1 | `next` → `main` | `next` | merge to `main` (deploy) |
| `src` | 1 | `next` → `main` | `next` | merge to `main` (deploy) |
| `zd` | 1 | trunk on `main` | `main` | push to `main` (image) |
| `zunit` | 2 | trunk on `main` | `main` | `vX.Y.Z` tag |
| `zsh-lint` | 2 | `next` → `main` | `next` | `vX.Y.Z` tag |
| packaged `zsh` | 2 | trunk on `main` | `main` | `vX.Y.Z` tag (deferred) |
| `zi` | 3 | `next` → `main` | `next` | `main` is consumable ref |
| `zsh-eza` | 3 | `next` → `main` | `next` | `main` is consumable ref |
| `z-a-meta-plugins` | 3 | trunk on `main` | `main` | `main` is consumable ref |
| `zsh-fancy-completions`| 3 | trunk on `main` | `main` | `main` is consumable ref |
| `.github` | 4 | trunk on `main` | `main` | n/a |
| Repo | Class | Branch model | Development branch | Publication boundary |
| ----------------------- | ----- | --------------- | ------------------ | ------------------------ |
| `wiki` | 1 | `next` → `main` | `next` | merge to `main` (deploy) |
| `src` | 1 | `next` → `main` | `next` | merge to `main` (deploy) |
| `zd` | 1 | trunk on `main` | `main` | push to `main` (image) |
| `zunit` | 2 | trunk on `main` | `main` | `vX.Y.Z` tag |
| `zsh-lint` | 2 | `next` → `main` | `next` | `vX.Y.Z` tag |
| packaged `zsh` | 2 | trunk on `main` | `main` | `vX.Y.Z` tag (deferred) |
| `zi` | 3 | `next` → `main` | `next` | `main` is consumable ref |
| `zsh-eza` | 3 | `next` → `main` | `next` | `main` is consumable ref |
| `z-a-meta-plugins` | 3 | trunk on `main` | `main` | `main` is consumable ref |
| `zsh-fancy-completions` | 3 | trunk on `main` | `main` | `main` is consumable ref |
| `.github` | 4 | trunk on `main` | `main` | n/a |

The publication-boundary column states the policy, not a complete inventory of
live workflow triggers. At the 2026-07-18 audit, `src` and `zd` also had
semantic-tag publication triggers that the table does not capture. Those
triggers are accepted class-1 compatibility publication paths that run
alongside the table's `main`-based boundary, not a deviation from it; they are
not scheduled for removal by this decision.

### How the class informs the default

Expand All @@ -71,25 +81,26 @@ requires amending this ADR (or a superseding one), not creating a branch.
branch (the "Development branch" column). For trunk repos, feature branches also
start from `main`.

`workspace/repos.yml` mirrors this table and must match it. Adding or removing a
repo's `next` branch is a decision recorded here first, then reflected in the
catalog in the same change.
The private catalog/schema and root agent guidance need a separate
meta-workspace reconciliation. They are deliberately not changed by this
public factual-reconciliation change.

## Consequences

- `workspace/repos.yml` derives from the canonical table above, so drift is
structurally prevented: the catalog is validated against an explicit table, not
"use judgment."
- New repos are added to the table (with their ADR-0007 class) as part of repo
creation, before the first branch is cut.
- **Action on acceptance:** the workspace `CLAUDE.md` currently states "default
development branch: `next` … all other work branches from `next`" as a universal
rule. On acceptance, update that section to reference this ADR's per-repo table
so agents do not get conflicting guidance for trunk-only repos. (Not done while
this ADR is PROPOSED — `CLAUDE.md` should not cite an unaccepted decision.)
- Promotion from `next` to `main` is a publication boundary only for class 1
(deploy) repos; for other classes the merge validates but does not mint a
release (consistent with ADR-0007).
- The table gives branch-policy audits an explicit public comparison point;
the ADR alone does not prevent catalog or repository drift.
- New repositories are added to the table (with their ADR-0007 class) as part
of repository creation, before the first branch is cut.
- **Pending action:** the private meta-workspace root guidance currently
states "default development branch: `next` … all other work branches from
`next`" as a universal rule. That guidance should be updated to reference
this ADR's per-repository table so agents do not get conflicting instructions
for trunk-only repositories. This separate meta-workspace change has not yet
been made.
- Promotion from `next` to `main` is a publication trigger for class-1 deploy
repositories; the live tag-trigger exceptions above are accepted alongside
it. For other classes the merge validates but does not mint a release
(consistent with ADR-0007).

## Alternatives considered

Expand All @@ -100,10 +111,11 @@ catalog in the same change.
from a staging branch before a change goes live, and `zi`'s scale warrants an
integration buffer.
- **Leave it per-repo and informal.** Rejected: that is the status quo that let
the catalog drift and required a manual remote audit to repair.
the catalog drift and required a manual remote audit to identify.

## References

- `decisions/0007-release-publication-flow.md` — repository classes this builds on.
- `workspace/repos.yml` (meta-workspace) — per-repo branch model catalog.
- `decisions/0003-conventional-commits.md` — commit/branch naming conventions.
- [Issue #454](https://github.com/z-shell/.github/issues/454) — dated live audit
and maintainer decision record.
109 changes: 67 additions & 42 deletions decisions/0009-testing-ci-strategy.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 9. Testing and CI Strategy

- **Status:** PROPOSED
- **Date:** 2026-05-29
- **Deciders:** TBD
- **Status:** ACCEPTED
- **Date:** 2026-07-25
- **Deciders:** ss-o
Comment thread
ss-o marked this conversation as resolved.
- **Supersedes:** None
- **Superseded by:** None

Expand All @@ -12,52 +12,65 @@ CI exists across the org's repos but its scope is decided per repo and per
workflow rather than from a shared policy. The result is uneven: some repos run
`zsh -n` syntax checks plus `zcompile`, others add ZUnit suites, `zsh-lint` has
Go tests, and the container repo builds a multi-arch matrix. There is no written
statement of *what level of testing each repository class owes*, which makes it
statement of _what level of testing each repository class owes_, which makes it
hard to know whether a repo is under- or over-tested, and what a reviewer should
require before merge.

ADR-0007 classified repos by delivery model and ADR-0008 derived branching from
those classes. Testing depth should be derived the same way, so CI scope is a
property of the repo class rather than an accident of history.
ADR-0007 classified repositories by delivery model and ADR-0008 establishes a
branch model informed by those classes. This ADR similarly establishes a
target CI scope by class, accepted with owned rollout gaps: it does not claim
that every listed control is already live or configured as a required check.

## Decision

Required CI scope follows the ADR-0007 repository class. Every class shares a
common baseline; higher-risk classes add to it.
common target baseline; higher-risk classes add to it. Live rollout gaps and
the dated evidence behind them are tracked in issue #454 rather than copied
into this ADR as a transient workflow inventory.

### Baseline (all repos)

- All workflows comply with the org workflow conventions (SHA-pinned actions,
Every item below is target policy — the baseline every repository is expected
to meet, not an assertion that each control is already live everywhere. Live
rollout status per repository is tracked in issue #454, not restated here.

- Workflows comply with the org workflow conventions (SHA-pinned actions,
least-privilege `permissions:`, `concurrency:`, no-emoji `name:` per ADR-0005,
kebab-case filenames). These conventions are defined in the workspace `CLAUDE.md`
and the CI instructions — this ADR references them rather than restating, so
there is a single source of truth.
kebab-case filenames), defined in
`.github/instructions/github-actions-ci-cd-best-practices.instructions.md`
and `decisions/0005-workflow-naming-conventions.md`; `AGENTS.md` only
summarizes them.
- Zsh sources pass `zsh -n` (syntax) and `zcompile` (compile) checks.
- Dependency and secret scanning per `decisions/0004-dependabot-unification.md`.
- **Target state (not yet a live org-wide control):** Conventional Commits
(ADR-0003) and the disallowed-trailer rule enforced in CI. Neither is a live
org-wide gate today: a sweep of the default branches of all 86 active,
non-fork, non-archived repositories found no workflow that runs the
- Dependency-update and vulnerability-remediation ownership follows accepted
ADR-0012. ADR-0012 does **not** establish organization-wide secret-scanning
coverage; that requires separately configured and verified controls.
- Conventional Commits (ADR-0003), PR-title validation, and the
disallowed-trailer rule are enforced in CI. None of these is a live org-wide
gate today: a sweep of the default branches of all 86 active, non-fork,
non-archived repositories found no workflow that runs the
`DISALLOWED_TRAILER_PATTERN` check or validates commit messages. Non-default
branches were not swept. The only implementation is a `commit-lint` workflow
on the `next`
branch of `z-shell/zi`, which has never reached a default branch. Both the
trailer ban and Conventional Commits are convention-and-review until a shared
commit/PR-title-lint check is promoted into a reusable workflow here and rolled
out; that work is tracked separately.
on the `next` branch of `z-shell/zi`, which has never reached a default
branch.

### By class

1. **Continuously deployed artifacts** (`wiki`, `src`, `zd`) — build must succeed
on the development branch before deploy. Wiki runs lint (ESLint/Stylelint) and
a production build; `zd` runs the Docker build matrix; `src` validates the
installer/loader. CodeQL where a supported language is present.
2. **Versioned tools and packages** (`zunit`, `zsh-lint`, packaged `zsh`) — full
functional test suite is **required** and gates release tags. ZUnit for Zsh
tools; `go test` for the `zsh-lint` Go CLI. A release tag must not be cut from
a commit whose suite is red. Compiled tools additionally run SAST (CodeQL
and/or `gosec` for the `zsh-lint` Go CLI); a release artifact is part of the
security surface governed by `decisions/0010-security-incident-response.md`.
1. **Continuously deployed artifacts** (`wiki`, `src`, `zd`) — the build succeeds
on the development branch before deploy. The target checks cover the wiki's
lint and production build, `zd`'s Docker build matrix, and `src`'s
installer/loader validation, plus CodeQL where a supported language is
present.
2. **Versioned tools and packages** (`zunit`, `zsh-lint`, packaged `zsh`) — a full
functional suite is **required** before a release tag is cut, defined per
tool: `zunit`'s release workflow must run its own native suite on the exact
tag commit before publication, since prior branch checks alone do not prove
the released commit; packaged `zsh`'s gate is metadata/manifest validation
plus a clean disposable install and a startup/version smoke test, all on
the exact tag commit, since metadata-only validation alone is not a
functional suite; `zsh-lint`'s gate is `go test` on the exact tag commit.
Compiled tools additionally run an appropriate SAST control, such as
CodeQL or `gosec`; a release artifact is part of the security surface
governed by `decisions/0010-security-incident-response.md`.
3. **Git-consumed source** (`zi`, most plugins/annexes) — **validation-only**: the
baseline checks above, plus ZUnit where the plugin ships tests. No release
automation and no coverage gate; these repos are consumed from source and the
Expand All @@ -72,19 +85,28 @@ ratcheting is a per-repo maintainer decision.

### Required checks

Each repo marks its class-appropriate checks as required for merge to its
publication branch (`main`, or `next`→`main` per ADR-0008). Validation-only repos
require the baseline; class-2 repos additionally require the functional suite.
Under the target policy, each repository marks its class-appropriate checks as
required for merge to its publication branch (`main`, or `next` → `main` per
ADR-0008). Validation-only repositories require the baseline; class-2
repositories additionally require the functional suite. Required checks,
development-branch validation, SAST coverage, and the release-suite gate are
each verified repository by repository through owning issues; the presence of
a workflow file is not by itself treated as proof that a check is required or
that a ruleset enforces it.

## Consequences

- A reviewer can determine the expected CI bar from the repo's class instead of
reading each workflow.
- New repos start with the correct CI scope for their class.
- "Validation-only for git-consumed repos" is now an explicit rule, preventing
release machinery from creeping into class-3 repos (consistent with ADR-0007).
- A reviewer can determine the target CI bar from the repository's class
instead of inferring policy from each workflow.
- New repositories have an explicit target CI scope for their class.
- "Validation-only for git-consumed repositories" becomes an explicit rule,
discouraging release machinery in class-3 repositories (consistent with
ADR-0007).
- The testing instruction file (`.github/instructions/testing.instructions.md`)
operationalizes this ADR for day-to-day authoring.
currently reads as present-tense enforcement (for example, it states
Conventional Commits and the disallowed-trailer check "are enforced"). That
is target-state language, not evidence of current conformance, and the file
should be reconciled to say so. It is not changed by this decision.

## Alternatives considered

Expand All @@ -102,6 +124,9 @@ require the baseline; class-2 repos additionally require the functional suite.
- `decisions/0007-release-publication-flow.md` — repository classes.
- `decisions/0008-branching-model.md` — branch model per class.
- `decisions/0005-workflow-naming-conventions.md` — workflow naming baseline.
- `decisions/0004-dependabot-unification.md` — dependency scanning baseline.
- `decisions/0012-hybrid-dependency-management.md` — dependency-update and
vulnerability-remediation ownership.
- `decisions/0010-security-incident-response.md` — SAST/release security surface.
- `z-shell/zd` `.github/workflows/test-native.yml` — reusable ZUnit workflow.
- [Issue #454](https://github.com/z-shell/.github/issues/454) — dated rollout-gap
evidence and maintainer decision record.
Loading
Loading