|
1 | 1 | const Diffable = require('./diffable') |
2 | 2 | const NopCommand = require('../nopcommand') |
3 | 3 |
|
| 4 | +// Detects the `{ include: [...], exclude: [...] }` config shape. Either key on |
| 5 | +// its own is enough; a missing `include` is treated as an empty include list. |
| 6 | +function isExcludeAwareConfig (entries) { |
| 7 | + return !!entries && |
| 8 | + typeof entries === 'object' && |
| 9 | + !Array.isArray(entries) && |
| 10 | + (Array.isArray(entries.include) || Array.isArray(entries.exclude)) |
| 11 | +} |
| 12 | + |
4 | 13 | module.exports = class CustomProperties extends Diffable { |
5 | | - constructor (...args) { |
6 | | - super(...args) |
| 14 | + constructor (nop, github, repo, entries, log, errors) { |
| 15 | + // Two config shapes are supported: |
| 16 | + // |
| 17 | + // custom_properties: | custom_properties: |
| 18 | + // - name: jira-team | include: |
| 19 | + // value: Platform | - name: jira-team |
| 20 | + // | value: Platform |
| 21 | + // | exclude: |
| 22 | + // | - name: ^app- |
| 23 | + // |
| 24 | + // The object shape lets a config declare the properties it manages |
| 25 | + // (`include`) while protecting properties owned by other automation |
| 26 | + // (`exclude`) from being nulled out. The plain array shape is unchanged. |
| 27 | + let include = entries |
| 28 | + let exclude = [] |
| 29 | + |
| 30 | + if (isExcludeAwareConfig(entries)) { |
| 31 | + include = Array.isArray(entries.include) ? entries.include : [] |
| 32 | + exclude = Array.isArray(entries.exclude) ? entries.exclude : [] |
| 33 | + } |
| 34 | + |
| 35 | + super(nop, github, repo, include, log, errors) |
| 36 | + |
| 37 | + const { patterns, excludeAll } = this.compileExcludePatterns(exclude) |
| 38 | + this.exclude = patterns |
| 39 | + this.excludeAll = excludeAll |
7 | 40 |
|
8 | 41 | if (this.entries) { |
9 | 42 | this.normalizeEntries() |
10 | 43 | } |
11 | 44 | } |
12 | 45 |
|
| 46 | + // Compile the `exclude[].name` regex strings, skipping entries without a |
| 47 | + // string name. An unparseable pattern is recorded as a config error for this |
| 48 | + // repo rather than thrown: child plugins are constructed outside of any |
| 49 | + // try/catch in `Settings.updateRepos`, so throwing here would reject the |
| 50 | + // org-wide `Promise.all` and abort the sync for every other repo too. |
| 51 | + // |
| 52 | + // An invalid pattern fails closed - every property on the repo is treated as |
| 53 | + // excluded. A typo in an exclude pattern is a request to protect something, |
| 54 | + // so the safe reading is "protect everything until the config is fixed" |
| 55 | + // rather than "protect nothing", which would clear the very values the |
| 56 | + // pattern was written to defend. Properties in `include` are still enforced. |
| 57 | + compileExcludePatterns (exclude) { |
| 58 | + return exclude.reduce((state, item) => { |
| 59 | + if (!item || typeof item.name !== 'string') { |
| 60 | + return state |
| 61 | + } |
| 62 | + |
| 63 | + try { |
| 64 | + // Property names are lowercased before matching, so lowercase the |
| 65 | + // pattern too - an uppercase pattern would otherwise be a valid regex |
| 66 | + // that silently matches nothing and clears the properties it names. |
| 67 | + state.patterns.push(new RegExp(item.name.toLowerCase())) |
| 68 | + } catch (e) { |
| 69 | + this.logError(`Invalid custom property exclude pattern "${item.name}": ${e.message || e}. Excluding all custom properties for this repo so no values are cleared.`) |
| 70 | + state.excludeAll = true |
| 71 | + } |
| 72 | + |
| 73 | + return state |
| 74 | + }, { patterns: [], excludeAll: false }) |
| 75 | + } |
| 76 | + |
| 77 | + // Exclude patterns are matched against the normalized (lowercased) property |
| 78 | + // name. Patterns are lowercased when compiled, so config casing does not |
| 79 | + // matter. Note these are regexes, not globs: `.*` matches everything, `*` is |
| 80 | + // not a valid pattern. |
| 81 | + isExcluded (name) { |
| 82 | + if (this.excludeAll) { |
| 83 | + return true |
| 84 | + } |
| 85 | + |
| 86 | + return typeof name === 'string' && this.exclude.some(rx => rx.test(name)) |
| 87 | + } |
| 88 | + |
13 | 89 | // Force all names to lowercase to avoid comparison issues. |
14 | 90 | normalizeEntries () { |
15 | 91 | this.entries = this.entries.reduce((normalizedEntries, entry) => { |
@@ -90,6 +166,10 @@ module.exports = class CustomProperties extends Diffable { |
90 | 166 |
|
91 | 167 | // Custom Properties on repository does not support deletion, so we set the value to null |
92 | 168 | async remove ({ name }) { |
| 169 | + if (this.isExcluded(name)) { |
| 170 | + this.log.debug(`Custom Property "${name}" matches an exclude pattern; leaving its value untouched`) |
| 171 | + return Promise.resolve([]) |
| 172 | + } |
93 | 173 | return this.modifyProperty('Delete', { name, value: null }) |
94 | 174 | } |
95 | 175 |
|
|
0 commit comments