openxiangda 1.0.172 → 1.0.173
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 +4 -7
- package/lib/change-baseline.js +117 -18
- package/lib/cli.js +695 -102
- package/lib/form-schema-loader.js +166 -0
- package/lib/git-remote-identity.js +89 -0
- package/lib/release-mainline.js +46 -24
- package/lib/release-plan.js +294 -0
- package/lib/runtime-lineage.js +23 -3
- package/lib/sdd.js +178 -41
- package/openxiangda-skills/SKILL.md +4 -6
- package/openxiangda-skills/references/openxiangda-api.md +6 -6
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/permissions-settings.md +4 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +4 -6
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +5 -4
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/templates/openxiangda-react-spa/AGENTS.md +5 -11
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -8
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { builtinModules, createRequire } = require('module');
|
|
4
|
+
const esbuild = require('esbuild');
|
|
5
|
+
|
|
6
|
+
function resolveWorkspaceAlias(workspaceRoot, importPath) {
|
|
7
|
+
const raw = path.join(
|
|
8
|
+
workspaceRoot,
|
|
9
|
+
'src',
|
|
10
|
+
String(importPath || '').replace(/^@\//, '')
|
|
11
|
+
);
|
|
12
|
+
const candidates = [
|
|
13
|
+
raw,
|
|
14
|
+
`${raw}.ts`,
|
|
15
|
+
`${raw}.tsx`,
|
|
16
|
+
`${raw}.js`,
|
|
17
|
+
`${raw}.mjs`,
|
|
18
|
+
`${raw}.cjs`,
|
|
19
|
+
path.join(raw, 'index.ts'),
|
|
20
|
+
path.join(raw, 'index.tsx'),
|
|
21
|
+
path.join(raw, 'index.js'),
|
|
22
|
+
];
|
|
23
|
+
return candidates.find(candidate => fs.existsSync(candidate)) || raw;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadLocalFormSchema(workspaceRoot, formCode) {
|
|
27
|
+
const schemaPath = path.join(
|
|
28
|
+
workspaceRoot,
|
|
29
|
+
'src',
|
|
30
|
+
'forms',
|
|
31
|
+
formCode,
|
|
32
|
+
'schema.ts'
|
|
33
|
+
);
|
|
34
|
+
if (!fs.existsSync(schemaPath)) return null;
|
|
35
|
+
const external = Array.from(
|
|
36
|
+
new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)])
|
|
37
|
+
);
|
|
38
|
+
const built = await esbuild.build({
|
|
39
|
+
entryPoints: [schemaPath],
|
|
40
|
+
bundle: true,
|
|
41
|
+
format: 'cjs',
|
|
42
|
+
platform: 'node',
|
|
43
|
+
target: ['node20'],
|
|
44
|
+
write: false,
|
|
45
|
+
logLevel: 'silent',
|
|
46
|
+
external,
|
|
47
|
+
plugins: [
|
|
48
|
+
{
|
|
49
|
+
name: 'openxiangda-form-schema-workspace-alias',
|
|
50
|
+
setup(build) {
|
|
51
|
+
build.onResolve({ filter: /^@\// }, args => ({
|
|
52
|
+
path: resolveWorkspaceAlias(workspaceRoot, args.path),
|
|
53
|
+
}));
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
const code = built.outputFiles?.[0]?.text;
|
|
59
|
+
if (!code) {
|
|
60
|
+
throw new Error(`FORM_SCHEMA_BUILD_FAILED: ${formCode} 未生成 schema bundle`);
|
|
61
|
+
}
|
|
62
|
+
const schemaModule = { exports: {} };
|
|
63
|
+
const localRequire = createRequire(schemaPath);
|
|
64
|
+
const evaluate = new Function(
|
|
65
|
+
'module',
|
|
66
|
+
'exports',
|
|
67
|
+
'require',
|
|
68
|
+
'__filename',
|
|
69
|
+
'__dirname',
|
|
70
|
+
code
|
|
71
|
+
);
|
|
72
|
+
evaluate(
|
|
73
|
+
schemaModule,
|
|
74
|
+
schemaModule.exports,
|
|
75
|
+
localRequire,
|
|
76
|
+
schemaPath,
|
|
77
|
+
path.dirname(schemaPath)
|
|
78
|
+
);
|
|
79
|
+
const exported = schemaModule.exports?.default || schemaModule.exports;
|
|
80
|
+
return normalizeLocalFormSchema(exported, formCode);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeLocalFormSchema(schema, formCode) {
|
|
84
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
85
|
+
throw new Error(`FORM_SCHEMA_INVALID: ${formCode} 默认导出必须是对象`);
|
|
86
|
+
}
|
|
87
|
+
if (!schema.formMeta || typeof schema.formMeta !== 'object') {
|
|
88
|
+
throw new Error(`FORM_SCHEMA_INVALID: ${formCode}.formMeta 缺失`);
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(schema.fields) || schema.fields.length === 0) {
|
|
91
|
+
throw new Error(`FORM_SCHEMA_INVALID: ${formCode}.fields 不能为空`);
|
|
92
|
+
}
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
const components = schema.fields.map((field, index) => {
|
|
95
|
+
const fieldId = String(field?.fieldId || '').trim();
|
|
96
|
+
const componentName = normalizeComponentName(field?.componentName);
|
|
97
|
+
if (!fieldId || !componentName) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`FORM_SCHEMA_INVALID: ${formCode}.fields[${index}] 缺少 fieldId/componentName`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (seen.has(fieldId)) {
|
|
103
|
+
throw new Error(`FORM_SCHEMA_INVALID: ${formCode} fieldId 重复: ${fieldId}`);
|
|
104
|
+
}
|
|
105
|
+
seen.add(fieldId);
|
|
106
|
+
return {
|
|
107
|
+
componentName,
|
|
108
|
+
id: field.id || `${fieldId}_${index + 1}`,
|
|
109
|
+
title: field.label || fieldId,
|
|
110
|
+
hidden: false,
|
|
111
|
+
isLocked: false,
|
|
112
|
+
condition: true,
|
|
113
|
+
conditionGroup: '',
|
|
114
|
+
props: {
|
|
115
|
+
...field,
|
|
116
|
+
componentName,
|
|
117
|
+
isFormComponent: true,
|
|
118
|
+
fieldId,
|
|
119
|
+
label: field.label || fieldId,
|
|
120
|
+
tips: field.tips || '',
|
|
121
|
+
value: field.value || '',
|
|
122
|
+
placeholder: field.placeholder || '',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const pageSchema = {
|
|
127
|
+
version: '2.0',
|
|
128
|
+
componentsTree: [
|
|
129
|
+
{
|
|
130
|
+
componentName: 'Page',
|
|
131
|
+
id: `${schema.formMeta.formUuid || formCode}_page`,
|
|
132
|
+
props: {},
|
|
133
|
+
children: components,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
name: schema.formMeta.title || formCode,
|
|
139
|
+
schema: JSON.stringify(pageSchema),
|
|
140
|
+
packages: JSON.stringify({}),
|
|
141
|
+
formType: normalizeFormType(
|
|
142
|
+
schema.formMeta.formType || schema.template?.formType
|
|
143
|
+
),
|
|
144
|
+
fieldCount: components.length,
|
|
145
|
+
sourcePath: `src/forms/${formCode}/schema.ts`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeComponentName(value) {
|
|
150
|
+
const name = String(value || '').trim();
|
|
151
|
+
return name === 'TextAreaField' ? 'TextareaField' : name;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeFormType(value) {
|
|
155
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
156
|
+
return ['process', 'workflow', 'flow', 'flowform', 'processform'].includes(
|
|
157
|
+
normalized
|
|
158
|
+
)
|
|
159
|
+
? 'process'
|
|
160
|
+
: 'receipt';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
loadLocalFormSchema,
|
|
165
|
+
normalizeLocalFormSchema,
|
|
166
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
function legacyRemoteIdentity(value) {
|
|
4
|
+
const remote = String(value || '').trim();
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(remote);
|
|
7
|
+
url.username = '';
|
|
8
|
+
url.password = '';
|
|
9
|
+
url.search = '';
|
|
10
|
+
url.hash = '';
|
|
11
|
+
return url.toString();
|
|
12
|
+
} catch {
|
|
13
|
+
return remote
|
|
14
|
+
.replace(/^[^@/]+@([^:]+):/, '$1:')
|
|
15
|
+
.replace(/[?#].*$/, '');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toggleDotGit(value) {
|
|
20
|
+
const normalized = String(value || '').replace(/\/+$/, '');
|
|
21
|
+
return /\.git$/i.test(normalized)
|
|
22
|
+
? normalized.replace(/\.git$/i, '')
|
|
23
|
+
: `${normalized}.git`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalRemoteIdentity(value) {
|
|
27
|
+
const legacy = legacyRemoteIdentity(value).replace(/\/+$/, '');
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(legacy);
|
|
30
|
+
const host = url.hostname.toLowerCase();
|
|
31
|
+
const port = url.port ? `:${url.port}` : '';
|
|
32
|
+
const pathname = url.pathname
|
|
33
|
+
.replace(/^\/+|\/+$/g, '')
|
|
34
|
+
.replace(/\.git$/i, '');
|
|
35
|
+
return `${host}${port}/${pathname}`.replace(/\/+$/, '');
|
|
36
|
+
} catch {
|
|
37
|
+
return legacy
|
|
38
|
+
.replace(/^([^:]+):/, '$1/')
|
|
39
|
+
.replace(/\.git$/i, '')
|
|
40
|
+
.replace(/\/+$/, '');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function remoteIdentityVariants(value) {
|
|
45
|
+
const legacy = legacyRemoteIdentity(value).replace(/\/+$/, '');
|
|
46
|
+
return Array.from(
|
|
47
|
+
new Set(
|
|
48
|
+
[
|
|
49
|
+
legacy,
|
|
50
|
+
toggleDotGit(legacy),
|
|
51
|
+
canonicalRemoteIdentity(legacy),
|
|
52
|
+
].filter(Boolean)
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256(value) {
|
|
58
|
+
return `sha256:${crypto
|
|
59
|
+
.createHash('sha256')
|
|
60
|
+
.update(String(value))
|
|
61
|
+
.digest('hex')}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function remoteRepositoryHashes(value) {
|
|
65
|
+
return remoteIdentityVariants(value).map(identity =>
|
|
66
|
+
sha256(`remote:${identity}`)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function remoteUrlHashes(value) {
|
|
71
|
+
return remoteIdentityVariants(value).map(sha256);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function identitiesIntersect(left, right) {
|
|
75
|
+
const leftSet = new Set(
|
|
76
|
+
(Array.isArray(left) ? left : [left]).filter(Boolean)
|
|
77
|
+
);
|
|
78
|
+
return (Array.isArray(right) ? right : [right]).some(value =>
|
|
79
|
+
leftSet.has(value)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
identitiesIntersect,
|
|
85
|
+
legacyRemoteIdentity,
|
|
86
|
+
remoteIdentityVariants,
|
|
87
|
+
remoteRepositoryHashes,
|
|
88
|
+
remoteUrlHashes,
|
|
89
|
+
};
|
package/lib/release-mainline.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
1
|
const path = require('path');
|
|
3
2
|
const { spawnSync } = require('child_process');
|
|
4
3
|
|
|
5
4
|
const { readGitSourceBase } = require('./change-baseline');
|
|
5
|
+
const {
|
|
6
|
+
identitiesIntersect,
|
|
7
|
+
remoteUrlHashes,
|
|
8
|
+
} = require('./git-remote-identity');
|
|
6
9
|
const { listRuntimeBuildInputChanges } = require('./runtime-lineage');
|
|
7
10
|
|
|
8
11
|
const GIT_OUTPUT_LIMIT = 16 * 1024 * 1024;
|
|
@@ -68,6 +71,7 @@ function prepareReleaseSourceRevision(options = {}) {
|
|
|
68
71
|
worktreeRoot: readGit(cwd, ['rev-parse', '--show-toplevel']).stdout,
|
|
69
72
|
remoteName: mainline.remoteName,
|
|
70
73
|
remoteUrlHash: mainline.remoteUrlHash,
|
|
74
|
+
remoteUrlHashAliases: mainline.remoteUrlHashAliases,
|
|
71
75
|
mainBranch: mainline.branchName,
|
|
72
76
|
mainTipCommit: mainline.tipCommit,
|
|
73
77
|
mainlineSource: mainline.source,
|
|
@@ -82,7 +86,10 @@ function assertReleaseSourceRevisionStable(sourceRevision, options = {}) {
|
|
|
82
86
|
const current = readGitSourceBase(cwd, 'HEAD');
|
|
83
87
|
const dirtyPaths = listRuntimeBuildInputChanges(cwd);
|
|
84
88
|
if (
|
|
85
|
-
|
|
89
|
+
!identitiesIntersect(
|
|
90
|
+
[expected.repo, ...(expected.repoAliases || [])],
|
|
91
|
+
[current.repo, ...(current.repoAliases || [])]
|
|
92
|
+
) ||
|
|
86
93
|
expected.baseCommit !== current.baseCommit ||
|
|
87
94
|
expected.treeHash !== current.treeHash ||
|
|
88
95
|
dirtyPaths.length > 0
|
|
@@ -109,7 +116,12 @@ function inspectReleaseSourceIntegration(sourceRevision, options = {}) {
|
|
|
109
116
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
110
117
|
const source = normalizeSourceRevision(sourceRevision);
|
|
111
118
|
const currentRepository = readGitSourceBase(cwd, 'HEAD');
|
|
112
|
-
if (
|
|
119
|
+
if (
|
|
120
|
+
!identitiesIntersect(
|
|
121
|
+
[source.repo, ...(source.repoAliases || [])],
|
|
122
|
+
[currentRepository.repo, ...(currentRepository.repoAliases || [])]
|
|
123
|
+
)
|
|
124
|
+
) {
|
|
113
125
|
throw releaseMainlineError(
|
|
114
126
|
'RELEASE_SOURCE_REPOSITORY_MISMATCH',
|
|
115
127
|
'冻结的发布提交与当前 Git 仓库不一致,不能验证主分支回合状态'
|
|
@@ -120,7 +132,10 @@ function inspectReleaseSourceIntegration(sourceRevision, options = {}) {
|
|
|
120
132
|
if (
|
|
121
133
|
source.remoteName !== mainline.remoteName ||
|
|
122
134
|
source.mainBranch !== mainline.branchName ||
|
|
123
|
-
|
|
135
|
+
!identitiesIntersect(
|
|
136
|
+
[source.remoteUrlHash, ...(source.remoteUrlHashAliases || [])],
|
|
137
|
+
[mainline.remoteUrlHash, ...(mainline.remoteUrlHashAliases || [])]
|
|
138
|
+
)
|
|
124
139
|
) {
|
|
125
140
|
throw releaseMainlineError(
|
|
126
141
|
'RELEASE_MAINLINE_TARGET_CHANGED',
|
|
@@ -204,7 +219,8 @@ function resolveAuthoritativeMainline(options = {}) {
|
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
function resolveRemoteMainline({ cwd, remoteName }) {
|
|
207
|
-
const
|
|
222
|
+
const remoteUrlHashAliases = readRemoteUrlHashes(cwd, remoteName);
|
|
223
|
+
const remoteUrlHash = remoteUrlHashAliases[0];
|
|
208
224
|
const remoteHead = readGit(
|
|
209
225
|
cwd,
|
|
210
226
|
['ls-remote', '--symref', remoteName, 'HEAD'],
|
|
@@ -217,6 +233,7 @@ function resolveRemoteMainline({ cwd, remoteName }) {
|
|
|
217
233
|
return {
|
|
218
234
|
remoteName,
|
|
219
235
|
remoteUrlHash,
|
|
236
|
+
remoteUrlHashAliases,
|
|
220
237
|
branchName: parsed.branchName,
|
|
221
238
|
tipCommit: parsed.tipCommit,
|
|
222
239
|
source: 'remote-head',
|
|
@@ -236,6 +253,7 @@ function resolveRemoteMainline({ cwd, remoteName }) {
|
|
|
236
253
|
return {
|
|
237
254
|
remoteName,
|
|
238
255
|
remoteUrlHash,
|
|
256
|
+
remoteUrlHashAliases,
|
|
239
257
|
...candidates[0],
|
|
240
258
|
source: 'remote-main-master-fallback',
|
|
241
259
|
};
|
|
@@ -422,6 +440,25 @@ function normalizeSourceRevision(value) {
|
|
|
422
440
|
const remoteName = normalizeOptionalName(value.remoteName, 'remote');
|
|
423
441
|
const mainBranch = String(value.mainBranch || '').trim();
|
|
424
442
|
const remoteUrlHash = String(value.remoteUrlHash || '').trim().toLowerCase();
|
|
443
|
+
const remoteUrlHashAliases = Array.from(
|
|
444
|
+
new Set(
|
|
445
|
+
[
|
|
446
|
+
remoteUrlHash,
|
|
447
|
+
...(Array.isArray(value.remoteUrlHashAliases)
|
|
448
|
+
? value.remoteUrlHashAliases
|
|
449
|
+
: []),
|
|
450
|
+
]
|
|
451
|
+
.map(item => String(item || '').trim().toLowerCase())
|
|
452
|
+
.filter(item => /^sha256:[0-9a-f]{64}$/.test(item))
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
const repoAliases = Array.from(
|
|
456
|
+
new Set(
|
|
457
|
+
[repo, ...(Array.isArray(value.repoAliases) ? value.repoAliases : [])]
|
|
458
|
+
.map(item => String(item || '').trim().toLowerCase())
|
|
459
|
+
.filter(item => /^sha256:[0-9a-f]{64}$/.test(item))
|
|
460
|
+
)
|
|
461
|
+
);
|
|
425
462
|
if (
|
|
426
463
|
!/^sha256:[0-9a-f]{64}$/i.test(repo) ||
|
|
427
464
|
!isObjectId(baseCommit) ||
|
|
@@ -439,15 +476,17 @@ function normalizeSourceRevision(value) {
|
|
|
439
476
|
...value,
|
|
440
477
|
repo,
|
|
441
478
|
repositoryId: repo,
|
|
479
|
+
repoAliases,
|
|
442
480
|
baseCommit,
|
|
443
481
|
treeHash,
|
|
444
482
|
remoteName,
|
|
445
483
|
remoteUrlHash,
|
|
484
|
+
remoteUrlHashAliases,
|
|
446
485
|
mainBranch,
|
|
447
486
|
};
|
|
448
487
|
}
|
|
449
488
|
|
|
450
|
-
function
|
|
489
|
+
function readRemoteUrlHashes(cwd, remoteName) {
|
|
451
490
|
const result = readGit(cwd, ['remote', 'get-url', remoteName]);
|
|
452
491
|
if (!result.stdout) {
|
|
453
492
|
throw releaseMainlineError(
|
|
@@ -455,24 +494,7 @@ function readRemoteUrlHash(cwd, remoteName) {
|
|
|
455
494
|
`无法读取权威远端地址: ${remoteName}`
|
|
456
495
|
);
|
|
457
496
|
}
|
|
458
|
-
|
|
459
|
-
return `sha256:${crypto.createHash('sha256').update(identity).digest('hex')}`;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function normalizeRemoteUrlIdentity(value) {
|
|
463
|
-
const remote = String(value || '').trim();
|
|
464
|
-
try {
|
|
465
|
-
const url = new URL(remote);
|
|
466
|
-
url.username = '';
|
|
467
|
-
url.password = '';
|
|
468
|
-
url.search = '';
|
|
469
|
-
url.hash = '';
|
|
470
|
-
return url.toString();
|
|
471
|
-
} catch {
|
|
472
|
-
return remote
|
|
473
|
-
.replace(/^[^@/]+@([^:]+):/, '$1:')
|
|
474
|
-
.replace(/[?#].*$/, '');
|
|
475
|
-
}
|
|
497
|
+
return remoteUrlHashes(result.stdout);
|
|
476
498
|
}
|
|
477
499
|
|
|
478
500
|
function assertSafeDefaultBranch(remoteName, branchName) {
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
function uniqueSorted(values = []) {
|
|
4
|
+
return Array.from(
|
|
5
|
+
new Set(
|
|
6
|
+
values
|
|
7
|
+
.map(value => String(value || '').trim())
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
)
|
|
10
|
+
).sort();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeReleaseTargets(targets = {}, runtimeMode = 'legacy') {
|
|
14
|
+
const logicalTargets = {
|
|
15
|
+
forms: uniqueSorted(targets.forms || []),
|
|
16
|
+
pages: uniqueSorted(targets.pages || []),
|
|
17
|
+
functions: uniqueSorted(targets.functions || []),
|
|
18
|
+
automations: uniqueSorted(targets.automations || []),
|
|
19
|
+
workflows: uniqueSorted(targets.workflows || []),
|
|
20
|
+
jsCodeNodes: uniqueSorted(targets.jsCodeNodes || []),
|
|
21
|
+
resources: Boolean(targets.resources),
|
|
22
|
+
runtime: Boolean(targets.runtime),
|
|
23
|
+
other: uniqueSorted(targets.other || []),
|
|
24
|
+
};
|
|
25
|
+
const activationTargets = {
|
|
26
|
+
...logicalTargets,
|
|
27
|
+
pages:
|
|
28
|
+
runtimeMode === 'react-spa'
|
|
29
|
+
? []
|
|
30
|
+
: logicalTargets.pages,
|
|
31
|
+
runtime:
|
|
32
|
+
runtimeMode === 'react-spa'
|
|
33
|
+
? logicalTargets.runtime || logicalTargets.pages.length > 0
|
|
34
|
+
: logicalTargets.runtime,
|
|
35
|
+
};
|
|
36
|
+
return { logicalTargets, activationTargets };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildTargetedResourceReleaseCommands(
|
|
40
|
+
targets,
|
|
41
|
+
profileArg,
|
|
42
|
+
changeId
|
|
43
|
+
) {
|
|
44
|
+
return buildTargetedResourceReleaseSteps(
|
|
45
|
+
targets,
|
|
46
|
+
profileArg,
|
|
47
|
+
changeId
|
|
48
|
+
).map(step => step.command);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function commandFromArgs(args) {
|
|
52
|
+
return ['openxiangda', ...args]
|
|
53
|
+
.map(value => {
|
|
54
|
+
const text = String(value);
|
|
55
|
+
return /^[A-Za-z0-9_./,:@<>=+-]+$/.test(text)
|
|
56
|
+
? text
|
|
57
|
+
: `'${text.replace(/'/g, `'\\''`)}'`;
|
|
58
|
+
})
|
|
59
|
+
.join(' ');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createStep(id, args, options = {}) {
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
args,
|
|
66
|
+
command: commandFromArgs(args),
|
|
67
|
+
writes: options.writes !== false,
|
|
68
|
+
stagedKind: options.stagedKind || null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function appendProfileAndChange(args, profileArg, changeId) {
|
|
73
|
+
args.push('--profile', profileArg);
|
|
74
|
+
if (changeId) args.push('--change', changeId);
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildTargetedResourceReleaseSteps(
|
|
79
|
+
targets,
|
|
80
|
+
profileArg,
|
|
81
|
+
changeId
|
|
82
|
+
) {
|
|
83
|
+
const steps = [];
|
|
84
|
+
const functions = targets.functions || [];
|
|
85
|
+
const automations = targets.automations || [];
|
|
86
|
+
if (functions.length > 0 || automations.length > 0) {
|
|
87
|
+
const resourceTypes = [
|
|
88
|
+
...(functions.length > 0 ? ['function'] : []),
|
|
89
|
+
...(automations.length > 0 ? ['automation'] : []),
|
|
90
|
+
];
|
|
91
|
+
const qualify = resourceTypes.length > 1;
|
|
92
|
+
const selectors = [
|
|
93
|
+
...functions.map(code => (qualify ? `function:${code}` : code)),
|
|
94
|
+
...automations.map(code => (qualify ? `automation:${code}` : code)),
|
|
95
|
+
];
|
|
96
|
+
steps.push(
|
|
97
|
+
createStep(
|
|
98
|
+
'backend-stage',
|
|
99
|
+
appendProfileAndChange(
|
|
100
|
+
[
|
|
101
|
+
'resource',
|
|
102
|
+
'publish',
|
|
103
|
+
resourceTypes.join(','),
|
|
104
|
+
'--only',
|
|
105
|
+
selectors.join(','),
|
|
106
|
+
'--stage-only',
|
|
107
|
+
],
|
|
108
|
+
profileArg,
|
|
109
|
+
changeId
|
|
110
|
+
),
|
|
111
|
+
{ stagedKind: 'BackendRelease' }
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const workflows = targets.workflows || [];
|
|
116
|
+
if (workflows.length > 0) {
|
|
117
|
+
steps.push(
|
|
118
|
+
createStep(
|
|
119
|
+
'workflow-publish',
|
|
120
|
+
appendProfileAndChange(
|
|
121
|
+
[
|
|
122
|
+
'resource',
|
|
123
|
+
'publish',
|
|
124
|
+
'workflow',
|
|
125
|
+
'--only',
|
|
126
|
+
workflows.join(','),
|
|
127
|
+
],
|
|
128
|
+
profileArg,
|
|
129
|
+
changeId
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return steps;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildWorkspaceReleaseCommands(
|
|
138
|
+
targetsInput,
|
|
139
|
+
runtimeMode,
|
|
140
|
+
profile,
|
|
141
|
+
changeId
|
|
142
|
+
) {
|
|
143
|
+
return buildWorkspaceReleaseSteps(
|
|
144
|
+
targetsInput,
|
|
145
|
+
runtimeMode,
|
|
146
|
+
profile,
|
|
147
|
+
changeId
|
|
148
|
+
).map(step => step.command);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildWorkspaceReleaseSteps(
|
|
152
|
+
targetsInput,
|
|
153
|
+
runtimeMode,
|
|
154
|
+
profile,
|
|
155
|
+
changeId
|
|
156
|
+
) {
|
|
157
|
+
const profileArg = profile || '<profile>';
|
|
158
|
+
const { activationTargets: targets } = normalizeReleaseTargets(
|
|
159
|
+
targetsInput,
|
|
160
|
+
runtimeMode
|
|
161
|
+
);
|
|
162
|
+
const steps = [];
|
|
163
|
+
const hasResourceTargets =
|
|
164
|
+
Boolean(targets.resources) ||
|
|
165
|
+
targets.functions.length > 0 ||
|
|
166
|
+
targets.automations.length > 0 ||
|
|
167
|
+
targets.workflows.length > 0 ||
|
|
168
|
+
targets.jsCodeNodes.length > 0;
|
|
169
|
+
if (runtimeMode === 'react-spa') {
|
|
170
|
+
if (targets.forms.length > 0) {
|
|
171
|
+
steps.push(
|
|
172
|
+
createStep(
|
|
173
|
+
'form-stage',
|
|
174
|
+
appendProfileAndChange(
|
|
175
|
+
[
|
|
176
|
+
'resource',
|
|
177
|
+
'publish',
|
|
178
|
+
'form-setting',
|
|
179
|
+
'--only',
|
|
180
|
+
targets.forms.join(','),
|
|
181
|
+
],
|
|
182
|
+
profileArg,
|
|
183
|
+
changeId
|
|
184
|
+
),
|
|
185
|
+
{ stagedKind: 'FormRelease' }
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (hasResourceTargets) {
|
|
190
|
+
steps.push(
|
|
191
|
+
...buildTargetedResourceReleaseSteps(
|
|
192
|
+
targets,
|
|
193
|
+
profileArg,
|
|
194
|
+
changeId
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (targets.runtime) {
|
|
199
|
+
steps.push(
|
|
200
|
+
createStep(
|
|
201
|
+
'runtime-stage',
|
|
202
|
+
appendProfileAndChange(
|
|
203
|
+
['runtime', 'deploy', '--no-activate'],
|
|
204
|
+
profileArg,
|
|
205
|
+
changeId
|
|
206
|
+
),
|
|
207
|
+
{ stagedKind: 'RuntimeRelease' }
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
targets.forms.length > 0 ||
|
|
213
|
+
targets.functions.length > 0 ||
|
|
214
|
+
targets.automations.length > 0 ||
|
|
215
|
+
targets.runtime
|
|
216
|
+
) {
|
|
217
|
+
steps.push(
|
|
218
|
+
createStep(
|
|
219
|
+
'app-finalize',
|
|
220
|
+
appendProfileAndChange(
|
|
221
|
+
[
|
|
222
|
+
'release',
|
|
223
|
+
'app-finalize',
|
|
224
|
+
'--staged-resources-json',
|
|
225
|
+
`.openxiangda/releases/${changeId || 'change'}/staged-resources.json`,
|
|
226
|
+
],
|
|
227
|
+
profileArg,
|
|
228
|
+
changeId
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return steps;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const onlyTargets = [
|
|
237
|
+
...targets.forms.map(code => `forms/${code}`),
|
|
238
|
+
...targets.pages.map(code => `pages/${code}`),
|
|
239
|
+
];
|
|
240
|
+
if (onlyTargets.length > 0) {
|
|
241
|
+
steps.push(
|
|
242
|
+
createStep(
|
|
243
|
+
'workspace-publish',
|
|
244
|
+
appendProfileAndChange(
|
|
245
|
+
[
|
|
246
|
+
'workspace',
|
|
247
|
+
'publish',
|
|
248
|
+
'--only',
|
|
249
|
+
onlyTargets.join(','),
|
|
250
|
+
'--skip-resources',
|
|
251
|
+
],
|
|
252
|
+
profileArg,
|
|
253
|
+
changeId
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (hasResourceTargets) {
|
|
259
|
+
steps.push(
|
|
260
|
+
...buildTargetedResourceReleaseSteps(targets, profileArg, changeId)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return steps;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function releasePlanHash(value) {
|
|
267
|
+
return crypto
|
|
268
|
+
.createHash('sha256')
|
|
269
|
+
.update(canonicalJson(value))
|
|
270
|
+
.digest('hex');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function canonicalJson(value) {
|
|
274
|
+
if (Array.isArray(value)) {
|
|
275
|
+
return `[${value.map(item => canonicalJson(item)).join(',')}]`;
|
|
276
|
+
}
|
|
277
|
+
if (value && typeof value === 'object') {
|
|
278
|
+
return `{${Object.keys(value)
|
|
279
|
+
.sort()
|
|
280
|
+
.map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
281
|
+
.join(',')}}`;
|
|
282
|
+
}
|
|
283
|
+
return JSON.stringify(value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
buildTargetedResourceReleaseCommands,
|
|
288
|
+
buildTargetedResourceReleaseSteps,
|
|
289
|
+
buildWorkspaceReleaseCommands,
|
|
290
|
+
buildWorkspaceReleaseSteps,
|
|
291
|
+
commandFromArgs,
|
|
292
|
+
normalizeReleaseTargets,
|
|
293
|
+
releasePlanHash,
|
|
294
|
+
};
|