|
| 1 | +--- |
| 2 | +title: Repository Type Property |
| 3 | +description: How a single "Type" custom property classifies every repository in an initiative organization and drives which org-wide controls apply to it. |
| 4 | +--- |
| 5 | + |
| 6 | +# Repository Type Property |
| 7 | + |
| 8 | +[Organization Standard](Organization-Standard.md) requires every initiative to define |
| 9 | +"repository types used by the initiative" and "required custom properties, labels, branch |
| 10 | +protection, and review rules." This page is the concrete mechanism that satisfies both at |
| 11 | +once: a single GitHub organization **custom property named `Type`**, whose value per |
| 12 | +repository determines which org-wide rulesets and controls apply. |
| 13 | + |
| 14 | +## The pattern |
| 15 | + |
| 16 | +Each initiative organization defines: |
| 17 | + |
| 18 | +1. One `single_select` custom property named `Type`, required on every repository, with a |
| 19 | + default value (typically `Other`). |
| 20 | +2. An allowed-values list specific to that organization's actual repository shapes (a docs |
| 21 | + org and a module-publishing org will not need the same list). |
| 22 | +3. Org-wide rulesets (branch protection, required reviews, and similar controls) that |
| 23 | + target repositories by their `Type` value instead of by repository name. |
| 24 | + |
| 25 | +Setting a repository's `Type` is then the single action that determines every `Type`-scoped |
| 26 | +control it inherits — no per-repository ruleset edits, no repository-name lists to keep in |
| 27 | +sync by hand. |
| 28 | + |
| 29 | +## Filter by exclusion, not by inclusion |
| 30 | + |
| 31 | +When a ruleset condition targets `Type`, write it as an **exclude** list of the `Type` |
| 32 | +values that should be exempt, with an empty `include` list — not an include allow-list of |
| 33 | +the values that should be covered. |
| 34 | + |
| 35 | +```json |
| 36 | +"repository_property": { |
| 37 | + "include": [], |
| 38 | + "exclude": [{ "name": "Type", "source": "custom", "property_values": ["Memory"] }] |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +An empty `include` array matches every repository; the `exclude` array then subtracts |
| 43 | +specific `Type` values. This means: |
| 44 | + |
| 45 | +- Adding a brand-new `Type` value in the future (a new repository shape nobody has |
| 46 | + invented yet) is covered by the ruleset automatically, with no ruleset edit required. |
| 47 | +- Only `Type` values an administrator has explicitly named in `exclude` ever lose |
| 48 | + coverage. Nothing falls out silently. |
| 49 | + |
| 50 | +An include allow-list inverts this safety property: any repository whose `Type` is not on |
| 51 | +the list silently loses coverage, including every future `Type` value nobody remembered to |
| 52 | +add. Exclude-based conditions are the only version of this pattern that is safe to extend |
| 53 | +over time. |
| 54 | + |
| 55 | +## Verifying a migration before cutting over |
| 56 | + |
| 57 | +Moving a ruleset from a name-based or single-purpose-property condition onto `Type` is a |
| 58 | +live change to branch protection. Verify it does not silently drop coverage for any |
| 59 | +repository before making it live: |
| 60 | + |
| 61 | +1. Enumerate every repository in the organization and the `Type` (or prior property) value |
| 62 | + it currently has, via `GET /orgs/{org}/properties/values`. |
| 63 | +2. Compute, for the **current** ruleset condition, which repositories are covered. |
| 64 | +3. Create the replacement ruleset under a temporary name so both rulesets can exist side by |
| 65 | + side, then use `GET /repos/{owner}/{repo}/rules/branches/{branch}` per repository to |
| 66 | + compute which repositories the **new** condition actually covers (this is more reliable |
| 67 | + than reasoning about condition JSON by hand, since it reflects GitHub's own evaluation). |
| 68 | +4. Diff the two coverage sets. The only differences should be the `Type` values the |
| 69 | + migration intentionally excludes. Any other difference means the new condition is wrong. |
| 70 | +5. Only after the diff matches expectations: delete the old ruleset, then create the final |
| 71 | + ruleset under its real name, then delete the temporary one. This ordering means both |
| 72 | + rulesets are briefly active together rather than there being a gap with neither active. |
| 73 | + |
| 74 | +## A known API quirk: ruleset `PATCH` may not work |
| 75 | + |
| 76 | +`PATCH` on `/orgs/{org}/rulesets/{id}` has been observed to fail (`404`) for tokens that |
| 77 | +otherwise have full `GET`/`POST`/`DELETE` access to the same endpoint, including on a |
| 78 | +disposable test ruleset created solely to isolate the failure. Treat in-place ruleset edits |
| 79 | +as unreliable and use the delete-old / create-new sequence above instead, even for small |
| 80 | +condition changes. |
| 81 | + |
| 82 | +## Deprecating single-purpose properties |
| 83 | + |
| 84 | +An initiative may have started with a narrow, single-purpose property (for example, a |
| 85 | +`BranchStrategy` property with values like `None` / `GitHub Flow`, used only to gate one |
| 86 | +ruleset). Once a `Type` property exists and a ruleset condition has been migrated onto it |
| 87 | +and verified, retire the narrow property (`DELETE /orgs/{org}/properties/schema/{name}`) |
| 88 | +rather than keeping two overlapping classification properties. `Type` is the one property |
| 89 | +that should answer "what kind of repository is this," and every `Type`-scoped control |
| 90 | +should read from it. |
| 91 | + |
| 92 | +## Worked examples |
| 93 | + |
| 94 | +Both current MSX initiative organizations use this pattern: |
| 95 | + |
| 96 | +| Organization | `Type` allowed values | Notes | |
| 97 | +| --- | --- | --- | |
| 98 | +| `MSXOrg` | `Docs`, `Memory`, `VSCodeExtension`, `Other` | Introduced from scratch, replacing a prior single-purpose `BranchStrategy` property. | |
| 99 | +| `PSModule` | `Action`, `Archive`, `Docs`, `Framework`, `FunctionApp`, `Memory`, `Module`, `Other`, `Template`, `Workflow` | `Memory` added to an existing, already-populated `Type` property; the ruleset condition changed from a repository-name allow-list (`~ALL`) to a `Type`-based exclude. | |
| 100 | + |
| 101 | +In both organizations, repositories with `Type: Memory` — the |
| 102 | +[Memory Repository Template](../Frameworks/Agentic-Development/memory-template.md)'s |
| 103 | +no-PR, direct-commit-to-`main` repositories — are excluded from the org-wide pull-request- |
| 104 | +required ruleset. That template's workflow only works because the ruleset stops matching |
| 105 | +`Memory`-typed repositories; without this, direct pushes to a memory repository's `main` |
| 106 | +are rejected the same as on any other repository. |
| 107 | + |
| 108 | +## Where this connects |
| 109 | + |
| 110 | +- [Organization Standard](Organization-Standard.md) — the requirement this property |
| 111 | + implements: documented repository types and the custom properties, rulesets, and review |
| 112 | + rules attached to them. |
| 113 | +- [Repository Standard](Repository-Standard.md) — the mandatory/type-specific/ |
| 114 | + repository-specific file-set distinction that `Type` also drives over time. |
| 115 | +- [Memory Repository Template](../Frameworks/Agentic-Development/memory-template.md) — the |
| 116 | + concrete repository type whose no-PR workflow motivated excluding `Type: Memory` from the |
| 117 | + pull-request-required ruleset in both current organizations. |
0 commit comments