openxiangda 1.0.174 → 1.0.176
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/README.md +35 -2
- package/lib/cli.js +1118 -98
- package/lib/content-cache.js +97 -0
- package/lib/dependency-capsule.js +246 -0
- package/lib/integration-bundle.js +204 -0
- package/lib/policy.js +97 -0
- package/lib/release-explain.js +75 -0
- package/lib/release-plan.js +65 -2
- package/lib/sdd.js +163 -13
- package/lib/task-status.js +267 -0
- package/lib/worktree-owner.js +7 -1
- package/openxiangda-skills/SKILL.md +3 -2
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -2
- package/package.json +19 -3
- package/policy/openxiangda-policy.json +51 -0
- package/templates/openxiangda-react-spa/AGENTS.md +2 -1
- package/templates/openxiangda-react-spa/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -1
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
function explainReleaseExecution(execution) {
|
|
2
|
+
if (!execution) {
|
|
3
|
+
return {
|
|
4
|
+
state: 'not-started',
|
|
5
|
+
resumable: false,
|
|
6
|
+
writeReviewRequired: false,
|
|
7
|
+
summary: '本地没有该 change 的 release execution journal。',
|
|
8
|
+
nextAction: '先运行 release publish --dry-run 检查计划,再开始正式发布。',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
const steps = Array.isArray(execution.steps) ? execution.steps : [];
|
|
12
|
+
const failedStep = steps.find(step => step.status === 'failed');
|
|
13
|
+
const runningStep = steps.find(step =>
|
|
14
|
+
['running', 'write-started'].includes(step.status)
|
|
15
|
+
);
|
|
16
|
+
const pendingSteps = steps.filter(step => step.status === 'pending');
|
|
17
|
+
const completedSteps = steps.filter(step => step.status === 'completed');
|
|
18
|
+
const state = execution.status || 'unknown';
|
|
19
|
+
const common = {
|
|
20
|
+
state,
|
|
21
|
+
resumable: state === 'staged-resumable',
|
|
22
|
+
writeReviewRequired: state === 'write-review-required',
|
|
23
|
+
progress: {
|
|
24
|
+
completed: completedSteps.length,
|
|
25
|
+
pending: pendingSteps.length,
|
|
26
|
+
total: steps.length,
|
|
27
|
+
currentStep: runningStep?.id || failedStep?.id || pendingSteps[0]?.id || null,
|
|
28
|
+
},
|
|
29
|
+
failedStep: failedStep
|
|
30
|
+
? {
|
|
31
|
+
id: failedStep.id,
|
|
32
|
+
code: failedStep.errorCode || null,
|
|
33
|
+
message: failedStep.error || null,
|
|
34
|
+
}
|
|
35
|
+
: null,
|
|
36
|
+
};
|
|
37
|
+
if (state === 'completed') {
|
|
38
|
+
return {
|
|
39
|
+
...common,
|
|
40
|
+
summary: '本次应用原子发布已完成。',
|
|
41
|
+
nextAction: '执行验收;确认无误后可归档 SDD change。',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (state === 'write-review-required') {
|
|
45
|
+
return {
|
|
46
|
+
...common,
|
|
47
|
+
summary: '上次写请求的结果不确定,CLI 已停止自动重试以避免重复写入。',
|
|
48
|
+
nextAction:
|
|
49
|
+
'先只读核对 staged child/App Release;确认可安全重试后追加 --resume-after-review。',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (state === 'staged-resumable') {
|
|
53
|
+
return {
|
|
54
|
+
...common,
|
|
55
|
+
summary: '已有 staged child,active head 尚未全部切换,可以从执行日志继续。',
|
|
56
|
+
nextAction: '重新运行同一条 release publish 命令继续,不要另起全量发布。',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (state === 'failed') {
|
|
60
|
+
return {
|
|
61
|
+
...common,
|
|
62
|
+
summary: '发布在确认没有成功写入后失败,可以修复原因后重试。',
|
|
63
|
+
nextAction: '修复 failedStep 后重新运行同一条 release publish 命令。',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...common,
|
|
68
|
+
summary: runningStep
|
|
69
|
+
? `发布停在 ${runningStep.id}。`
|
|
70
|
+
: '发布尚未完成。',
|
|
71
|
+
nextAction: '使用同一 change/profile 重新运行 release publish,让执行日志决定续跑位置。',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { explainReleaseExecution };
|
package/lib/release-plan.js
CHANGED
|
@@ -11,6 +11,11 @@ function uniqueSorted(values = []) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function normalizeReleaseTargets(targets = {}, runtimeMode = 'legacy') {
|
|
14
|
+
const resourceSelectors = Object.fromEntries(
|
|
15
|
+
Object.entries(targets.resourceSelectors || targets.resourceTargets || {})
|
|
16
|
+
.map(([type, values]) => [type, uniqueSorted(values || [])])
|
|
17
|
+
.filter(([, values]) => values.length > 0)
|
|
18
|
+
);
|
|
14
19
|
const logicalTargets = {
|
|
15
20
|
forms: uniqueSorted(targets.forms || []),
|
|
16
21
|
pages: uniqueSorted(targets.pages || []),
|
|
@@ -19,6 +24,7 @@ function normalizeReleaseTargets(targets = {}, runtimeMode = 'legacy') {
|
|
|
19
24
|
workflows: uniqueSorted(targets.workflows || []),
|
|
20
25
|
jsCodeNodes: uniqueSorted(targets.jsCodeNodes || []),
|
|
21
26
|
resources: Boolean(targets.resources),
|
|
27
|
+
resourceSelectors,
|
|
22
28
|
runtime: Boolean(targets.runtime),
|
|
23
29
|
other: uniqueSorted(targets.other || []),
|
|
24
30
|
};
|
|
@@ -36,6 +42,35 @@ function normalizeReleaseTargets(targets = {}, runtimeMode = 'legacy') {
|
|
|
36
42
|
return { logicalTargets, activationTargets };
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
function unsupportedAtomicReleaseTargets(targets = {}) {
|
|
46
|
+
const unsupported = [];
|
|
47
|
+
if (targets.resources) unsupported.push('generic-resources');
|
|
48
|
+
for (const [type, selectors] of Object.entries(targets.resourceSelectors || {})) {
|
|
49
|
+
if ((selectors || []).length > 0) {
|
|
50
|
+
unsupported.push(`${type}:${selectors.join(',')}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
(targets.jsCodeNodes || []).length > 0 &&
|
|
55
|
+
(targets.functions || []).length === 0 &&
|
|
56
|
+
(targets.automations || []).length === 0
|
|
57
|
+
) {
|
|
58
|
+
unsupported.push(`js-code-owner:${targets.jsCodeNodes.join(',')}`);
|
|
59
|
+
}
|
|
60
|
+
return unsupported;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertAtomicReleaseTargetsSupported(targets) {
|
|
64
|
+
const unsupported = unsupportedAtomicReleaseTargets(targets);
|
|
65
|
+
if (unsupported.length === 0) return;
|
|
66
|
+
const error = new Error(
|
|
67
|
+
`RELEASE_ATOMIC_TARGET_UNSUPPORTED: 以下资源尚无 immutable staged child,禁止与原子发布混合后降级为直接写入: ${unsupported.join('; ')}`
|
|
68
|
+
);
|
|
69
|
+
error.code = 'RELEASE_ATOMIC_TARGET_UNSUPPORTED';
|
|
70
|
+
error.unsupportedTargets = unsupported;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
function buildTargetedResourceReleaseCommands(
|
|
40
75
|
targets,
|
|
41
76
|
profileArg,
|
|
@@ -116,7 +151,7 @@ function buildTargetedResourceReleaseSteps(
|
|
|
116
151
|
if (workflows.length > 0) {
|
|
117
152
|
steps.push(
|
|
118
153
|
createStep(
|
|
119
|
-
'workflow-
|
|
154
|
+
'workflow-stage',
|
|
120
155
|
appendProfileAndChange(
|
|
121
156
|
[
|
|
122
157
|
'resource',
|
|
@@ -124,10 +159,12 @@ function buildTargetedResourceReleaseSteps(
|
|
|
124
159
|
'workflow',
|
|
125
160
|
'--only',
|
|
126
161
|
workflows.join(','),
|
|
162
|
+
'--stage-only',
|
|
127
163
|
],
|
|
128
164
|
profileArg,
|
|
129
165
|
changeId
|
|
130
|
-
)
|
|
166
|
+
),
|
|
167
|
+
{ stagedKind: 'WorkflowRelease' }
|
|
131
168
|
)
|
|
132
169
|
);
|
|
133
170
|
}
|
|
@@ -159,6 +196,7 @@ function buildWorkspaceReleaseSteps(
|
|
|
159
196
|
targetsInput,
|
|
160
197
|
runtimeMode
|
|
161
198
|
);
|
|
199
|
+
assertAtomicReleaseTargetsSupported(targets);
|
|
162
200
|
const steps = [];
|
|
163
201
|
const hasResourceTargets =
|
|
164
202
|
Boolean(targets.resources) ||
|
|
@@ -212,6 +250,7 @@ function buildWorkspaceReleaseSteps(
|
|
|
212
250
|
targets.forms.length > 0 ||
|
|
213
251
|
targets.functions.length > 0 ||
|
|
214
252
|
targets.automations.length > 0 ||
|
|
253
|
+
targets.workflows.length > 0 ||
|
|
215
254
|
targets.runtime
|
|
216
255
|
) {
|
|
217
256
|
steps.push(
|
|
@@ -223,6 +262,7 @@ function buildWorkspaceReleaseSteps(
|
|
|
223
262
|
'app-finalize',
|
|
224
263
|
'--staged-resources-json',
|
|
225
264
|
`.openxiangda/releases/${changeId || 'change'}/staged-resources.json`,
|
|
265
|
+
'--wait',
|
|
226
266
|
],
|
|
227
267
|
profileArg,
|
|
228
268
|
changeId
|
|
@@ -260,6 +300,28 @@ function buildWorkspaceReleaseSteps(
|
|
|
260
300
|
...buildTargetedResourceReleaseSteps(targets, profileArg, changeId)
|
|
261
301
|
);
|
|
262
302
|
}
|
|
303
|
+
if (
|
|
304
|
+
targets.functions.length > 0 ||
|
|
305
|
+
targets.automations.length > 0 ||
|
|
306
|
+
targets.workflows.length > 0
|
|
307
|
+
) {
|
|
308
|
+
steps.push(
|
|
309
|
+
createStep(
|
|
310
|
+
'app-finalize',
|
|
311
|
+
appendProfileAndChange(
|
|
312
|
+
[
|
|
313
|
+
'release',
|
|
314
|
+
'app-finalize',
|
|
315
|
+
'--staged-resources-json',
|
|
316
|
+
`.openxiangda/releases/${changeId || 'change'}/staged-resources.json`,
|
|
317
|
+
'--wait',
|
|
318
|
+
],
|
|
319
|
+
profileArg,
|
|
320
|
+
changeId
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
263
325
|
return steps;
|
|
264
326
|
}
|
|
265
327
|
|
|
@@ -291,4 +353,5 @@ module.exports = {
|
|
|
291
353
|
commandFromArgs,
|
|
292
354
|
normalizeReleaseTargets,
|
|
293
355
|
releasePlanHash,
|
|
356
|
+
unsupportedAtomicReleaseTargets,
|
|
294
357
|
};
|
package/lib/sdd.js
CHANGED
|
@@ -166,6 +166,12 @@ function getSddGovernanceConfig(options = {}) {
|
|
|
166
166
|
const strictDocumentation = Boolean(
|
|
167
167
|
configured && /strictDocumentation\s*:\s*true/.test(block)
|
|
168
168
|
);
|
|
169
|
+
const documentationModeMatch = block.match(
|
|
170
|
+
/documentationMode\s*:\s*['"]?(structured|full)['"]?/
|
|
171
|
+
);
|
|
172
|
+
const documentationMode = strictDocumentation
|
|
173
|
+
? 'full'
|
|
174
|
+
: documentationModeMatch?.[1] || 'structured';
|
|
169
175
|
const pathMatch = block.match(/path\s*:\s*['"]([^'"]+)['"]/);
|
|
170
176
|
const schemaMatch = block.match(/schemaVersion\s*:\s*['"]([^'"]+)['"]/);
|
|
171
177
|
const sddPath = normalizeRelPath(pathMatch?.[1] || DEFAULT_SDD_DIR);
|
|
@@ -182,6 +188,7 @@ function getSddGovernanceConfig(options = {}) {
|
|
|
182
188
|
enabled,
|
|
183
189
|
strictHighRisk,
|
|
184
190
|
strictDocumentation,
|
|
191
|
+
documentationMode,
|
|
185
192
|
path: sddPath,
|
|
186
193
|
rootDir,
|
|
187
194
|
initialized,
|
|
@@ -204,6 +211,7 @@ function buildDefaultConfigYaml() {
|
|
|
204
211
|
' coverage: coverage.json',
|
|
205
212
|
' release: release.json',
|
|
206
213
|
' metadata: change.json',
|
|
214
|
+
' documentationMode: structured',
|
|
207
215
|
'rules:',
|
|
208
216
|
' requireApprovalForHighRisk: true',
|
|
209
217
|
' requireAffectedCoverage: true',
|
|
@@ -324,6 +332,18 @@ function splitList(value) {
|
|
|
324
332
|
.filter(Boolean);
|
|
325
333
|
}
|
|
326
334
|
|
|
335
|
+
function normalizeResourceSelectors(value = {}) {
|
|
336
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
337
|
+
return Object.fromEntries(
|
|
338
|
+
Object.entries(value)
|
|
339
|
+
.map(([key, selectors]) => [
|
|
340
|
+
String(key || '').trim(),
|
|
341
|
+
Array.from(new Set(splitList(selectors))).sort(),
|
|
342
|
+
])
|
|
343
|
+
.filter(([key, selectors]) => key && selectors.length > 0)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
327
347
|
function normalizeAffected(input = {}) {
|
|
328
348
|
const affected = input.affected || input;
|
|
329
349
|
return {
|
|
@@ -334,6 +354,9 @@ function normalizeAffected(input = {}) {
|
|
|
334
354
|
workflows: splitList(affected.workflows || affected.workflow),
|
|
335
355
|
jsCodeNodes: splitList(affected.jsCodeNodes || affected.jsCode || affected['js-code-nodes']),
|
|
336
356
|
files: splitList(affected.files || affected.file),
|
|
357
|
+
resourceSelectors: normalizeResourceSelectors(
|
|
358
|
+
affected.resourceSelectors || affected.resourceTargets
|
|
359
|
+
),
|
|
337
360
|
resources: affected.resources === true || affected.resources === 'true' || splitList(affected.resources).includes('*'),
|
|
338
361
|
runtime: affected.runtime === true || affected.runtime === 'true',
|
|
339
362
|
backend: affected.backend === true || affected.backend === 'true',
|
|
@@ -346,6 +369,9 @@ function mergeAffectedScopes(...inputs) {
|
|
|
346
369
|
const mergeList = key =>
|
|
347
370
|
Array.from(new Set(scopes.flatMap(scope => scope[key] || []))).sort();
|
|
348
371
|
const hasFlag = key => scopes.some(scope => Boolean(scope[key]));
|
|
372
|
+
const resourceSelectorKeys = Array.from(
|
|
373
|
+
new Set(scopes.flatMap(scope => Object.keys(scope.resourceSelectors || {})))
|
|
374
|
+
).sort();
|
|
349
375
|
return {
|
|
350
376
|
forms: mergeList('forms'),
|
|
351
377
|
pages: mergeList('pages'),
|
|
@@ -354,6 +380,16 @@ function mergeAffectedScopes(...inputs) {
|
|
|
354
380
|
workflows: mergeList('workflows'),
|
|
355
381
|
jsCodeNodes: mergeList('jsCodeNodes'),
|
|
356
382
|
files: mergeList('files'),
|
|
383
|
+
resourceSelectors: Object.fromEntries(
|
|
384
|
+
resourceSelectorKeys.map(key => [
|
|
385
|
+
key,
|
|
386
|
+
Array.from(
|
|
387
|
+
new Set(
|
|
388
|
+
scopes.flatMap(scope => scope.resourceSelectors?.[key] || [])
|
|
389
|
+
)
|
|
390
|
+
).sort(),
|
|
391
|
+
])
|
|
392
|
+
),
|
|
357
393
|
resources: hasFlag('resources'),
|
|
358
394
|
runtime: hasFlag('runtime'),
|
|
359
395
|
backend: hasFlag('backend'),
|
|
@@ -769,6 +805,52 @@ The application SHALL provide the approved "${meta.title}" behavior for the affe
|
|
|
769
805
|
`;
|
|
770
806
|
}
|
|
771
807
|
|
|
808
|
+
function renderSddDocumentation(options = {}) {
|
|
809
|
+
const loaded = options.loaded || loadSddChange(options);
|
|
810
|
+
const meta = loaded.meta;
|
|
811
|
+
const requirementSpec = loaded.coverage?.requirements?.find(
|
|
812
|
+
item => typeof item?.spec === 'string' && item.spec.trim()
|
|
813
|
+
)?.spec;
|
|
814
|
+
const specRelativePath =
|
|
815
|
+
requirementSpec && !requirementSpec.includes('..')
|
|
816
|
+
? requirementSpec.replace(/\\/g, '/')
|
|
817
|
+
: 'specs/app/spec.md';
|
|
818
|
+
const specMatch = specRelativePath.match(/^specs\/([^/]+)\/spec\.md$/);
|
|
819
|
+
const domain = options.domain || specMatch?.[1] || 'app';
|
|
820
|
+
const files = {
|
|
821
|
+
proposal: path.join(loaded.changeDir, 'proposal.md'),
|
|
822
|
+
design: path.join(loaded.changeDir, 'design.md'),
|
|
823
|
+
tasks: path.join(loaded.changeDir, 'tasks.md'),
|
|
824
|
+
evidence: path.join(loaded.changeDir, 'evidence.md'),
|
|
825
|
+
spec: path.join(loaded.changeDir, 'specs', domain, 'spec.md'),
|
|
826
|
+
};
|
|
827
|
+
const contents = {
|
|
828
|
+
proposal: createProposalContent(meta),
|
|
829
|
+
design: createDesignContent(meta),
|
|
830
|
+
tasks: createTasksContent(meta),
|
|
831
|
+
evidence: createEvidenceContent(meta),
|
|
832
|
+
spec: createDeltaSpecContent(meta, domain),
|
|
833
|
+
};
|
|
834
|
+
const rendered = [];
|
|
835
|
+
const skipped = [];
|
|
836
|
+
for (const [kind, file] of Object.entries(files)) {
|
|
837
|
+
if (fs.existsSync(file) && !options.force) {
|
|
838
|
+
skipped.push(path.relative(options.cwd || process.cwd(), file).replace(/\\/g, '/'));
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
ensureDir(path.dirname(file));
|
|
842
|
+
fs.writeFileSync(file, contents[kind]);
|
|
843
|
+
rendered.push(path.relative(options.cwd || process.cwd(), file).replace(/\\/g, '/'));
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
schemaVersion: meta.schemaVersion || SDD_SCHEMA_VERSION,
|
|
847
|
+
changeId: loaded.changeId,
|
|
848
|
+
mode: 'full',
|
|
849
|
+
rendered,
|
|
850
|
+
skipped,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
772
854
|
function createCoverageManifest(meta, domain) {
|
|
773
855
|
const reqId = `req-${slug(meta.title || meta.id, meta.id)}`;
|
|
774
856
|
return {
|
|
@@ -860,25 +942,45 @@ function proposeSddChange(options = {}) {
|
|
|
860
942
|
writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta, jsonWriteOptions);
|
|
861
943
|
writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage, jsonWriteOptions);
|
|
862
944
|
writeJsonFile(path.join(changeDir, RELEASE_FILE), release, jsonWriteOptions);
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
945
|
+
const loaded = {
|
|
946
|
+
governance,
|
|
947
|
+
changeId,
|
|
948
|
+
changeDir,
|
|
949
|
+
meta,
|
|
950
|
+
coverage,
|
|
951
|
+
};
|
|
952
|
+
const documentation =
|
|
953
|
+
governance.documentationMode === 'full' || options.renderDocumentation
|
|
954
|
+
? renderSddDocumentation({
|
|
955
|
+
cwd: options.cwd,
|
|
956
|
+
loaded,
|
|
957
|
+
domain,
|
|
958
|
+
force: true,
|
|
959
|
+
})
|
|
960
|
+
: {
|
|
961
|
+
mode: 'structured',
|
|
962
|
+
rendered: [],
|
|
963
|
+
skipped: [],
|
|
964
|
+
};
|
|
871
965
|
return {
|
|
872
966
|
schemaVersion: SDD_SCHEMA_VERSION,
|
|
873
967
|
change: meta,
|
|
874
968
|
coverage,
|
|
875
969
|
release,
|
|
970
|
+
documentation,
|
|
876
971
|
dir: path.relative(options.cwd || process.cwd(), changeDir).replace(/\\/g, '/'),
|
|
877
|
-
nextActions:
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
972
|
+
nextActions:
|
|
973
|
+
documentation.mode === 'full'
|
|
974
|
+
? [
|
|
975
|
+
`Review openspec/changes/${changeId}/proposal.md`,
|
|
976
|
+
`Sharpen openspec/changes/${changeId}/specs/${domain}/spec.md`,
|
|
977
|
+
`Run openxiangda sdd approve ${changeId} after user confirmation`,
|
|
978
|
+
]
|
|
979
|
+
: [
|
|
980
|
+
`Review openspec/changes/${changeId}/change.json and coverage.json`,
|
|
981
|
+
`Run openxiangda sdd approve ${changeId} after user confirmation`,
|
|
982
|
+
`Optional: openxiangda sdd render ${changeId}`,
|
|
983
|
+
],
|
|
882
984
|
};
|
|
883
985
|
}
|
|
884
986
|
|
|
@@ -981,6 +1083,7 @@ function createMainlineSddBundle(options = {}) {
|
|
|
981
1083
|
changeId,
|
|
982
1084
|
mainlinePolicy: 'publish-from-main-v1',
|
|
983
1085
|
bundledChanges: sourceIds,
|
|
1086
|
+
...(options.integration ? { integration: options.integration } : {}),
|
|
984
1087
|
targets: normalizedTargets.activationTargets,
|
|
985
1088
|
logicalTargets: normalizedTargets.logicalTargets,
|
|
986
1089
|
changedFiles: affected.files,
|
|
@@ -1221,6 +1324,16 @@ function affectedCoversTargets(affected, targets = {}) {
|
|
|
1221
1324
|
missing.push(`${key}:${required.filter(item => !normalized[key].includes(item)).join(',')}`);
|
|
1222
1325
|
}
|
|
1223
1326
|
if (targets.resources && !normalized.resources && !normalized.backend) missing.push('resources');
|
|
1327
|
+
for (const [key, required] of Object.entries(targets.resourceSelectors || {})) {
|
|
1328
|
+
if (normalized.resources || includesAll(normalized.resourceSelectors?.[key], required)) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
missing.push(
|
|
1332
|
+
`resourceSelectors.${key}:${required
|
|
1333
|
+
.filter(item => !(normalized.resourceSelectors?.[key] || []).includes(item))
|
|
1334
|
+
.join(',')}`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1224
1337
|
if (targets.runtime && !normalized.runtime) missing.push('runtime');
|
|
1225
1338
|
return {
|
|
1226
1339
|
covered: missing.length === 0,
|
|
@@ -1305,6 +1418,14 @@ function normalizeCoverageResources(coverage, fallbackAffected = {}, options = {
|
|
|
1305
1418
|
resources[key] === 'true' ||
|
|
1306
1419
|
requirementCovers.some(item => item[key] === true || item[key] === 'true') ||
|
|
1307
1420
|
Boolean(fallback[key]);
|
|
1421
|
+
const selectorSources = [
|
|
1422
|
+
resources.resourceSelectors,
|
|
1423
|
+
...requirementCovers.map(item => item.resourceSelectors),
|
|
1424
|
+
fallback.resourceSelectors,
|
|
1425
|
+
].map(normalizeResourceSelectors);
|
|
1426
|
+
const selectorKeys = Array.from(
|
|
1427
|
+
new Set(selectorSources.flatMap(item => Object.keys(item)))
|
|
1428
|
+
).sort();
|
|
1308
1429
|
return {
|
|
1309
1430
|
forms: Array.from(new Set(mergeList('forms'))),
|
|
1310
1431
|
pages: Array.from(new Set(mergeList('pages'))),
|
|
@@ -1313,6 +1434,14 @@ function normalizeCoverageResources(coverage, fallbackAffected = {}, options = {
|
|
|
1313
1434
|
workflows: Array.from(new Set(mergeList('workflows'))),
|
|
1314
1435
|
jsCodeNodes: Array.from(new Set(mergeList('jsCodeNodes'))),
|
|
1315
1436
|
files: Array.from(new Set(mergeList('files'))),
|
|
1437
|
+
resourceSelectors: Object.fromEntries(
|
|
1438
|
+
selectorKeys.map(key => [
|
|
1439
|
+
key,
|
|
1440
|
+
Array.from(
|
|
1441
|
+
new Set(selectorSources.flatMap(item => item[key] || []))
|
|
1442
|
+
).sort(),
|
|
1443
|
+
])
|
|
1444
|
+
),
|
|
1316
1445
|
resources: hasBool('resources'),
|
|
1317
1446
|
runtime: hasBool('runtime'),
|
|
1318
1447
|
backend: hasBool('backend'),
|
|
@@ -1425,6 +1554,26 @@ function getSddChangeScope(options = {}) {
|
|
|
1425
1554
|
automations: mergeList('automations'),
|
|
1426
1555
|
workflows: mergeList('workflows'),
|
|
1427
1556
|
jsCodeNodes: mergeList('jsCodeNodes'),
|
|
1557
|
+
resourceSelectors: Object.fromEntries(
|
|
1558
|
+
Array.from(
|
|
1559
|
+
new Set([
|
|
1560
|
+
...Object.keys(releaseTargets.resourceSelectors || {}),
|
|
1561
|
+
...Object.keys(coverage.resourceSelectors || {}),
|
|
1562
|
+
...Object.keys(affected.resourceSelectors || {}),
|
|
1563
|
+
])
|
|
1564
|
+
)
|
|
1565
|
+
.sort()
|
|
1566
|
+
.map(key => [
|
|
1567
|
+
key,
|
|
1568
|
+
Array.from(
|
|
1569
|
+
new Set([
|
|
1570
|
+
...splitList(releaseTargets.resourceSelectors?.[key]),
|
|
1571
|
+
...splitList(coverage.resourceSelectors?.[key]),
|
|
1572
|
+
...splitList(affected.resourceSelectors?.[key]),
|
|
1573
|
+
])
|
|
1574
|
+
).sort(),
|
|
1575
|
+
])
|
|
1576
|
+
),
|
|
1428
1577
|
resources: Boolean(releaseTargets.resources || coverage.resources || affected.resources),
|
|
1429
1578
|
runtime: Boolean(releaseTargets.runtime || coverage.runtime || affected.runtime),
|
|
1430
1579
|
other: Array.from(new Set(splitList(releaseTargets.other))).sort(),
|
|
@@ -2854,6 +3003,7 @@ module.exports = {
|
|
|
2854
3003
|
proposeQuickSddChange,
|
|
2855
3004
|
proposeSddChange,
|
|
2856
3005
|
recordSddReleaseSourceRevision,
|
|
3006
|
+
renderSddDocumentation,
|
|
2857
3007
|
approveSddChange,
|
|
2858
3008
|
syncSddChange,
|
|
2859
3009
|
validateQuickSddScope,
|