Skip to content

Commit 0204519

Browse files
authored
Merge pull request #91 from frostyard/fluent/repository-settings-contract
feat(organization): publish the repository settings contract (ADR-0040)
2 parents f370ed6 + 2334bc9 commit 0204519

14 files changed

Lines changed: 922 additions & 4 deletions

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Docs are split by the question they answer:
5252
- [0037 — Publish executable organization goals](adr/0037-publish-executable-organization-goals.md)
5353
- [0038 — make ci stays canonical; the TestI/Integration name filter is chairlift-only](adr/0038-scope-the-test-name-filter-to-chairlift.md)
5454
- [0039 — Widen `maintenance_programs` within repository schema v1](adr/0039-widen-maintenance-programs-within-schema-v1.md)
55+
- [0040 — Publish the repository settings contract](adr/0040-publish-the-repository-settings-contract.md)
5556

5657
### Design
5758

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# 0040 — Publish the repository settings contract
2+
3+
- **Status:** Accepted
4+
- **Date:** 2026-08-18
5+
6+
## Context
7+
8+
The organization tree ([ADR-0035](0035-author-organization-authority-as-strict-json.md))
9+
says which repositories are in the fleet, what programs they run, and which
10+
files they must carry (the surfaces contract). It says nothing about the
11+
GitHub *repository settings* that decide whether that maintenance is safe:
12+
whether merged branches are deleted, whether the default branch can be pushed
13+
to directly, which checks are required and from which producer, what
14+
permissions a workflow token gets, whether secret scanning is on.
15+
16+
A survey of the five fleet repositories on 2026-08-18 found no default-branch
17+
ruleset or protection anywhere; workflow tokens with `write` and the right to
18+
approve pull requests on four of five; secret scanning off everywhere;
19+
Dependabot alerts off on one; branch deletion on merge on one; two without a
20+
license. Fluent's own required-checks evaluator
21+
([Fluent ADR-0056](https://github.com/frostyard/fluent/blob/main/docs/adr/0056-derive-required-checks-from-enforced-github-rules.md))
22+
had already specified the v1 default-branch ruleset that nothing had enabled.
23+
Each of these is an admin action a person clicks or forgets; none is reviewed,
24+
none is recorded, and drift is invisible.
25+
26+
## Decision
27+
28+
- Core publishes **one repository settings contract**,
29+
`organization/contracts/repository-settings/v1.json`, validated by
30+
`organization/schemas/v1/repository-settings.schema.json` and conformance
31+
fixtures, alongside the surfaces contract and under the same rules: strict
32+
JSON, closed vocabularies, immutable schema version once a consumer supports
33+
it (compatible enum widening per [ADR-0039](0039-widen-maintenance-programs-within-schema-v1.md)).
34+
- Version one is **organization-wide**: every enrolled repository must match
35+
every value. It covers merge hygiene (delete branch on merge, suggest
36+
updating branches, no auto-merge, merge methods and commit-message
37+
defaults so the conventional pull-request title becomes the commit subject,
38+
wiki and projects off), the default-branch ruleset (active, no bypass, pull
39+
request required with zero approvals, conversation resolution required,
40+
strict required status checks, no deletion or force push, no merge queue,
41+
no classic protection — the Fluent ADR-0056 shape), a tag ruleset on `v*`
42+
(no deletion or force update, creation restricted), Actions token
43+
permissions (read, cannot approve pull requests), security features
44+
(Dependabot alerts and security updates, secret scanning with push
45+
protection, private vulnerability reporting), metadata (license,
46+
description, the `frostyard` topic), and the labels the fleet depends on
47+
(`fluent`). Values a repository may legitimately differ on — visibility,
48+
discussions, code scanning, the exact required-check names — are
49+
deliberately not in version one; they are observed, not required, until a
50+
later version adds per-repository declarations.
51+
- **The contract is read, not applied, by Fluent.** Fluent's conformance
52+
sweep compares each enrolled repository's live settings with the contract
53+
through read-only GitHub calls and proposes drift for the operator; Fluent
54+
holds no admin credential and changes no setting.
55+
- **Applying is a human act, scripted here**: `scripts/apply-repo-settings.sh
56+
<owner/repo>` issues the exact idempotent GitHub API calls the contract
57+
implies (dry-run by default), and takes the repository's required-check
58+
names as arguments because the contract does not carry them. An operator
59+
runs it once per repository and again whenever Fluent reports drift.
60+
- The contract may only tighten in place: a change that would relax a value
61+
is a new ADR, not an edit.
62+
63+
## Consequences
64+
65+
- Repository safety settings become reviewed organization decisions with a
66+
digest, like declarations and surfaces; drift becomes a proposal in
67+
Fluent's inbox rather than a surprise.
68+
- Enabling the default-branch ruleset changes how every fleet repository is
69+
updated: no direct pushes to `main`, every change through a pull request
70+
whose named checks pass. Fluent's required-checks evaluator finally has
71+
enforced rules to observe.
72+
- Version one cannot express per-repository variance; a repository that
73+
needs it (a private repository, discussions on) is a finding until a later
74+
version — acceptable for a five-repository fleet, revisited with the first
75+
real exception.
76+
- Consumers that pin schema digests (Fluent) bundle the new schema before
77+
this merges; older Fluent revisions accept a core tree without the contract
78+
(it is an addition, like verification profiles were).
79+
80+
## Alternatives considered
81+
82+
- **Per-repository settings inside each declaration:** rejected for version
83+
one; adds a field to the declaration schema (a compatibility question under
84+
ADR-0039) for variance the fleet does not yet have.
85+
- **Enforce from Fluent with an admin token:** rejected; Fluent's boundary is
86+
read and propose, and an admin credential in the coordinator is a wider
87+
blast radius than any setting it would fix.
88+
- **Organization-level rulesets and GitHub's own policy features only:**
89+
useful later, but they are not reviewable JSON in core and do not cover
90+
merge hygiene, Actions permissions, or labels; the contract can point at
91+
them once they exist.
92+
93+
## References
94+
95+
- Shapes: [organization repository enrollment](../specs/organization-repository-enrollment.md),
96+
[organization authority](../design/organization-authority.md),
97+
Fluent's [maintenance programs plan](https://github.com/frostyard/fluent/blob/main/docs/plans/maintenance-programs.md)
98+
and [required-check ruleset operations](https://github.com/frostyard/fluent/blob/main/docs/design/required-check-ruleset-operations.md)
99+
- Builds on: [ADR-0035](0035-author-organization-authority-as-strict-json.md),
100+
[ADR-0019](0019-governance-as-code-and-risk-tiers.md),
101+
[ADR-0039](0039-widen-maintenance-programs-within-schema-v1.md)

docs/specs/organization-repository-enrollment.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,34 @@ surface IDs and canonical repository paths:
6565
| `agent-skills` | `.agents/skills` | `directory` | none |
6666
| `documentation-index` | `docs/README.md` | `file` | none |
6767

68+
### Repository settings contract
69+
70+
The version-one repository settings contract lives only at
71+
`organization/contracts/repository-settings/v1.json`, validated by
72+
`organization/schemas/v1/repository-settings.schema.json`
73+
([ADR-0040](../adr/0040-publish-the-repository-settings-contract.md)). It is
74+
organization-wide: every enrolled repository is expected to match every value.
75+
76+
| Section | What it fixes |
77+
| --- | --- |
78+
| `repository` | delete branch on merge, suggest updating branches, no auto-merge, allowed merge methods, merge/squash commit title and message defaults, wiki/projects/issues, web sign-off |
79+
| `actions` | default workflow token permissions, whether workflows may approve pull requests |
80+
| `security` | Dependabot alerts and security updates, secret scanning and push protection, private vulnerability reporting |
81+
| `default_branch_ruleset` | active, no bypass, pull request required with the approval count, conversation resolution, strict required status checks, no deletion or force push, no merge queue, no classic protection |
82+
| `tag_ruleset` | tag pattern, no deletion or force update, creation restricted |
83+
| `metadata` | license and description required, topics that must be present |
84+
| `labels` | labels the fleet depends on |
85+
86+
The contract names no per-repository values (visibility, discussions, code
87+
scanning, the exact required-check contexts); those are observed by the
88+
consumer, not required, until a later version. Fluent reads the contract and
89+
proposes drift; `scripts/apply-repo-settings.sh <owner/repo> [--apply]
90+
[--required-checks "<ctx>,…"]` applies it — dry-run by default, idempotent,
91+
never deleting a ruleset it did not create, and never writing a license or
92+
description (those are pull requests and settings the operator makes). The
93+
tag ruleset it creates lets repository admins bypass, so a maintainer's
94+
`make bump` still creates the tag and nobody else can.
95+
6896
## Rules
6997

7098
- Every JSON document MUST be UTF-8, MUST reject duplicate keys, and MUST
@@ -94,6 +122,9 @@ surface IDs and canonical repository paths:
94122
platform, organization, enrollment, root-work, or delegated ceiling.
95123
- `npm run check:organization` MUST validate live records and both valid and
96124
invalid conformance fixtures using the same parser and schemas.
125+
- The repository settings contract MUST require a pull request whenever it
126+
requires status checks, and its tag ruleset MUST block deletion or restrict
127+
creation; a change that relaxes any value is a new ADR, not an edit.
97128

98129
## Derived artifacts
99130

@@ -108,5 +139,7 @@ surface IDs and canonical repository paths:
108139
- Rationale:
109140
[ADR-0035](../adr/0035-author-organization-authority-as-strict-json.md),
110141
[ADR-0039](../adr/0039-widen-maintenance-programs-within-schema-v1.md)
111-
(compatible enum widening within a schema version)
142+
(compatible enum widening within a schema version),
143+
[ADR-0040](../adr/0040-publish-the-repository-settings-contract.md)
144+
(repository settings contract)
112145
- Context: [organization authority](../design/organization-authority.md)

organization/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The implemented version-one slice contains:
88

99
- `repositories/<owner>/<repository>.json` — repository declarations;
1010
- `contracts/repository-surfaces/v1.json` — canonical repository surfaces;
11+
- `contracts/repository-settings/v1.json` — the GitHub repository settings
12+
every enrolled repository must match (ADR-0040);
1113
- `contracts/verification-profiles/<id>/v<version>.json` — immutable success-
1214
measure verification contracts;
1315
- `goals/<id>.json` — reviewed organization outcomes that can influence future
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"schema_version": 1,
3+
"contract": {
4+
"id": "repository-settings",
5+
"version": 1
6+
},
7+
"repository": {
8+
"delete_branch_on_merge": true,
9+
"allow_update_branch": true,
10+
"allow_auto_merge": false,
11+
"allow_merge_commit": true,
12+
"allow_squash_merge": true,
13+
"allow_rebase_merge": true,
14+
"merge_commit_title": "MERGE_MESSAGE",
15+
"merge_commit_message": "PR_TITLE",
16+
"squash_merge_commit_title": "PR_TITLE",
17+
"squash_merge_commit_message": "COMMIT_MESSAGES",
18+
"has_wiki": false,
19+
"has_projects": false,
20+
"has_issues": true,
21+
"web_commit_signoff_required": false
22+
},
23+
"actions": {
24+
"default_workflow_permissions": "read",
25+
"can_approve_pull_request_reviews": false
26+
},
27+
"security": {
28+
"vulnerability_alerts": true,
29+
"dependabot_security_updates": true,
30+
"secret_scanning": true,
31+
"secret_scanning_push_protection": true,
32+
"private_vulnerability_reporting": true
33+
},
34+
"default_branch_ruleset": {
35+
"enforcement": "active",
36+
"bypass_actors": "none",
37+
"require_pull_request": true,
38+
"required_approving_review_count": 0,
39+
"require_conversation_resolution": true,
40+
"require_status_checks": true,
41+
"strict_required_status_checks": true,
42+
"block_deletions": true,
43+
"block_force_pushes": true,
44+
"merge_queue": false,
45+
"classic_branch_protection": "absent"
46+
},
47+
"tag_ruleset": {
48+
"pattern": "v*",
49+
"enforcement": "active",
50+
"block_deletions": true,
51+
"block_force_pushes": true,
52+
"restrict_creation": true
53+
},
54+
"metadata": {
55+
"license_required": true,
56+
"description_required": true,
57+
"topics_include": ["frostyard"]
58+
},
59+
"labels": {
60+
"required": ["fluent"]
61+
}
62+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"schema_version": 1,
3+
"contract": {
4+
"id": "repository-settings",
5+
"version": 1
6+
},
7+
"repository": {
8+
"delete_branch_on_merge": true,
9+
"allow_update_branch": true,
10+
"allow_auto_merge": false,
11+
"allow_merge_commit": true,
12+
"allow_squash_merge": true,
13+
"allow_rebase_merge": true,
14+
"merge_commit_title": "MERGE_MESSAGE",
15+
"merge_commit_message": "PR_TITLE",
16+
"squash_merge_commit_title": "PR_TITLE",
17+
"squash_merge_commit_message": "COMMIT_MESSAGES",
18+
"has_wiki": false,
19+
"has_projects": false,
20+
"has_issues": true,
21+
"web_commit_signoff_required": false,
22+
"allow_bots": true
23+
},
24+
"actions": {
25+
"default_workflow_permissions": "read",
26+
"can_approve_pull_request_reviews": false
27+
},
28+
"security": {
29+
"vulnerability_alerts": true,
30+
"dependabot_security_updates": true,
31+
"secret_scanning": true,
32+
"secret_scanning_push_protection": true,
33+
"private_vulnerability_reporting": true
34+
},
35+
"default_branch_ruleset": {
36+
"enforcement": "active",
37+
"bypass_actors": "none",
38+
"require_pull_request": true,
39+
"required_approving_review_count": 0,
40+
"require_conversation_resolution": true,
41+
"require_status_checks": true,
42+
"strict_required_status_checks": true,
43+
"block_deletions": true,
44+
"block_force_pushes": true,
45+
"merge_queue": false,
46+
"classic_branch_protection": "absent"
47+
},
48+
"tag_ruleset": {
49+
"pattern": "v*",
50+
"enforcement": "active",
51+
"block_deletions": true,
52+
"block_force_pushes": true,
53+
"restrict_creation": true
54+
},
55+
"metadata": {
56+
"license_required": true,
57+
"description_required": true,
58+
"topics_include": [
59+
"frostyard"
60+
]
61+
},
62+
"labels": {
63+
"required": [
64+
"fluent"
65+
]
66+
}
67+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"schema_version": 1,
3+
"contract": {
4+
"id": "repository-settings",
5+
"version": 1
6+
},
7+
"repository": {
8+
"delete_branch_on_merge": true,
9+
"allow_update_branch": true,
10+
"allow_auto_merge": false,
11+
"allow_merge_commit": true,
12+
"allow_squash_merge": true,
13+
"allow_rebase_merge": true,
14+
"merge_commit_title": "MERGE_MESSAGE",
15+
"merge_commit_message": "PR_TITLE",
16+
"squash_merge_commit_title": "PR_TITLE",
17+
"squash_merge_commit_message": "COMMIT_MESSAGES",
18+
"has_wiki": false,
19+
"has_projects": false,
20+
"has_issues": true,
21+
"web_commit_signoff_required": false
22+
},
23+
"actions": {
24+
"default_workflow_permissions": "read",
25+
"can_approve_pull_request_reviews": false
26+
},
27+
"security": {
28+
"vulnerability_alerts": true,
29+
"dependabot_security_updates": true,
30+
"secret_scanning": true,
31+
"secret_scanning_push_protection": true,
32+
"private_vulnerability_reporting": true
33+
},
34+
"default_branch_ruleset": {
35+
"enforcement": "active",
36+
"bypass_actors": "admins",
37+
"require_pull_request": true,
38+
"required_approving_review_count": 0,
39+
"require_conversation_resolution": true,
40+
"require_status_checks": true,
41+
"strict_required_status_checks": true,
42+
"block_deletions": true,
43+
"block_force_pushes": true,
44+
"merge_queue": false,
45+
"classic_branch_protection": "absent"
46+
},
47+
"tag_ruleset": {
48+
"pattern": "v*",
49+
"enforcement": "active",
50+
"block_deletions": true,
51+
"block_force_pushes": true,
52+
"restrict_creation": true
53+
},
54+
"metadata": {
55+
"license_required": true,
56+
"description_required": true,
57+
"topics_include": [
58+
"frostyard"
59+
]
60+
},
61+
"labels": {
62+
"required": [
63+
"fluent"
64+
]
65+
}
66+
}

0 commit comments

Comments
 (0)