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
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Treat this file as authoritative for everything else; don't restate its rules el
The specific rules in this file implement a few governing principles. Read these first: they are the reason the branching, release, and versioning rules are shaped the way they are, and every rule below serves one of them.

- **Distribution respects the user: pull by default, push only where the channel forces it.** Docker images, GitHub Releases, and NuGet/PyPI packages are **pull** - the user decides when to consume them. A few channels are **push**: HACS surfaces a new release to every installed user as a pending update they did not go looking for, and a consumer that vendors from `main` picks up its current state. Because a release can reach users who did not ask for it, releasing is a deliberate act that marks a real functional change - never mechanical churn. This is why a **human merge never auto-publishes** - a release is a deliberate `workflow_dispatch`, or a conditional auto-release when the App merges a code-affecting Dependabot/codegen PR to `main` (Docker also refreshes on a weekly schedule) - together with the no-op republish guarantee and maintainer-gated version bumps: a needless release spends the user's attention and, on a push channel, acts on their machine.
- **Both branches stay in sync, so a promotion never needs a back-merge.** Dependabot and codegen target `develop` and `main` in parallel, so neither branch drifts and a `develop -> main` promotion stays a clean forward merge by default. That is exactly what lets the model be **signed, linear, and free of back-merges** - forward sync removes any need to merge `main` back into `develop`, which the rules forbid. If sync is ever broken (a change lands on one branch only, or normalizes a file on one side), restore it forward-only; never back-merge. See "Branching Model". (The auto-publish rules below describe `release` repos only; **operational** repos - registry `workflowModel: operational` - commit directly to `develop`, run no codegen or auto-publish bots, and cut a GitHub release **only** by manual `workflow_dispatch` (`releaseTrigger: dispatch-only`) - never automatically. Dependabot's dual-target sync and the App-signed merge-bot run on **every** tier, operational included. See "Branching Model".)
- **Both branches stay in sync, so a promotion never needs a back-merge.** Dependabot and codegen target `develop` and `main` in parallel, so neither branch drifts and a `develop -> main` promotion stays a clean forward merge by default. That is exactly what lets the model be **signed, linear, and free of back-merges** - forward sync removes any need to merge `main` back into `develop`, which the rules forbid. If sync is ever broken (a change lands on one branch only, or normalizes a file on one side), restore it forward-only, never back-merge. See "Branching Model". (These auto-publish rules describe `release` repos. **Operational** repos differ - direct-to-`develop`, dispatch-only release - see "Operational Repositories".)
- **Two version numbers, two jobs.** The 2-digit `major.minor` in `version.json` carries human meaning - the maintainer raises it only for a functional change (feature, behavior or API change, breaking change), at their discretion - while NBGV owns the patch position and always increments with git height, so every build is uniquely versioned with no edit. Human-facing docs name the 2-digit line; the toolchain guarantees monotonic builds. See "Release Model".
- **Contracts state what, not how, and favor reuse.** [`WORKFLOW.md`](./WORKFLOW.md) fixes required outcomes, not a required implementation - two repos may satisfy a guarantee with different YAML. Within that freedom, apply good engineering practice: minimize duplication and maximize reuse, which is why the pipeline splits a carried, generic orchestration layer from a repo-owned build layer.

Expand All @@ -34,7 +34,7 @@ A state-changing GitHub call is the highest-blast-radius thing an agent does her

## Branching Model

- **Two workflow models, set per repo by the registry [`workflowModel`](./registry/repos.json) field.** Most repos are `release`: they ship versioned units of delivery through the feature -> `develop` -> `main` flow this section describes. **Operational** repos (live-service config - Home Assistant, ESPHome, Vantage, home automation) instead track the running state of a service: the maintainer commits configuration **directly to `develop`** (there is no feature branch) and *occasionally* opens a `develop -> main` PR to bless a known-good snapshot. For an operational repo the [`develop` ruleset](./repo-config/operational/develop.json) drops the PR and status-check gate - direct signed pushes are allowed (force-push, deletion, and unsigned commits are still blocked) and CI runs on the push as **advisory** feedback that never rejects a commit. The [`main` ruleset](./repo-config/main.json) is shared and **unchanged**, so the promotion PR still **enforces** the required `Check pull request workflow status job`; that check is lint/validation only (editorconfig/EOL plus domain linters - a Home Assistant or ESPHome config validation, a firmware build - never unit tests), so `develop` stays the live surface and a broken config can never reach `main`. Operational repos still cut GitHub releases (tag + source zip), but **only** by manual `workflow_dispatch` (`releaseTrigger: dispatch-only`), never automatically - see "Release Model". The rest of this section is the `release` model unless noted; the promotion mechanics (never delete `develop`, EOL-conflict resolution) apply to both.
- **Two workflow models, set per repo by the registry [`workflowModel`](./registry/repos.json) field.** Most repos are `release`: they ship versioned units of delivery through the feature -> `develop` -> `main` flow this section describes. **Operational** repos instead track a live service's running state and differ substantially - direct-to-`develop`, advisory CI, dispatch-only release - see "Operational Repositories". The rest of this section is the `release` model unless noted. The promotion mechanics (never delete `develop`, EOL-conflict resolution) apply to both.
- `develop` is the integration branch. Feature branches -> `develop` is **squash-only**; develop is kept linear.
- `develop` -> `main` is **merge-commit only** (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main, which lets the release model attribute releases to the develop commits that produced them (see "Release Model" below). Branch protection enforces this: the develop ruleset allows only `squash`, the main ruleset allows only `merge`.
- All commits on both branches must be cryptographically signed (SSH or GPG). Squash and merge commits created via the GitHub UI are signed by GitHub's web-flow key.
Expand All @@ -56,7 +56,7 @@ A state-changing GitHub call is the highest-blast-radius thing an agent does her

## Release Model

The **two-phase model is the default**: PRs build fast, publishing is batched. See [README "Release Distribution Model"](./WORKFLOW.md) for the full rationale; the load-bearing rules. The auto-publish paths (bot push, schedule) apply to `release` repos; **operational** repos carry `releaseTrigger: dispatch-only`, so they publish **only** on a manual `workflow_dispatch` - the same source-only release the publisher already supports (tag + source zip + README + LICENSE, NBGV-versioned) - never automatically. Their `develop -> main` promotion just blesses a known-good config snapshot; a release is a separate, deliberate dispatch (see "Branching Model").
The **two-phase model is the default**: PRs build fast, publishing is batched. See [README "Release Distribution Model"](./WORKFLOW.md) for the full rationale. The load-bearing rules follow. The auto-publish paths (bot push, schedule) apply to `release` repos. **Operational** repos differ - dispatch-only release, no auto-publish - see "Operational Repositories".

- **PRs smoke-test only.** [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) always runs unit tests, then a `dorny/paths-filter` `changes` job gates a **reduced** build of only the changed targets (Docker `linux/amd64` only, executable on a representative runtime subset), never pushing. Build-workflow files are intentionally not in the path filters - a filter can't tell a logic change from an action-version bump - so a workflow-only change isn't smoke-built; the reusable workflows are exercised by the next run that uses them (a later code PR's smoke build, or the scheduled/publish run). Workflow YAML is still linted in CI by the lint job's `actionlint` step; also run `actionlint` locally before pushing to catch issues early.
- **A human merge never auto-publishes.** [`publish-release.yml`](./.github/workflows/publish-release.yml) is the sole publisher; each run builds the **single trigger branch** (`main` a release, `develop` a prerelease). A first [`plan`](./catalog/snippets/workflows/publish-plan-task.yml) job decides once whether the run publishes and every other job gates on its output. It publishes on a **`workflow_dispatch`** of `main`/`develop` (the human-initiated release), a **code-affecting bot push to `main`** (the codegen App merges every Dependabot/codegen PR, so `github.actor` is the gate - a human merge/promotion to `main` skips), or a **weekly `schedule`** (Docker only, to refresh the base image). The `push` is main-only and paths-filtered, so a develop bot merge and an Actions-only bump publish nothing. A source-only repo publishes on dispatch only.
Expand All @@ -79,6 +79,19 @@ The **two-phase model is the default**: PRs build fast, publishing is batched. S
- **Issue-closing keywords (`Closes #N`, `Fixes #N`) go in the `develop -> main` promotion PR, not the feature -> develop PR.** GitHub auto-closes an issue only when the closing keyword merges into the **default branch** (`main`); a feature/develop PR merges into `develop`, so the keyword never fires there. Reference the issue in the develop PR body if useful, but put the actual closing keyword on the promotion PR.
- **Wrapper repos that track an upstream release.** A repo wrapping an upstream release uses [`check-upstream-version-task.yml`](./catalog/snippets/workflows/check-upstream-version-task.yml): a resolver command prints the upstream version(s) as a **JSON object of `name -> version`**, written to a committed state file at the **repo root beside `version.json`** (default `upstream-version.json` - it is a build-input version source, not GitHub-platform config, so it does not belong under `.github/`), and opens a rolling App-signed bump PR per branch that the merge-bot auto-merges (`merge-upstream-version`). The object carries one key for the common single-version case (`{"version":"X"}`) or N keys for a wrapper that pins several upstream components (e.g. an image plus a companion tool), and the build reads each component by key; the bump PR's title/body name only the keys that actually moved. Call it from a scheduled entry-point workflow and matrix only the branches that ship the version (a CI-only version uses `["develop"]`). A merged bump ships on the **next publish**, not immediately - the two-phase latency tradeoff.

## Operational Repositories

The registry [`workflowModel`](./registry/repos.json) field is `release` (the default) or `operational`. This section is the operational delta - every other rule in this file is the `release` model unless it says otherwise.

**Operational** repos track a live service's running state rather than shipping versioned units of delivery - live-service config such as Home Assistant, ESPHome, Vantage, and home automation.

- **Commit configuration directly to `develop`.** There is no feature branch - the maintainer commits straight to `develop` and *occasionally* opens a `develop -> main` PR to bless a known-good snapshot. The [`develop` ruleset](./repo-config/operational/develop.json) drops the PR and status-check gate, so direct signed pushes are allowed (force-push, deletion, and unsigned commits are still blocked) and CI runs on the push as **advisory** feedback that never rejects a commit.
- **The `main` promotion gate is unchanged.** The [`main` ruleset](./repo-config/main.json) is shared with `release` repos, so the `develop -> main` PR still **enforces** the required `Check pull request workflow status job`. For an operational repo that check is lint/validation only - editorconfig/EOL plus domain linters (a Home Assistant or ESPHome config validation, a firmware build), never unit tests - so `develop` stays the live surface and a broken config can never reach `main`.
- **Release only by manual dispatch.** Operational repos carry `releaseTrigger: dispatch-only` and run no codegen or auto-publish bots, so they publish **only** on a manual `workflow_dispatch` - the source-only release the publisher already supports (tag + source zip + README + LICENSE, NBGV-versioned), never automatically. The `develop -> main` promotion just blesses a known-good snapshot, and a release is a separate, deliberate dispatch.
- **Fleet sync still applies.** Dependabot's dual-target sync and the App-signed merge-bot run on **every** tier, operational included, so both branches stay in sync and a promotion stays a clean forward merge.

Line-ending governance for an operational repo is in [Line Endings](#line-endings) - its `[*]` default follows the consuming app's native platform per the registry `lineEndings` field, not the fleet CRLF default.

## Repository Onboarding and Conformance

Every fleet repo is a standard-style repo the hub audits **downward** against its declared type - the model the fleet uses because managing downstream divergence is too costly. Three obligations follow, and they are not optional:
Expand Down
2 changes: 1 addition & 1 deletion spec/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "./files.schema.json",
"note": "The standardization baseline: files and sections a fleet repo is expected to carry, and their intent authority. The audit checks presence (letter) and equivalence (intent); a section for an absent language or target is N/A. Each entry, and each section, carries an appliesTo selector - see spec/scope-model.md for the scope model and selector vocabulary.",
"baseline": [
{ "path": "AGENTS.md", "sections": ["Repository Boundaries and Write Safety", "Git and Commit Rules", "Branching Model", "Release Model", "Pull Request Title and Commit Message Conventions", "Documentation Style Conventions", "PR Review Etiquette", "Workflow YAML Conventions"], "intentRef": "AGENTS.md", "appliesTo": "*" },
{ "path": "AGENTS.md", "sections": ["Repository Boundaries and Write Safety", "Git and Commit Rules", "Branching Model", "Release Model", { "name": "Operational Repositories", "appliesTo": ["operational"] }, "Pull Request Title and Commit Message Conventions", "Documentation Style Conventions", "PR Review Etiquette", "Workflow YAML Conventions"], "intentRef": "AGENTS.md", "appliesTo": "*" },
{ "path": "CODESTYLE.md", "whole": true, "placeholders": ["InternalsVisibleTo project names"], "intentRef": "CODESTYLE.md", "appliesTo": "*" },
{ "path": "WORKFLOW.md", "whole": true, "intentRef": "WORKFLOW.md", "appliesTo": "*" },
{ "path": "README.md", "appliesTo": "*" },
Expand Down