feat(custom_properties): support include/exclude to preserve unmanaged property values - #1036
Draft
jordonpeterson wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends the custom_properties plugin to support an { include, exclude } configuration shape, allowing safe-settings to manage only declared properties while preserving values owned by other automation via exclude regexes. It also updates schemas, docs, and unit tests to cover the new behavior while keeping the existing plain-array config shape unchanged.
Changes:
- Add
{ include, exclude }config-shape support tocustom_properties, including exclude-regex compilation and delete short-circuiting. - Update JSON schemas (and dereferenced copies) to accept both the legacy array shape and the new object shape.
- Document the ownership/clearing caveat and add unit tests for include/exclude behavior, casing, nop-mode, and invalid regex handling.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/plugins/custom_properties.js | Implements include/exclude shape detection, exclude regex compilation, and protected remove behavior. |
| test/unit/lib/plugins/custom_properties.test.js | Adds unit tests validating new include/exclude behavior and regressions for the legacy array shape. |
| schema/repos.json | Updates repo override schema to accept both array and object forms for custom_properties. |
| schema/settings.json | Updates org-level schema to accept both array and object forms for custom_properties. |
| schema/suborgs.json | Updates suborg-level schema to accept both array and object forms for custom_properties. |
| schema/dereferenced/repos.json | Mirrors the schema change in the dereferenced repo schema. |
| schema/dereferenced/settings.json | Mirrors the schema change in the dereferenced settings schema. |
| schema/dereferenced/suborgs.json | Mirrors the schema change in the dereferenced suborg schema. |
| docs/sample-settings/settings.yml | Documents the new object form and the “clearing sets value to null” ownership caveat. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+27
to
+37
| let include = entries | ||
| let exclude = [] | ||
|
|
||
| if (isExcludeAwareConfig(entries)) { | ||
| include = Array.isArray(entries.include) ? entries.include : [] | ||
| exclude = Array.isArray(entries.exclude) ? entries.exclude : [] | ||
| } | ||
|
|
||
| super(nop, github, repo, include, log, errors) | ||
|
|
||
| this.exclude = this.compileExcludePatterns(exclude) |
| }, | ||
| { | ||
| "type": "object", | ||
| "properties": { |
| }, | ||
| { | ||
| "type": "object", | ||
| "properties": { |
| }, | ||
| { | ||
| "type": "object", | ||
| "properties": { |
| }, | ||
| { | ||
| "type": "object", | ||
| "properties": { |
| }, | ||
| { | ||
| "type": "object", | ||
| "properties": { |
| }, | ||
| { | ||
| "type": "object", | ||
| "properties": { |
jordonpeterson
marked this pull request as draft
July 27, 2026 19:04
jordonpeterson
force-pushed
the
feat/custom-properties-exclude
branch
from
July 27, 2026 19:28
ec6a24d to
5da417f
Compare
…d property values Custom properties cannot be deleted through the API, so Diffable's removal path sets `value: null`. Any property present on a repository but absent from config was therefore cleared on every sync, including values written by other automation. Safe-settings effectively claimed exclusive ownership of the whole custom-properties surface on any repo with a `custom_properties` section. Adds the `include`/`exclude` config shape already used by the Labels plugin (github-community-projects#408). Properties matching an `exclude` regex are never cleared; properties listed in `include` are enforced exactly as before, even if they also match an exclude pattern. The plain-array config shape is unchanged. Both config-error paths are designed to fail safe, because the cost of misreading an exclude pattern is cleared property values: * Exclude patterns are lowercased when compiled. Property names are lowercased before matching, so an uppercase pattern would otherwise be a valid regex that silently matches nothing and clears the properties it was written to protect. * An unparseable pattern is recorded as a per-repo config error and fails closed - every property on that repo is treated as excluded, so a typo protects values rather than clearing them. This matters because these are regexes, not globs: the intuitive `*` is not a valid pattern. Properties in `include` are still applied. The error is not thrown, because child plugins are constructed outside any try/catch in Settings.updateRepos, so a throw there rejects the org-wide Promise.all and aborts the sync for every other repo. * The object form requires `include`, `exclude`, or both. A config that is neither the array shape nor the include/exclude shape (`custom_properties: {}`, say) is reported and also fails closed. The schemas reject that shape too, but they are authoring aids rather than a runtime gate - nothing in lib/ validates config against them - so the plugin does not rely on them. Without this the empty object reached `normalizeEntries` and threw a TypeError out of the constructor, which is the org-wide abort described above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jordonpeterson
force-pushed
the
feat/custom-properties-exclude
branch
from
July 27, 2026 19:57
5da417f to
224131c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets a
custom_propertiesconfig declare the properties it manages and leaveproperties owned by other automation untouched, instead of clearing every property
it does not list.
Closes #986 — implements the
include/excludeshape requested there, mirroringthe Labels plugin as the issue suggests.
What
To manage only what you declare and leave every other property alone, exclude
everything with
.*:The existing plain-array shape is unchanged.
Why
Custom properties cannot be deleted through the API, so
Diffable's removal pathsets
value: null. Any property present on a repository but absent from config wastherefore cleared on every sync — including values written by other automation. In
effect safe-settings claimed exclusive ownership of the entire custom-properties
surface of any repo with a
custom_propertiessection, with no way to opt out shortof dropping the section altogether. That is exactly the case #986 describes:
org-wide properties should be enforced while per-repo ones are left alone.
Behavior
excluderegex is never cleared.includeis created/updated as before, even if it also matches anexcludepattern.includewins.excludeon its own still means safe-settings manages this repo's properties —everything not matching a pattern is cleared. To manage nothing, omit the section.
Config mistakes fail closed rather than clearing values — see the design notes below.
Changes
lib/plugins/custom_properties.js— config-shape detection, exclude-patterncompilation, exclude check in
remove()test/unit/lib/plugins/custom_properties.test.js— 22 new testsdocs/sample-settings/settings.yml— worked example and the ownership caveatschema/{repos,settings,suborgs}.json— accept both shapes; require the objectform to carry
include,exclude, or bothschema/dereferenced/*.json— 6 of the 9 changed files are these generatedschemas. Their
custom_propertiesblocks are verified byte-equal tonpm run build:schemaoutput. They are spliced in rather than committed wholesalebecause a full regeneration also pulls in unrelated
repositories/teams/rulesetschurn from the live GitHub API description, which drifts on
main-enterpriseindependently of this change.
Testing
New behavior —
test/unit/lib/plugins/custom_properties.test.jspasses 27/27,covering exclude protection,
include-wins precedence, casing on both the APIresponse and the pattern, nop mode, degenerate configs, invalid regexes, and
malformed object configs.
Existing behavior is provably unchanged. Since this touches a path that nulls out
property values, the old behavior was verified directly rather than by inspection: a
15-scenario matrix (× apply and nop modes = 30 runs) was run against the plugin as it
exists on
main-enterpriseand against this branch, capturing API writes,NopCommands, normalized entries, and recorded errors for each. The two transcriptsare byte-identical. The matrix uses only pre-change config shapes — plain arrays,
null,undefined— and covers update, create, no-op, clearing unmanaged properties,the
property_namekey variant, mixed casing on both sides, nameless and non-objectentries, and multi-property syncs. 11 of the 30 runs perform API writes and 11 emit
NopCommands, so the comparison is exercising real output.The schemas were checked the same way: every config shape that validated against the
schema on
main-enterprisestill validates here — empty array,{name,value},{property_name,value}, multiple entries, name-only, and nocustom_propertieskey.Zero regressions.
npm run test:unit(what CI runs) passes 156 tests across 16 suites.standardisclean on both changed files, and
eslintreports the same 145 pre-existing problemsas the base commit — no new lint.
Design notes: why config errors fail closed, and where this diverges from Labels
The
include/excludevocabulary and the unanchored-regex matching followlib/plugins/labels.js. Two deliberate divergences, both because the cost ofmisreading an exclude pattern here is cleared property values rather than a stray
label:
matching, so an uppercase pattern would otherwise be a valid regex that silently
matches nothing and clears the properties it was written to protect.
every property on that repo is treated as excluded, so a typo protects values
rather than clearing them. This matters because these are regexes, not globs — the
intuitive
*is not a valid pattern, and under a fail-open reading it would clearevery unmanaged property. Properties in
includeare still applied.A malformed object config fails closed the same way. The object form requires
include,exclude, or both; the schemas reject anything else, but the plugin doesnot rely on them — they are authoring aids, not a runtime gate, since nothing in
lib/validates config against them. Without the runtime checkcustom_properties: {}reachednormalizeEntriesand threw aTypeErrorout of theconstructor.
Errors are recorded rather than thrown throughout: child plugins are constructed
outside any try/catch in
Settings.updateRepos, so throwing would reject the org-widePromise.alland abort the sync for every other repo. A bad config in one repo shouldnot take down the org's sync.
Happy to rebase if #1032 lands first — it touches the same plugin.