@cat-factory/app 0.225.0 → 0.227.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/board/StepExecutionHistory.vue +12 -16
- package/app/components/panels/MergerResultView.vue +7 -0
- package/app/components/riskPolicy/RiskPolicyPicker.logic.spec.ts +61 -1
- package/app/components/riskPolicy/RiskPolicyPicker.logic.ts +40 -0
- package/app/components/riskPolicy/RiskPolicyPicker.vue +68 -7
- package/app/components/riskPolicy/RiskPolicyPreview.vue +6 -2
- package/app/components/settings/MergeRolePolicyEditor.logic.spec.ts +65 -2
- package/app/components/settings/MergeRolePolicyEditor.logic.ts +72 -0
- package/app/components/settings/MergeRolePolicyEditor.vue +133 -39
- package/app/components/settings/RiskPolicyPanel.vue +8 -0
- package/app/composables/usePipelineErrorToast.ts +4 -0
- package/app/types/merge.ts +2 -0
- package/app/utils/riskPolicy.spec.ts +21 -2
- package/app/utils/riskPolicy.ts +12 -0
- package/i18n/locales/de.json +22 -7
- package/i18n/locales/en.json +25 -7
- package/i18n/locales/es.json +22 -7
- package/i18n/locales/fr.json +22 -7
- package/i18n/locales/he.json +22 -7
- package/i18n/locales/it.json +22 -7
- package/i18n/locales/ja.json +22 -7
- package/i18n/locales/pl.json +22 -7
- package/i18n/locales/tr.json +22 -7
- package/i18n/locales/uk.json +22 -7
- package/package.json +4 -4
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// The ROLE layer of one merge preset: what happens to a run depending on WHO started it.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// be held to stricter per-class rules than the preset's base map (`classRulesByRole`),
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Three settings, edited together because they answer the same question at three strengths. A role
|
|
5
|
+
// can be held to stricter per-class rules than the preset's base map (`classRulesByRole`), held to
|
|
6
|
+
// an allowlist of the change classes it may LAND at all (`submissionClassesByRole`), or held to dry
|
|
7
|
+
// runs entirely (`dryRunRoles`): the pipeline works and opens its pull request, and nothing merges.
|
|
8
|
+
// The strongest of the three that applies is what governs, which is why a sandboxed role says so on
|
|
9
|
+
// its row rather than leaving the two below it reading as the policy.
|
|
9
10
|
//
|
|
10
11
|
// Composition is NARROW-ONLY, so this editor only offers a role rules STRICTER than the base one
|
|
11
12
|
// (see MergeRolePolicyEditor.logic.ts). A looser rule is discarded by the engine, and an editor
|
|
@@ -18,14 +19,19 @@ import type {
|
|
|
18
19
|
MergeClassRule,
|
|
19
20
|
MergeClassRules,
|
|
20
21
|
RuleableChangeClass,
|
|
22
|
+
SubmissionClassesByRole,
|
|
21
23
|
WorkspaceRole,
|
|
22
24
|
} from '~/types/merge'
|
|
23
25
|
import {
|
|
24
26
|
INHERIT_RULE,
|
|
25
27
|
roleClassRuleRows,
|
|
26
28
|
roleNarrowedCount,
|
|
29
|
+
roleSubmissionRows,
|
|
30
|
+
roleSubmissionScoped,
|
|
27
31
|
setRoleClassRule,
|
|
32
|
+
setRoleSubmissionScoped,
|
|
28
33
|
toggleDryRunRole,
|
|
34
|
+
toggleSubmissionClass,
|
|
29
35
|
type RoleRuleSelection,
|
|
30
36
|
} from '~/components/settings/MergeRolePolicyEditor.logic'
|
|
31
37
|
|
|
@@ -34,6 +40,12 @@ const props = defineProps<{
|
|
|
34
40
|
classRulesByRole: ClassRulesByRole
|
|
35
41
|
/** The roles whose runs this preset sandboxes. */
|
|
36
42
|
dryRunRoles: DryRunRoles
|
|
43
|
+
/**
|
|
44
|
+
* Which change classes each role may LAND. A role with no entry is unrestricted; an entry with
|
|
45
|
+
* no classes lands nothing. The editor keeps those two apart (a switch, then tick boxes),
|
|
46
|
+
* because tick boxes alone would render them identically.
|
|
47
|
+
*/
|
|
48
|
+
submissionClassesByRole: SubmissionClassesByRole
|
|
37
49
|
/** The base rules the role layer narrows, so each row can show what it inherits. */
|
|
38
50
|
classRules: MergeClassRules
|
|
39
51
|
/**
|
|
@@ -50,6 +62,7 @@ const props = defineProps<{
|
|
|
50
62
|
const emit = defineEmits<{
|
|
51
63
|
'update:classRulesByRole': [ClassRulesByRole]
|
|
52
64
|
'update:dryRunRoles': [DryRunRoles]
|
|
65
|
+
'update:submissionClassesByRole': [SubmissionClassesByRole]
|
|
53
66
|
}>()
|
|
54
67
|
|
|
55
68
|
const { t } = useI18n()
|
|
@@ -92,6 +105,14 @@ const roles = computed(() =>
|
|
|
92
105
|
sandboxed: props.dryRunRoles.includes(role),
|
|
93
106
|
narrowed,
|
|
94
107
|
open: !!expanded.value[role],
|
|
108
|
+
// Scoped-ness is read off the MAP rather than off the entry being truthy, so an empty
|
|
109
|
+
// allowlist (a real policy: this role lands nothing) stays scoped instead of reading as
|
|
110
|
+
// the unrestricted default.
|
|
111
|
+
submissionScoped: roleSubmissionScoped(props.submissionClassesByRole, role),
|
|
112
|
+
submissionRows: roleSubmissionRows(props.submissionClassesByRole[role] ?? []).map((row) => ({
|
|
113
|
+
...row,
|
|
114
|
+
label: t(CLASS_LABEL_KEYS[row.changeClass]),
|
|
115
|
+
})),
|
|
95
116
|
rows: roleClassRuleRows(props.classRules, entry).map((row) => ({
|
|
96
117
|
...row,
|
|
97
118
|
label: t(CLASS_LABEL_KEYS[row.changeClass]),
|
|
@@ -118,6 +139,24 @@ function setSandboxed(role: WorkspaceRole, sandboxed: boolean) {
|
|
|
118
139
|
function setRule(role: WorkspaceRole, changeClass: RuleableChangeClass, rule: RoleRuleSelection) {
|
|
119
140
|
emit('update:classRulesByRole', setRoleClassRule(props.classRulesByRole, role, changeClass, rule))
|
|
120
141
|
}
|
|
142
|
+
|
|
143
|
+
function setSubmissionScoped(role: WorkspaceRole, scoped: boolean) {
|
|
144
|
+
emit(
|
|
145
|
+
'update:submissionClassesByRole',
|
|
146
|
+
setRoleSubmissionScoped(props.submissionClassesByRole, role, scoped),
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function setSubmissionClass(
|
|
151
|
+
role: WorkspaceRole,
|
|
152
|
+
changeClass: RuleableChangeClass,
|
|
153
|
+
allowed: boolean,
|
|
154
|
+
) {
|
|
155
|
+
emit(
|
|
156
|
+
'update:submissionClassesByRole',
|
|
157
|
+
toggleSubmissionClass(props.submissionClassesByRole, role, changeClass, allowed),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
121
160
|
</script>
|
|
122
161
|
|
|
123
162
|
<template>
|
|
@@ -129,8 +168,9 @@ function setRule(role: WorkspaceRole, changeClass: RuleableChangeClass, rule: Ro
|
|
|
129
168
|
<p class="mt-0.5 text-[11px] leading-snug text-slate-500">
|
|
130
169
|
{{ t('settings.riskPolicy.roleRules.help') }}
|
|
131
170
|
</p>
|
|
132
|
-
<!-- Auto-merge off already sends every pull request to a human, so
|
|
133
|
-
|
|
171
|
+
<!-- Auto-merge off already sends every pull request to a human, so the per-class narrowing
|
|
172
|
+
has nothing left to subtract. The other two settings still bite, because both refuse the
|
|
173
|
+
MANUAL merge as well, which is the one thing the master switch leaves open. -->
|
|
134
174
|
<p
|
|
135
175
|
v-if="!autoMergeEnabled"
|
|
136
176
|
class="mt-1 text-[11px] leading-snug text-amber-400/90"
|
|
@@ -174,11 +214,20 @@ function setRule(role: WorkspaceRole, changeClass: RuleableChangeClass, rule: Ro
|
|
|
174
214
|
)
|
|
175
215
|
}}
|
|
176
216
|
</UButton>
|
|
217
|
+
<!-- A collapsed group must not hide an active policy: the tally above counts class rules
|
|
218
|
+
only, and a role can land nothing at all while showing "No class limits". -->
|
|
219
|
+
<span
|
|
220
|
+
v-if="group.submissionScoped"
|
|
221
|
+
class="text-[11px] text-amber-400/90"
|
|
222
|
+
:data-testid="`merge-role-submission-badge-${group.role}`"
|
|
223
|
+
>
|
|
224
|
+
{{ t('settings.riskPolicy.roleRules.submissionBadge') }}
|
|
225
|
+
</span>
|
|
177
226
|
</div>
|
|
178
227
|
|
|
179
|
-
<!-- A sandboxed role merges nothing at all, so
|
|
180
|
-
|
|
181
|
-
|
|
228
|
+
<!-- A sandboxed role merges nothing at all, so neither limit below can make its runs any
|
|
229
|
+
stricter. Both stay editable (removing the sandbox brings them straight back) and the
|
|
230
|
+
row says which of the three is actually governing. -->
|
|
182
231
|
<p
|
|
183
232
|
v-if="group.sandboxed"
|
|
184
233
|
class="mt-1 text-[11px] leading-snug text-amber-400/90"
|
|
@@ -187,38 +236,83 @@ function setRule(role: WorkspaceRole, changeClass: RuleableChangeClass, rule: Ro
|
|
|
187
236
|
{{ t('settings.riskPolicy.roleRules.sandboxNote') }}
|
|
188
237
|
</p>
|
|
189
238
|
|
|
190
|
-
<div v-if="group.open" class="mt-2 space-y-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<div
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
class="flex flex-wrap items-center gap-2"
|
|
198
|
-
:data-testid="`merge-role-row-${group.role}-${row.changeClass}`"
|
|
199
|
-
>
|
|
200
|
-
<span class="min-w-[7rem] text-xs text-slate-400">{{ row.label }}</span>
|
|
201
|
-
<USelect
|
|
202
|
-
:model-value="row.selected"
|
|
203
|
-
:items="row.items"
|
|
204
|
-
value-key="value"
|
|
239
|
+
<div v-if="group.open" class="mt-2 space-y-2 border-t border-slate-800 pt-2">
|
|
240
|
+
<!-- What this role may LAND at all. Above the class rules because it outranks them: a
|
|
241
|
+
class can be auto-mergeable under the rules and still outside this list, and then
|
|
242
|
+
nothing merges, through the review card's own button included. -->
|
|
243
|
+
<div class="space-y-1.5">
|
|
244
|
+
<USwitch
|
|
245
|
+
:model-value="group.submissionScoped"
|
|
205
246
|
size="sm"
|
|
206
|
-
|
|
207
|
-
:
|
|
208
|
-
:data-testid="`merge-role-
|
|
209
|
-
@update:model-value="
|
|
247
|
+
:disabled="disabled"
|
|
248
|
+
:label="t('settings.riskPolicy.roleRules.submissionLabel')"
|
|
249
|
+
:data-testid="`merge-role-submission-${group.role}`"
|
|
250
|
+
@update:model-value="setSubmissionScoped(group.role, $event)"
|
|
210
251
|
/>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
252
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
253
|
+
{{ t('settings.riskPolicy.roleRules.submissionHelp') }}
|
|
254
|
+
</p>
|
|
255
|
+
<div v-if="group.submissionScoped" class="flex flex-wrap gap-x-4 gap-y-1">
|
|
256
|
+
<UCheckbox
|
|
257
|
+
v-for="row in group.submissionRows"
|
|
258
|
+
:key="row.changeClass"
|
|
259
|
+
:model-value="row.allowed"
|
|
260
|
+
size="sm"
|
|
261
|
+
:disabled="disabled"
|
|
262
|
+
:label="row.label"
|
|
263
|
+
:data-testid="`merge-role-submission-class-${group.role}-${row.changeClass}`"
|
|
264
|
+
@update:model-value="setSubmissionClass(group.role, row.changeClass, $event === true)"
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
<!-- An empty allowlist is a real policy, not an unfinished edit, so it says what it
|
|
268
|
+
does rather than reading as a switch somebody forgot to fill in. -->
|
|
269
|
+
<p
|
|
270
|
+
v-if="group.submissionScoped && !group.submissionRows.some((r) => r.allowed)"
|
|
271
|
+
class="text-[11px] leading-snug text-amber-400/90"
|
|
272
|
+
:data-testid="`merge-role-submission-none-${group.role}`"
|
|
273
|
+
>
|
|
274
|
+
{{ t('settings.riskPolicy.roleRules.submissionNone') }}
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div class="space-y-1.5 border-t border-slate-800/70 pt-2">
|
|
279
|
+
<p class="text-[11px] leading-snug text-slate-500">
|
|
280
|
+
{{ t('settings.riskPolicy.roleRules.classHeading') }}
|
|
281
|
+
</p>
|
|
282
|
+
<p v-if="!anyBaseRule" class="text-[11px] leading-snug text-slate-500">
|
|
283
|
+
{{ t('settings.riskPolicy.roleRules.baseHint') }}
|
|
284
|
+
</p>
|
|
285
|
+
<div
|
|
286
|
+
v-for="row in group.rows"
|
|
287
|
+
:key="row.changeClass"
|
|
288
|
+
class="flex flex-wrap items-center gap-2"
|
|
289
|
+
:data-testid="`merge-role-row-${group.role}-${row.changeClass}`"
|
|
219
290
|
>
|
|
220
|
-
{{
|
|
221
|
-
|
|
291
|
+
<span class="min-w-[7rem] text-xs text-slate-400">{{ row.label }}</span>
|
|
292
|
+
<USelect
|
|
293
|
+
:model-value="row.selected"
|
|
294
|
+
:items="row.items"
|
|
295
|
+
value-key="value"
|
|
296
|
+
size="sm"
|
|
297
|
+
class="w-52"
|
|
298
|
+
:disabled="disabled || row.items.length === 1"
|
|
299
|
+
:data-testid="`merge-role-rule-${group.role}-${row.changeClass}`"
|
|
300
|
+
@update:model-value="
|
|
301
|
+
setRule(group.role, row.changeClass, $event as RoleRuleSelection)
|
|
302
|
+
"
|
|
303
|
+
/>
|
|
304
|
+
<!-- Nothing left to narrow: the base rule already routes this class to a human. -->
|
|
305
|
+
<span v-if="row.items.length === 1" class="text-[11px] text-slate-500">
|
|
306
|
+
{{ t('settings.riskPolicy.roleRules.alreadyStrictest') }}
|
|
307
|
+
</span>
|
|
308
|
+
<span
|
|
309
|
+
v-else-if="row.redundant"
|
|
310
|
+
class="text-[11px] text-amber-400/90"
|
|
311
|
+
:data-testid="`merge-role-redundant-${group.role}-${row.changeClass}`"
|
|
312
|
+
>
|
|
313
|
+
{{ t('settings.riskPolicy.roleRules.redundant') }}
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
222
316
|
</div>
|
|
223
317
|
</div>
|
|
224
318
|
</div>
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
MergeClassRules,
|
|
12
12
|
RiskPolicy,
|
|
13
13
|
RequirementConcernLevel,
|
|
14
|
+
SubmissionClassesByRole,
|
|
14
15
|
} from '~/types/merge'
|
|
15
16
|
import type { StepGating } from '@cat-factory/contracts'
|
|
16
17
|
import {
|
|
@@ -90,6 +91,9 @@ interface Draft {
|
|
|
90
91
|
// which is why clearing one in the editor is a plain omission.
|
|
91
92
|
classRulesByRole: ClassRulesByRole
|
|
92
93
|
dryRunRoles: DryRunRoles
|
|
94
|
+
// Which classes each role may LAND at all. A role with no entry is unrestricted, so `{}` is the
|
|
95
|
+
// identity; an entry with no classes is the different policy that the role lands nothing.
|
|
96
|
+
submissionClassesByRole: SubmissionClassesByRole
|
|
93
97
|
// Implementation-fork decision gating (edited 0..100, stored 0..1); disabled ⇒ off in `auto`.
|
|
94
98
|
forkEnabled: boolean
|
|
95
99
|
forkMinComplexity: number
|
|
@@ -129,6 +133,7 @@ function toDraft(p: RiskPolicy): Draft {
|
|
|
129
133
|
classRules: { ...p.classRules },
|
|
130
134
|
classRulesByRole: { ...p.classRulesByRole },
|
|
131
135
|
dryRunRoles: [...p.dryRunRoles],
|
|
136
|
+
submissionClassesByRole: { ...p.submissionClassesByRole },
|
|
132
137
|
forkEnabled: p.forkDecision?.enabled ?? false,
|
|
133
138
|
forkMinComplexity: Math.round((p.forkDecision?.minComplexity ?? 0.5) * 100),
|
|
134
139
|
forkMinRisk: Math.round((p.forkDecision?.minRisk ?? 0.4) * 100),
|
|
@@ -174,6 +179,7 @@ async function save(p: RiskPolicy) {
|
|
|
174
179
|
classRules: d.classRules,
|
|
175
180
|
classRulesByRole: d.classRulesByRole,
|
|
176
181
|
dryRunRoles: d.dryRunRoles,
|
|
182
|
+
submissionClassesByRole: d.submissionClassesByRole,
|
|
177
183
|
forkDecision: forkGating(d),
|
|
178
184
|
})
|
|
179
185
|
toast.add({
|
|
@@ -235,6 +241,7 @@ const draft = reactive<Draft>({
|
|
|
235
241
|
classRules: {},
|
|
236
242
|
classRulesByRole: {},
|
|
237
243
|
dryRunRoles: [],
|
|
244
|
+
submissionClassesByRole: {},
|
|
238
245
|
forkEnabled: false,
|
|
239
246
|
forkMinComplexity: 50,
|
|
240
247
|
forkMinRisk: 40,
|
|
@@ -395,6 +402,7 @@ async function create() {
|
|
|
395
402
|
<MergeRolePolicyEditor
|
|
396
403
|
v-model:class-rules-by-role="drafts[p.id]!.classRulesByRole"
|
|
397
404
|
v-model:dry-run-roles="drafts[p.id]!.dryRunRoles"
|
|
405
|
+
v-model:submission-classes-by-role="drafts[p.id]!.submissionClassesByRole"
|
|
398
406
|
:class-rules="drafts[p.id]!.classRules"
|
|
399
407
|
:auto-merge-enabled="drafts[p.id]!.autoMergeEnabled"
|
|
400
408
|
:disabled="busy === p.id"
|
|
@@ -117,6 +117,10 @@ const CONFLICT_INFO: Record<Exclude<ConflictReason, BespokeConflictReason>, Conf
|
|
|
117
117
|
titleKey: 'errors.conflict.title.dry_run_not_mergeable',
|
|
118
118
|
descriptionKey: 'errors.conflict.description.dry_run_not_mergeable',
|
|
119
119
|
},
|
|
120
|
+
submission_not_allowed: {
|
|
121
|
+
titleKey: 'errors.conflict.title.submission_not_allowed',
|
|
122
|
+
descriptionKey: 'errors.conflict.description.submission_not_allowed',
|
|
123
|
+
},
|
|
120
124
|
github_not_connected: {
|
|
121
125
|
titleKey: 'errors.conflict.title.github_not_connected',
|
|
122
126
|
descriptionKey: 'errors.conflict.description.github_not_connected',
|
package/app/types/merge.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type {
|
|
|
16
16
|
// runs are sandboxed (they open a pull request and merge nothing).
|
|
17
17
|
ClassRulesByRole,
|
|
18
18
|
DryRunRoles,
|
|
19
|
+
// Which change classes a role may LAND at all (absent ⇒ unrestricted, empty ⇒ nothing).
|
|
20
|
+
SubmissionClassesByRole,
|
|
19
21
|
WorkspaceRole,
|
|
20
22
|
MergeTrackRecord,
|
|
21
23
|
ReviewEffort,
|
|
@@ -27,6 +27,7 @@ const policy = (over: Partial<RiskPolicy> = {}): RiskPolicy =>
|
|
|
27
27
|
classRules: {},
|
|
28
28
|
classRulesByRole: {},
|
|
29
29
|
dryRunRoles: [],
|
|
30
|
+
submissionClassesByRole: {},
|
|
30
31
|
isDefault: true,
|
|
31
32
|
version: 1,
|
|
32
33
|
createdAt: 0,
|
|
@@ -61,7 +62,7 @@ describe('riskPolicyCeilings', () => {
|
|
|
61
62
|
|
|
62
63
|
describe('rolePolicySummary', () => {
|
|
63
64
|
it('is empty on a policy that treats every initiator alike', () => {
|
|
64
|
-
expect(rolePolicySummary(policy())).toEqual({ sandboxed: [], narrowed: [] })
|
|
65
|
+
expect(rolePolicySummary(policy())).toEqual({ sandboxed: [], narrowed: [], scoped: [] })
|
|
65
66
|
})
|
|
66
67
|
|
|
67
68
|
it('lists both layers in the shared role order', () => {
|
|
@@ -72,6 +73,7 @@ describe('rolePolicySummary', () => {
|
|
|
72
73
|
expect(rolePolicySummary(p)).toEqual({
|
|
73
74
|
sandboxed: ['member', 'viewer'],
|
|
74
75
|
narrowed: ['admin'],
|
|
76
|
+
scoped: [],
|
|
75
77
|
})
|
|
76
78
|
})
|
|
77
79
|
|
|
@@ -79,6 +81,23 @@ describe('rolePolicySummary', () => {
|
|
|
79
81
|
// limit that changes nothing about what that role's runs can do.
|
|
80
82
|
it('does not also report class rules for a role the policy sandboxes', () => {
|
|
81
83
|
const p = policy({ dryRunRoles: ['member'], classRulesByRole: { member: { docs: 'never' } } })
|
|
82
|
-
expect(rolePolicySummary(p)).toEqual({ sandboxed: ['member'], narrowed: [] })
|
|
84
|
+
expect(rolePolicySummary(p)).toEqual({ sandboxed: ['member'], narrowed: [], scoped: [] })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('lists a role allowlisted to a subset of change classes', () => {
|
|
88
|
+
const p = policy({ submissionClassesByRole: { member: ['docs'] } })
|
|
89
|
+
expect(rolePolicySummary(p)).toEqual({ sandboxed: [], narrowed: [], scoped: ['member'] })
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// The most restrictive policy this setting can express, and the one a summary must not drop:
|
|
93
|
+
// an EMPTY allowlist is scoped, where an absent entry is unrestricted.
|
|
94
|
+
it('lists a role allowlisted to NOTHING', () => {
|
|
95
|
+
const p = policy({ submissionClassesByRole: { viewer: [] } })
|
|
96
|
+
expect(rolePolicySummary(p).scoped).toEqual(['viewer'])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('does not also report an allowlist for a role the policy sandboxes', () => {
|
|
100
|
+
const p = policy({ dryRunRoles: ['member'], submissionClassesByRole: { member: ['docs'] } })
|
|
101
|
+
expect(rolePolicySummary(p)).toEqual({ sandboxed: ['member'], narrowed: [], scoped: [] })
|
|
83
102
|
})
|
|
84
103
|
})
|
package/app/utils/riskPolicy.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface RolePolicySummary {
|
|
|
52
52
|
* both would read as two limits where the second changes nothing.
|
|
53
53
|
*/
|
|
54
54
|
narrowed: WorkspaceRole[]
|
|
55
|
+
/**
|
|
56
|
+
* Roles allowlisted to a subset of change classes they may LAND. Same suppression rule as
|
|
57
|
+
* `narrowed` and for the same reason, and listed SEPARATELY from it because the two are not
|
|
58
|
+
* degrees of one setting: this one bars landing outright, including through the manual merge.
|
|
59
|
+
*/
|
|
60
|
+
scoped: WorkspaceRole[]
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
/**
|
|
@@ -66,5 +72,11 @@ export function rolePolicySummary(p: RiskPolicy): RolePolicySummary {
|
|
|
66
72
|
narrowed: WORKSPACE_ROLES.filter(
|
|
67
73
|
(role) => !sandboxed.includes(role) && Object.keys(p.classRulesByRole[role] ?? {}).length > 0,
|
|
68
74
|
),
|
|
75
|
+
// Scoped-ness is the PRESENCE of an entry, never its length: a role allowlisted to nothing
|
|
76
|
+
// is the most restrictive policy this setting can express, and reading it as "no allowlist"
|
|
77
|
+
// would drop the one summary line a reader most needs to see.
|
|
78
|
+
scoped: WORKSPACE_ROLES.filter(
|
|
79
|
+
(role) => !sandboxed.includes(role) && p.submissionClassesByRole[role] !== undefined,
|
|
80
|
+
),
|
|
69
81
|
}
|
|
70
82
|
}
|
package/i18n/locales/de.json
CHANGED
|
@@ -525,10 +525,15 @@
|
|
|
525
525
|
},
|
|
526
526
|
"roleRules": {
|
|
527
527
|
"heading": "Grenzen je startender Rolle",
|
|
528
|
-
"help": "Was ein Lauf darf, hängt auch davon ab, wer ihn gestartet hat. Eine Rolle kann
|
|
529
|
-
"autoMergeOffWarning": "
|
|
528
|
+
"help": "Was ein Lauf darf, hängt auch davon ab, wer ihn gestartet hat. Eine Rolle kann auf bestimmte Änderungsarten beschränkt, an strengere Regeln als die obigen gebunden oder auf Probeläufe begrenzt werden: Die Pipeline arbeitet und öffnet einen Pull Request, zusammengeführt wird nichts. Eine Rolle kann diese Richtlinie nur einschränken, nie erweitern.",
|
|
529
|
+
"autoMergeOffWarning": "Automatisches Zusammenführen ist für diese Vorlage aus, daher geht ohnehin jeder Pull Request an einen Menschen und die Klassenlimits unten bewirken nichts. Die Merge-Beschränkung und Probeläufe gelten weiterhin: Beide verweigern auch das manuelle Zusammenführen.",
|
|
530
530
|
"sandboxLabel": "Nur Probeläufe",
|
|
531
|
-
"sandboxNote": "Läufe dieser Rolle öffnen einen Pull Request und führen nichts zusammen,
|
|
531
|
+
"sandboxNote": "Läufe dieser Rolle öffnen einen Pull Request und führen nichts zusammen, daher kann keine der Beschränkungen unten sie strenger halten.",
|
|
532
|
+
"submissionLabel": "Einschränken, welche Änderungen diese Rolle zusammenführen darf",
|
|
533
|
+
"submissionHelp": "Wenn aktiv, werden bei Läufen dieser Rolle nur die angehakten Änderungsarten zusammengeführt. Alles andere bleibt für jemand anderen offen, unabhängig von den Bewertungen. Der Pull Request wird trotzdem geöffnet und kann auf dem Git-Host von Hand zusammengeführt werden.",
|
|
534
|
+
"submissionNone": "Nichts ist angehakt, also führt dieser Arbeitsbereich keinen Lauf dieser Rolle zusammen.",
|
|
535
|
+
"submissionBadge": "Zusammenführen beschränkt",
|
|
536
|
+
"classHeading": "Erforderliche Prüfung je Änderungsart",
|
|
532
537
|
"noRules": "Keine Kategoriegrenzen",
|
|
533
538
|
"ruleCount": "{count} Kategorie eingeschränkt | {count} Kategorien eingeschränkt",
|
|
534
539
|
"inherit": "Wie in dieser Richtlinie",
|
|
@@ -1800,6 +1805,7 @@
|
|
|
1800
1805
|
"class_auto_merge": "Diese Vorlage führt diese Änderungskategorie immer automatisch zusammen, daher wurden die Bewertungen nicht verglichen.",
|
|
1801
1806
|
"class_requires_review": "Diese Vorlage gibt diese Änderungskategorie unabhängig von den Bewertungen immer an einen Menschen.",
|
|
1802
1807
|
"role_requires_review": "Diese Vorgabe lässt Läufe, die ein {role} startet, für diese Art von Änderung unabhängig von den Bewertungen immer von einem Menschen prüfen. Ein Teammitglied mit höherer Berechtigung kann diesen PR unverändert zusammenführen.",
|
|
1808
|
+
"submission_not_allowed": "Diese Vorlage lässt Läufe, die von {role} gestartet wurden, nur diese Änderungsarten zusammenführen: {classes}. Dies ist eine andere Art, daher wartet der Pull Request auf jemanden, dessen Rolle ihn zusammenführen darf.",
|
|
1803
1809
|
"dry_run": "Dies war ein Probelauf, daher blieb der Pull Request für einen Menschen offen und die Bewertungen wurden nicht verglichen. Starte die Aufgabe erneut als echten Lauf, um einen Pull Request zu erzeugen, den dieser Arbeitsbereich zusammenführt."
|
|
1804
1810
|
},
|
|
1805
1811
|
"scores": "Bewertungen",
|
|
@@ -1811,7 +1817,8 @@
|
|
|
1811
1817
|
"ceiling": "Obergrenze {value}",
|
|
1812
1818
|
"rationale": "Begründung",
|
|
1813
1819
|
"noAssessment": "Der Merger hat keine bewertete Einschätzung zurückgegeben.",
|
|
1814
|
-
"noResult": "Noch keine Merge-Entscheidung aufgezeichnet."
|
|
1820
|
+
"noResult": "Noch keine Merge-Entscheidung aufgezeichnet.",
|
|
1821
|
+
"noSubmittableClasses": "keine"
|
|
1815
1822
|
},
|
|
1816
1823
|
"testReport": {
|
|
1817
1824
|
"title": "Testbericht",
|
|
@@ -5271,7 +5278,8 @@
|
|
|
5271
5278
|
"input_gate_parked": "Über die Eingabeprüfung der Aufgabe beantworten",
|
|
5272
5279
|
"ticket_already_linked": "Dieses Ticket hat bereits eine Aufgabe",
|
|
5273
5280
|
"document_already_linked": "Dokument bereits angehängt",
|
|
5274
|
-
"dry_run_not_mergeable": "Probelauf kann nicht zusammengeführt werden"
|
|
5281
|
+
"dry_run_not_mergeable": "Probelauf kann nicht zusammengeführt werden",
|
|
5282
|
+
"submission_not_allowed": "Zusammenführen für diesen Lauf nicht erlaubt"
|
|
5275
5283
|
},
|
|
5276
5284
|
"description": {
|
|
5277
5285
|
"dependencies_unmet": "Diese Aufgabe hängt von anderen ab, die noch nicht abgeschlossen sind. Schließe sie ab oder gib sie frei und starte dann erneut.",
|
|
@@ -5308,7 +5316,8 @@
|
|
|
5308
5316
|
"input_gate_parked": "Dieser Lauf wartet auf seine Eingabeprüfung, die über die Freigabe nicht beantwortet werden kann. Nutzen Sie den Hinweis am Lauf: Aufgabe ergänzen und erneut prüfen, oder trotzdem ausführen.",
|
|
5309
5317
|
"ticket_already_linked": "Ein Ticket kann nur eine Aufgabe stützen. Es erneut zu verknüpfen würde der bestehenden Aufgabe genau den Kontext entziehen, mit dem sie angelegt wurde. Öffne stattdessen diese Aufgabe oder hebe die Verknüpfung des Tickets zuerst auf.",
|
|
5310
5318
|
"document_already_linked": "Dieses Dokument ist an eine andere Aufgabe angehängt. Lösen Sie es dort zuerst, oder hängen Sie eine separate Kopie an.",
|
|
5311
|
-
"dry_run_not_mergeable": "Dieser Pull Request stammt aus einem Probelauf und kann hier nicht zusammengeführt werden. Starte die Aufgabe erneut als echten Lauf, um einen Pull Request zu erzeugen, den dieser Arbeitsbereich zusammenführt."
|
|
5319
|
+
"dry_run_not_mergeable": "Dieser Pull Request stammt aus einem Probelauf und kann hier nicht zusammengeführt werden. Starte die Aufgabe erneut als echten Lauf, um einen Pull Request zu erzeugen, den dieser Arbeitsbereich zusammenführt.",
|
|
5320
|
+
"submission_not_allowed": "Die Merge-Richtlinie dieser Aufgabe erlaubt der Rolle, die diesen Lauf gestartet hat, diese Änderungsart nicht zusammenzuführen. Jemand mit einer passenden Rolle kann es tun, oder ein Admin erweitert die Richtlinie in der Merge-Vorlage."
|
|
5312
5321
|
},
|
|
5313
5322
|
"action": {
|
|
5314
5323
|
"connectGitHub": "GitHub verbinden",
|
|
@@ -6156,7 +6165,12 @@
|
|
|
6156
6165
|
},
|
|
6157
6166
|
"picker": {
|
|
6158
6167
|
"workspaceDefaultCaption": "Gilt, weil diese Aufgabe keine eigene Richtlinie wählt.",
|
|
6159
|
-
"noneHint": "Keine Risikorichtlinie konfiguriert. Jeder Pull Request wartet auf eine menschliche Prüfung."
|
|
6168
|
+
"noneHint": "Keine Risikorichtlinie konfiguriert. Jeder Pull Request wartet auf eine menschliche Prüfung.",
|
|
6169
|
+
"refused": {
|
|
6170
|
+
"relaxes_role_sandbox": "Für deine Rolle nicht verfügbar: Läufe dieser Aufgabe laufen in der Sandbox, und diese Richtlinie würde sie mergen lassen. Eine Workspace-Administration kann das ändern.",
|
|
6171
|
+
"relaxes_role_submission_allowlist": "Für deine Rolle nicht verfügbar: Diese Richtlinie würde dich Änderungsarten mergen lassen, die dir bei dieser Aufgabe verwehrt sind. Eine Workspace-Administration kann das ändern.",
|
|
6172
|
+
"relaxes_role_class_rule": "Für deine Rolle nicht verfügbar: Diese Richtlinie merged Änderungen automatisch, die du bei dieser Aufgabe prüfen musst. Eine Workspace-Administration kann das ändern."
|
|
6173
|
+
}
|
|
6160
6174
|
},
|
|
6161
6175
|
"preview": {
|
|
6162
6176
|
"defaultBadge": "Standard",
|
|
@@ -6174,6 +6188,7 @@
|
|
|
6174
6188
|
"ciAttempts": "Der Lauf darf {count} Mal versuchen, eine rote CI grün zu bekommen, bevor er aufgibt. | Der Lauf darf {count} Mal versuchen, eine rote CI grün zu bekommen, bevor er aufgibt.",
|
|
6175
6189
|
"roleHeading": "Wer den Lauf gestartet hat",
|
|
6176
6190
|
"roleSandboxed": "Läufe von {roles} öffnen einen Pull Request und werden nie zusammengeführt.",
|
|
6191
|
+
"roleScoped": "Läufe von {roles} führen nur bestimmte Änderungsarten zusammen.",
|
|
6177
6192
|
"roleNarrowed": "Für Läufe von {roles} gelten in einigen Änderungskategorien strengere Regeln."
|
|
6178
6193
|
}
|
|
6179
6194
|
},
|
package/i18n/locales/en.json
CHANGED
|
@@ -644,7 +644,8 @@
|
|
|
644
644
|
"input_gate_parked": "Answer it in the task's input check",
|
|
645
645
|
"ticket_already_linked": "This issue already has a task",
|
|
646
646
|
"document_already_linked": "Document already attached",
|
|
647
|
-
"dry_run_not_mergeable": "Dry run cannot be merged"
|
|
647
|
+
"dry_run_not_mergeable": "Dry run cannot be merged",
|
|
648
|
+
"submission_not_allowed": "Merge not allowed for this run"
|
|
648
649
|
},
|
|
649
650
|
"description": {
|
|
650
651
|
"dependencies_unmet": "This task depends on others that aren't finished yet. Complete or unblock them, then start it again.",
|
|
@@ -684,7 +685,8 @@
|
|
|
684
685
|
"input_gate_parked": "This run is parked on its input check, which the approval rail cannot answer. Use the notice on the run: fix the task and re-check, or run it anyway.",
|
|
685
686
|
"ticket_already_linked": "An issue can back only one task, so linking it again would strip the existing task of the context it was created with. Open that task instead, or unlink the issue first.",
|
|
686
687
|
"document_already_linked": "That document is attached to another task. Detach it there first, or attach a separate copy.",
|
|
687
|
-
"dry_run_not_mergeable": "This pull request came from a dry run, so it can't be merged from here. Start the task again as a live run to produce a pull request this workspace will merge."
|
|
688
|
+
"dry_run_not_mergeable": "This pull request came from a dry run, so it can't be merged from here. Start the task again as a live run to produce a pull request this workspace will merge.",
|
|
689
|
+
"submission_not_allowed": "This task's merge policy doesn't let the role that started this run merge this kind of change. Somebody whose role may merge it can do so, or an admin can widen the policy in the merge preset."
|
|
688
690
|
},
|
|
689
691
|
"action": {
|
|
690
692
|
"connectGitHub": "Connect GitHub",
|
|
@@ -1354,6 +1356,7 @@
|
|
|
1354
1356
|
"class_auto_merge": "This preset always auto-merges this class of change, so the scores were not compared.",
|
|
1355
1357
|
"class_requires_review": "This preset always sends this class of change to a human, whatever the scores.",
|
|
1356
1358
|
"role_requires_review": "This preset holds runs started by a {role} to human review for this class of change, whatever the scores. A teammate on a higher tier can merge this PR as it stands.",
|
|
1359
|
+
"submission_not_allowed": "This preset lets runs started by a {role} merge only these kinds of change: {classes}. This one is a different kind, so the pull request is waiting for somebody whose role may merge it.",
|
|
1357
1360
|
"dry_run": "This was a dry run, so the pull request was left open for a human and the scores were not compared. Start the task again as a live run to produce a pull request this workspace will merge."
|
|
1358
1361
|
},
|
|
1359
1362
|
"scores": "Scores",
|
|
@@ -1365,7 +1368,11 @@
|
|
|
1365
1368
|
"ceiling": "ceiling {value}",
|
|
1366
1369
|
"rationale": "Rationale",
|
|
1367
1370
|
"noAssessment": "The merger did not return a scored assessment.",
|
|
1368
|
-
"noResult": "No merge decision recorded yet."
|
|
1371
|
+
"noResult": "No merge decision recorded yet.",
|
|
1372
|
+
"noSubmittableClasses": "none",
|
|
1373
|
+
"@noSubmittableClasses": {
|
|
1374
|
+
"description": "Substituted into the {classes} slot of `reason.submission_not_allowed` when the role's allowlist is empty, so it reads as a list with no members ('...only these kinds of change: none'), NOT as a standalone sentence or an absence of policy."
|
|
1375
|
+
}
|
|
1369
1376
|
},
|
|
1370
1377
|
"testReport": {
|
|
1371
1378
|
"title": "Test report",
|
|
@@ -3013,10 +3020,15 @@
|
|
|
3013
3020
|
},
|
|
3014
3021
|
"roleRules": {
|
|
3015
3022
|
"heading": "Limits per initiator role",
|
|
3016
|
-
"help": "What a run may do also depends on who started it. A role can be held to stricter rules than the ones above, or to dry runs only: the pipeline works and opens a pull request, and nothing merges. A role can only narrow this policy, never widen it.",
|
|
3017
|
-
"autoMergeOffWarning": "Auto-merge is off for this preset, so every pull request already goes to a human and the class limits below add nothing.
|
|
3023
|
+
"help": "What a run may do also depends on who started it. A role can be limited to merging only certain kinds of change, held to stricter rules than the ones above, or held to dry runs only: the pipeline works and opens a pull request, and nothing merges. A role can only narrow this policy, never widen it.",
|
|
3024
|
+
"autoMergeOffWarning": "Auto-merge is off for this preset, so every pull request already goes to a human and the per-class limits below add nothing. The merge limit and dry runs still apply: both refuse a manual merge too.",
|
|
3018
3025
|
"sandboxLabel": "Dry runs only",
|
|
3019
|
-
"sandboxNote": "Runs started by this role open a pull request and merge nothing, so
|
|
3026
|
+
"sandboxNote": "Runs started by this role open a pull request and merge nothing, so neither limit below can hold them to anything stricter.",
|
|
3027
|
+
"submissionLabel": "Limit which changes this role may merge",
|
|
3028
|
+
"submissionHelp": "When on, only the ticked kinds of change are merged for runs this role starts. Anything else stays open for somebody else, whatever the scores say. The pull request is still opened, and it can still be merged by hand on the git host.",
|
|
3029
|
+
"submissionNone": "Nothing is ticked, so this workspace will not merge any run this role starts.",
|
|
3030
|
+
"submissionBadge": "Merge limited",
|
|
3031
|
+
"classHeading": "Review required per class",
|
|
3020
3032
|
"noRules": "No class limits",
|
|
3021
3033
|
"ruleCount": "{count} class limited | {count} classes limited",
|
|
3022
3034
|
"@ruleCount": {
|
|
@@ -4744,7 +4756,12 @@
|
|
|
4744
4756
|
},
|
|
4745
4757
|
"picker": {
|
|
4746
4758
|
"workspaceDefaultCaption": "Applied because this task picks no policy of its own.",
|
|
4747
|
-
"noneHint": "No risk policy configured. Every pull request waits for a human review."
|
|
4759
|
+
"noneHint": "No risk policy configured. Every pull request waits for a human review.",
|
|
4760
|
+
"refused": {
|
|
4761
|
+
"relaxes_role_sandbox": "Not available for your role: this task’s runs are sandboxed, and this policy would let them merge. A workspace admin can change it.",
|
|
4762
|
+
"relaxes_role_submission_allowlist": "Not available for your role: this policy would let you land kinds of change this task holds you back from. A workspace admin can change it.",
|
|
4763
|
+
"relaxes_role_class_rule": "Not available for your role: this policy auto-merges changes you are held to review on this task. A workspace admin can change it."
|
|
4764
|
+
}
|
|
4748
4765
|
},
|
|
4749
4766
|
"preview": {
|
|
4750
4767
|
"defaultBadge": "Default",
|
|
@@ -4762,6 +4779,7 @@
|
|
|
4762
4779
|
"ciAttempts": "The run may try {count} time to turn a red CI green before it gives up. | The run may try {count} times to turn a red CI green before it gives up.",
|
|
4763
4780
|
"roleHeading": "Who started the run",
|
|
4764
4781
|
"roleSandboxed": "Runs started by {roles} open a pull request and never merge.",
|
|
4782
|
+
"roleScoped": "Runs started by {roles} merge only some kinds of change.",
|
|
4765
4783
|
"roleNarrowed": "Runs started by {roles} follow stricter rules for some change classes."
|
|
4766
4784
|
}
|
|
4767
4785
|
},
|