openxiangda 1.0.190 → 1.0.192
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/lib/application-environments.js +24 -0
- package/lib/cli.js +228 -18
- package/lib/developer-center.js +6 -3
- package/package.json +2 -1
|
@@ -401,10 +401,34 @@ function withReleaseClientSessionArgs(args = [], flags = {}) {
|
|
|
401
401
|
: [...args];
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
function isRecoverablePostActivationDeploymentError(error, deployment) {
|
|
405
|
+
if (
|
|
406
|
+
deployment?.status !== 'deployed' ||
|
|
407
|
+
!String(deployment?.targetAppReleaseId || '').trim()
|
|
408
|
+
) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
const detail = [
|
|
412
|
+
error?.code,
|
|
413
|
+
error?.message,
|
|
414
|
+
error?.publishLeaseCleanupError?.code,
|
|
415
|
+
error?.publishLeaseCleanupError?.message,
|
|
416
|
+
]
|
|
417
|
+
.filter(Boolean)
|
|
418
|
+
.join(' ');
|
|
419
|
+
return (
|
|
420
|
+
/(publish[\s_-]*lease|publishLease|发布租约)/i.test(detail) &&
|
|
421
|
+
/(concurr|conflict|cleanup|release[\s_-]*end|并发冲突|清理|释放)/i.test(
|
|
422
|
+
detail
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
404
427
|
module.exports = {
|
|
405
428
|
bindEnvironmentTarget,
|
|
406
429
|
buildCandidateBundle,
|
|
407
430
|
canonicalJson,
|
|
431
|
+
isRecoverablePostActivationDeploymentError,
|
|
408
432
|
normalizeEnvironmentKind,
|
|
409
433
|
normalizeManagedChangeSourceBase,
|
|
410
434
|
normalizeManagedReleaseSourceRevision,
|
package/lib/cli.js
CHANGED
|
@@ -130,6 +130,7 @@ const { buildDesignReview, renderDesignReview } = require('./design-review');
|
|
|
130
130
|
const {
|
|
131
131
|
bindEnvironmentTarget,
|
|
132
132
|
buildCandidateBundle,
|
|
133
|
+
isRecoverablePostActivationDeploymentError,
|
|
133
134
|
normalizeEnvironmentKind,
|
|
134
135
|
normalizeManagedChangeSourceBase,
|
|
135
136
|
normalizeManagedReleaseSourceRevision,
|
|
@@ -1300,9 +1301,9 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1300
1301
|
}
|
|
1301
1302
|
}
|
|
1302
1303
|
|
|
1303
|
-
function readReleaseRecoveryContext(target, changeId) {
|
|
1304
|
+
function readReleaseRecoveryContext(target, changeId, deploymentId) {
|
|
1304
1305
|
if (!changeId) return null;
|
|
1305
|
-
const execution = readReleaseExecution(changeId);
|
|
1306
|
+
const execution = readReleaseExecution(changeId, deploymentId);
|
|
1306
1307
|
const context = execution?.releaseContext;
|
|
1307
1308
|
if (!context || typeof context !== 'object' || Array.isArray(context)) {
|
|
1308
1309
|
return null;
|
|
@@ -2142,14 +2143,120 @@ function buildEnvironmentStatusDiff(status) {
|
|
|
2142
2143
|
};
|
|
2143
2144
|
}
|
|
2144
2145
|
|
|
2145
|
-
function readStudioGit(args) {
|
|
2146
|
+
function readStudioGit(args, cwd = process.cwd()) {
|
|
2146
2147
|
const result = spawnSync('git', args, {
|
|
2147
|
-
cwd
|
|
2148
|
+
cwd,
|
|
2148
2149
|
encoding: 'utf8',
|
|
2150
|
+
timeout: 5000,
|
|
2149
2151
|
});
|
|
2150
2152
|
return result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
2151
2153
|
}
|
|
2152
2154
|
|
|
2155
|
+
function studioGitSnapshot() {
|
|
2156
|
+
const porcelain = readStudioGit([
|
|
2157
|
+
'status',
|
|
2158
|
+
'--porcelain',
|
|
2159
|
+
'--untracked-files=all',
|
|
2160
|
+
]);
|
|
2161
|
+
const branch = readStudioGit(['branch', '--show-current']);
|
|
2162
|
+
const commit = readStudioGit(['rev-parse', 'HEAD']);
|
|
2163
|
+
const upstream = readStudioGit([
|
|
2164
|
+
'rev-parse',
|
|
2165
|
+
'--abbrev-ref',
|
|
2166
|
+
'--symbolic-full-name',
|
|
2167
|
+
'@{upstream}',
|
|
2168
|
+
]);
|
|
2169
|
+
const upstreamCommit = upstream
|
|
2170
|
+
? readStudioGit(['rev-parse', upstream])
|
|
2171
|
+
: '';
|
|
2172
|
+
const upstreamParts = upstream.split('/');
|
|
2173
|
+
const remoteName = upstreamParts.length > 1 ? upstreamParts[0] : '';
|
|
2174
|
+
const upstreamBranch = upstreamParts.slice(1).join('/');
|
|
2175
|
+
const remoteHeadCommit =
|
|
2176
|
+
remoteName && upstreamBranch
|
|
2177
|
+
? readStudioGit([
|
|
2178
|
+
'ls-remote',
|
|
2179
|
+
'--heads',
|
|
2180
|
+
remoteName,
|
|
2181
|
+
`refs/heads/${upstreamBranch}`,
|
|
2182
|
+
]).split(/\s+/)[0] || ''
|
|
2183
|
+
: '';
|
|
2184
|
+
const aheadBehind = upstream
|
|
2185
|
+
? readStudioGit([
|
|
2186
|
+
'rev-list',
|
|
2187
|
+
'--left-right',
|
|
2188
|
+
'--count',
|
|
2189
|
+
`${upstream}...HEAD`,
|
|
2190
|
+
])
|
|
2191
|
+
.split(/\s+/)
|
|
2192
|
+
.map(value => Number(value))
|
|
2193
|
+
: [];
|
|
2194
|
+
const worktreePaths = readStudioGit(['worktree', 'list', '--porcelain'])
|
|
2195
|
+
.split(/\r?\n/)
|
|
2196
|
+
.filter(line => line.startsWith('worktree '))
|
|
2197
|
+
.map(line => line.slice('worktree '.length))
|
|
2198
|
+
.filter(Boolean);
|
|
2199
|
+
const dirtyWorktrees = worktreePaths
|
|
2200
|
+
.filter(worktreePath => path.resolve(worktreePath) !== process.cwd())
|
|
2201
|
+
.filter(worktreePath =>
|
|
2202
|
+
Boolean(
|
|
2203
|
+
readStudioGit(
|
|
2204
|
+
['status', '--porcelain', '--untracked-files=all'],
|
|
2205
|
+
worktreePath
|
|
2206
|
+
)
|
|
2207
|
+
)
|
|
2208
|
+
);
|
|
2209
|
+
const ahead = Number.isFinite(aheadBehind[1]) ? aheadBehind[1] : null;
|
|
2210
|
+
const behind = Number.isFinite(aheadBehind[0]) ? aheadBehind[0] : null;
|
|
2211
|
+
const authoritativeBranch = ['main', 'master'].includes(branch);
|
|
2212
|
+
const upstreamMainline = /\/(?:main|master)$/.test(upstream);
|
|
2213
|
+
const clean = !porcelain;
|
|
2214
|
+
const mainlineAligned =
|
|
2215
|
+
clean &&
|
|
2216
|
+
authoritativeBranch &&
|
|
2217
|
+
upstreamMainline &&
|
|
2218
|
+
commit === upstreamCommit &&
|
|
2219
|
+
commit === remoteHeadCommit &&
|
|
2220
|
+
ahead === 0 &&
|
|
2221
|
+
behind === 0 &&
|
|
2222
|
+
dirtyWorktrees.length === 0;
|
|
2223
|
+
return {
|
|
2224
|
+
branch,
|
|
2225
|
+
commit,
|
|
2226
|
+
upstream: upstream || null,
|
|
2227
|
+
upstreamCommit: upstreamCommit || null,
|
|
2228
|
+
remoteHeadCommit: remoteHeadCommit || null,
|
|
2229
|
+
ahead,
|
|
2230
|
+
behind,
|
|
2231
|
+
clean,
|
|
2232
|
+
mainlineAligned,
|
|
2233
|
+
dirtyWorktrees,
|
|
2234
|
+
worktreeCount: worktreePaths.length,
|
|
2235
|
+
changes: porcelain ? porcelain.split(/\r?\n/).slice(0, 50) : [],
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function isProductionCommissioning(environment) {
|
|
2240
|
+
const policy = environment?.sideEffectPolicy || {};
|
|
2241
|
+
return (
|
|
2242
|
+
policy.payments === 'deny' &&
|
|
2243
|
+
policy.notifications === 'tester_allowlist' &&
|
|
2244
|
+
policy.externalWrites === 'deny' &&
|
|
2245
|
+
policy.publicIndexing === 'deny' &&
|
|
2246
|
+
policy.organizationWrites === 'deny' &&
|
|
2247
|
+
policy.scheduledAutomations === 'disabled' &&
|
|
2248
|
+
policy.environmentBanner === true
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function isDeploymentEvidenceValid(deployment, now = Date.now()) {
|
|
2253
|
+
if (!deployment?.evidenceHash) return false;
|
|
2254
|
+
const evidence = deployment.evidenceSummary;
|
|
2255
|
+
if (!evidence || evidence.outcome !== 'passed') return false;
|
|
2256
|
+
const validUntil = Date.parse(evidence.validUntil || '');
|
|
2257
|
+
return Number.isFinite(validUntil) && validUntil > now;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2153
2260
|
async function studio(args) {
|
|
2154
2261
|
const { flags } = parseArgs(args);
|
|
2155
2262
|
const config = loadConfig();
|
|
@@ -2165,26 +2272,69 @@ async function studio(args) {
|
|
|
2165
2272
|
state,
|
|
2166
2273
|
state.currentTarget || 'preproduction'
|
|
2167
2274
|
);
|
|
2168
|
-
const
|
|
2275
|
+
const remoteStatus = await requestWithAuth(
|
|
2169
2276
|
config,
|
|
2170
2277
|
selected.binding.profile,
|
|
2171
2278
|
environmentSetApiPath(state.logicalApp.code, '/status')
|
|
2172
2279
|
);
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2280
|
+
const environments = await Promise.all(
|
|
2281
|
+
(remoteStatus?.environments || []).map(async environment => {
|
|
2282
|
+
const deploymentId = environment?.latestDeployment?.id;
|
|
2283
|
+
if (!deploymentId) return environment;
|
|
2284
|
+
try {
|
|
2285
|
+
const latestDeployment = await requestWithAuth(
|
|
2286
|
+
config,
|
|
2287
|
+
selected.binding.profile,
|
|
2288
|
+
applicationDeploymentApiPath(
|
|
2289
|
+
state.logicalApp.code,
|
|
2290
|
+
deploymentId
|
|
2291
|
+
)
|
|
2292
|
+
);
|
|
2293
|
+
return { ...environment, latestDeployment };
|
|
2294
|
+
} catch {
|
|
2295
|
+
return environment;
|
|
2296
|
+
}
|
|
2297
|
+
})
|
|
2298
|
+
);
|
|
2299
|
+
const remote = { ...remoteStatus, environments };
|
|
2300
|
+
const git = studioGitSnapshot();
|
|
2301
|
+
const preproduction = environments.find(
|
|
2302
|
+
environment => environment.kind === 'preproduction'
|
|
2303
|
+
);
|
|
2304
|
+
const production = environments.find(
|
|
2305
|
+
environment => environment.kind === 'production'
|
|
2306
|
+
);
|
|
2307
|
+
const drift = buildEnvironmentStatusDiff(remote).drift;
|
|
2308
|
+
const evidenceValid = isDeploymentEvidenceValid(
|
|
2309
|
+
preproduction?.latestDeployment
|
|
2310
|
+
);
|
|
2311
|
+
const productionCommissioning =
|
|
2312
|
+
isProductionCommissioning(production);
|
|
2313
|
+
const candidateReady = git.mainlineAligned;
|
|
2314
|
+
const testRegistrationReady = Boolean(
|
|
2315
|
+
preproduction?.latestDeployment?.id &&
|
|
2316
|
+
preproduction.latestDeployment.status !== 'failed'
|
|
2317
|
+
);
|
|
2318
|
+
const promotionReady =
|
|
2319
|
+
candidateReady &&
|
|
2320
|
+
productionCommissioning &&
|
|
2321
|
+
evidenceValid &&
|
|
2322
|
+
preproduction?.latestDeployment?.status === 'succeeded' &&
|
|
2323
|
+
Boolean(preproduction.latestDeployment.candidateId);
|
|
2178
2324
|
return {
|
|
2179
2325
|
workspace: process.cwd(),
|
|
2180
2326
|
logicalApp: state.logicalApp,
|
|
2181
2327
|
currentTarget: state.currentTarget,
|
|
2182
2328
|
targets: state.targets,
|
|
2183
|
-
git
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2329
|
+
git,
|
|
2330
|
+
drift,
|
|
2331
|
+
deliveryGates: {
|
|
2332
|
+
candidateReady,
|
|
2333
|
+
evidenceValid,
|
|
2334
|
+
productionCommissioning,
|
|
2335
|
+
promotionReady,
|
|
2336
|
+
rollbackReady: Boolean(production?.heads?.appReleaseId),
|
|
2337
|
+
testRegistrationReady,
|
|
2188
2338
|
},
|
|
2189
2339
|
openxiangdaVersion: CURRENT_VERSION,
|
|
2190
2340
|
remote,
|
|
@@ -2493,6 +2643,33 @@ async function deployApplicationCandidate(
|
|
|
2493
2643
|
rememberTargetDeployment(target, detail);
|
|
2494
2644
|
return { deployment: detail, published };
|
|
2495
2645
|
} catch (error) {
|
|
2646
|
+
let activatedDeployment = null;
|
|
2647
|
+
try {
|
|
2648
|
+
activatedDeployment = await requestWithAuth(
|
|
2649
|
+
config,
|
|
2650
|
+
target.profileName,
|
|
2651
|
+
applicationDeploymentApiPath(logicalApp.code, deployment.id)
|
|
2652
|
+
);
|
|
2653
|
+
} catch {
|
|
2654
|
+
// Preserve the primary deployment error when the read-only check fails.
|
|
2655
|
+
}
|
|
2656
|
+
if (
|
|
2657
|
+
isRecoverablePostActivationDeploymentError(error, activatedDeployment)
|
|
2658
|
+
) {
|
|
2659
|
+
rememberTargetDeployment(target, activatedDeployment);
|
|
2660
|
+
warn(
|
|
2661
|
+
`AppRelease 已激活,发布租约清理发生并发冲突;部署 ${deployment.id} 保持 deployed,请执行 release status/end 完成只读回合清理。`
|
|
2662
|
+
);
|
|
2663
|
+
return {
|
|
2664
|
+
deployment: activatedDeployment,
|
|
2665
|
+
published: null,
|
|
2666
|
+
recoveredAfterActivation: true,
|
|
2667
|
+
postActivationError: {
|
|
2668
|
+
code: error?.code || 'PUBLISH_LEASE_CLEANUP_CONFLICT',
|
|
2669
|
+
message: maskText(error?.message || String(error)),
|
|
2670
|
+
},
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2496
2673
|
await markApplicationDeploymentFailed(
|
|
2497
2674
|
config,
|
|
2498
2675
|
target,
|
|
@@ -2549,6 +2726,35 @@ async function runApplicationEnvironmentReleaseCommand(
|
|
|
2549
2726
|
);
|
|
2550
2727
|
}
|
|
2551
2728
|
|
|
2729
|
+
if (subcommand === 'reconcile') {
|
|
2730
|
+
const deploymentId =
|
|
2731
|
+
readStringFlag(flags, 'deployment') ||
|
|
2732
|
+
readStringFlag(flags, 'deployment-id') ||
|
|
2733
|
+
positional[0] ||
|
|
2734
|
+
target.bound.lastDeploymentId;
|
|
2735
|
+
const reason = readStringFlag(flags, 'reason');
|
|
2736
|
+
if (!deploymentId || reason.length < 8) {
|
|
2737
|
+
fail(
|
|
2738
|
+
'release reconcile 必须提供 --deployment <id> 和至少 8 个字符的 --reason'
|
|
2739
|
+
);
|
|
2740
|
+
}
|
|
2741
|
+
const reconciled = await requestWithAuth(
|
|
2742
|
+
config,
|
|
2743
|
+
target.profileName,
|
|
2744
|
+
applicationDeploymentApiPath(
|
|
2745
|
+
logicalApp.code,
|
|
2746
|
+
deploymentId,
|
|
2747
|
+
'reconcile'
|
|
2748
|
+
),
|
|
2749
|
+
{
|
|
2750
|
+
method: 'POST',
|
|
2751
|
+
body: { reason },
|
|
2752
|
+
}
|
|
2753
|
+
);
|
|
2754
|
+
rememberTargetDeployment(target, reconciled);
|
|
2755
|
+
return { deployment: reconciled, reconciled: true };
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2552
2758
|
const candidateId =
|
|
2553
2759
|
readStringFlag(flags, 'candidate') ||
|
|
2554
2760
|
readStringFlag(flags, 'candidate-id') ||
|
|
@@ -2682,7 +2888,7 @@ async function release(args) {
|
|
|
2682
2888
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2683
2889
|
print([
|
|
2684
2890
|
'用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
2685
|
-
' openxiangda release candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--confirm-production] [--json]',
|
|
2891
|
+
' openxiangda release candidate|deploy|reconcile|test|promote|rollback [--candidate id] [--environment target] [--confirm-production] [--json]',
|
|
2686
2892
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
2687
2893
|
' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
|
|
2688
2894
|
'常用流程:',
|
|
@@ -2720,7 +2926,7 @@ async function release(args) {
|
|
|
2720
2926
|
flags
|
|
2721
2927
|
);
|
|
2722
2928
|
if (
|
|
2723
|
-
['candidate', 'deploy', 'test', 'promote', 'rollback'].includes(
|
|
2929
|
+
['candidate', 'deploy', 'reconcile', 'test', 'promote', 'rollback'].includes(
|
|
2724
2930
|
subcommand
|
|
2725
2931
|
)
|
|
2726
2932
|
) {
|
|
@@ -2853,7 +3059,11 @@ async function release(args) {
|
|
|
2853
3059
|
access: 'reconciliation-read',
|
|
2854
3060
|
});
|
|
2855
3061
|
const changeId = positional[0] || readStringFlag(flags, 'change');
|
|
2856
|
-
const recovered = readReleaseRecoveryContext(
|
|
3062
|
+
const recovered = readReleaseRecoveryContext(
|
|
3063
|
+
target,
|
|
3064
|
+
changeId,
|
|
3065
|
+
readStringFlag(flags, 'deployment-id')
|
|
3066
|
+
);
|
|
2857
3067
|
const sourceRevision =
|
|
2858
3068
|
baseline?.releaseSourceRevision ||
|
|
2859
3069
|
recovered?.context?.releaseSourceRevision;
|
package/lib/developer-center.js
CHANGED
|
@@ -60,7 +60,7 @@ function studioHtml(sessionToken) {
|
|
|
60
60
|
.pre{--accent:var(--pre)}.prod{--accent:var(--prod)}.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px}
|
|
61
61
|
h2{font-size:19px;margin:0}.badge{border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line));color:var(--accent);padding:5px 9px;border-radius:999px;font-size:11px;letter-spacing:.08em}
|
|
62
62
|
dl{display:grid;grid-template-columns:130px 1fr;gap:11px;margin:0;font-size:13px}dt{color:var(--muted)}dd{margin:0;font-family:ui-monospace,SFMono-Regular,monospace;overflow-wrap:anywhere}
|
|
63
|
-
.wide{grid-column:1/-1}.toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}button{appearance:none;border:1px solid #3a3d36;background:#22241f;color:#f4f4ef;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer}
|
|
63
|
+
.wide{grid-column:1/-1}.toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}.gate-ok{color:var(--prod)}.gate-bad{color:var(--bad)}button{appearance:none;border:1px solid #3a3d36;background:#22241f;color:#f4f4ef;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer}
|
|
64
64
|
button:hover{border-color:#777c70}button.primary{background:#e9efe9;color:#111;border-color:#e9efe9}button.danger{color:#ffaaa3;border-color:#70423e}
|
|
65
65
|
button:disabled{opacity:.4;cursor:not-allowed}.status{display:flex;gap:8px;align-items:center;color:var(--muted);font-size:13px}.dot{width:8px;height:8px;border-radius:50%;background:var(--prod);box-shadow:0 0 14px var(--prod)}
|
|
66
66
|
pre{white-space:pre-wrap;word-break:break-word;background:#10110f;border:1px solid #252722;border-radius:12px;padding:16px;color:#c7cbc2;max-height:300px;overflow:auto;font:12px/1.55 ui-monospace,SFMono-Regular,monospace}
|
|
@@ -76,6 +76,7 @@ function studioHtml(sessionToken) {
|
|
|
76
76
|
<section class="grid">
|
|
77
77
|
<article class="card env pre"><div class="card-head"><h2>预发环境</h2><span class="badge">PREPRODUCTION</span></div><dl id="pre"></dl></article>
|
|
78
78
|
<article class="card env prod"><div class="card-head"><h2>正式环境</h2><span class="badge">PRODUCTION</span></div><dl id="prod"></dl></article>
|
|
79
|
+
<article class="card wide"><div class="card-head"><h2>交付门禁与环境差异</h2><span class="badge" id="gate-badge">CHECKING</span></div><dl id="gates"></dl></article>
|
|
79
80
|
<article class="card wide"><div class="card-head"><h2>交付状态</h2><div class="status"><span class="dot"></span><span id="health">连接中</span></div></div><div class="toolbar">
|
|
80
81
|
<button data-action="candidate">生成候选</button><button data-action="deploy">部署预发</button><button data-action="test">登记测试证据</button><button class="primary" data-action="promote">晋级正式</button><button class="danger" data-action="rollback">准备回退</button><button data-action="refresh">刷新</button>
|
|
81
82
|
</div><pre id="output">Developer Center 只监听 127.0.0.1;所有动作复用 OpenXiangda CLI 门禁。</pre></article>
|
|
@@ -87,9 +88,11 @@ const token=${token}; history.replaceState(null,"",location.pathname);
|
|
|
87
88
|
const out=document.querySelector("#output"), dialog=document.querySelector("#dialog");
|
|
88
89
|
let status=null, pending=null;
|
|
89
90
|
const esc=v=>String(v??"-").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\\"":""","'":"'"}[c]));
|
|
90
|
-
const rows=e=>[["appType",e?.appType],["公开地址",e?.publicOrigin],["AppRelease",e?.heads?.appReleaseId],["候选版本",e?.latestDeployment?.candidateId],["部署状态",e?.latestDeployment?.status],["副作用策略",JSON.stringify(e?.sideEffectPolicy||{})]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${esc(v)}</dd>\`).join("");
|
|
91
|
+
const rows=e=>[["appType",e?.appType],["环境修订",e?.revision],["公开地址",e?.publicOrigin],["AppRelease",e?.heads?.appReleaseId],["RuntimeRelease",e?.heads?.runtimeReleaseId],["BackendRelease",e?.heads?.backendReleaseId],["PageRelease",e?.heads?.pageReleaseId],["WorkflowRelease",e?.heads?.workflowReleaseId],["候选版本",e?.latestDeployment?.candidateId],["Deployment",e?.latestDeployment?.id],["部署状态",e?.latestDeployment?.status],["测试证据",e?.latestDeployment?.evidenceHash],["证据有效期",e?.latestDeployment?.evidenceSummary?.validUntil],["副作用策略",JSON.stringify(e?.sideEffectPolicy||{})]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${esc(v)}</dd>\`).join("");
|
|
92
|
+
const bool=v=>\`<span class="\${v?"gate-ok":"gate-bad"}">\${v?"通过":"阻断"}</span>\`;
|
|
93
|
+
const gateRows=s=>[["权威主线",s?.git?.upstream||"-"],["远端主线提交",s?.git?.remoteHeadCommit],["ahead / behind",\`\${s?.git?.ahead??"-"} / \${s?.git?.behind??"-"}\`],["工作区干净",bool(Boolean(s?.git?.clean))],["其他脏 worktree",s?.git?.dirtyWorktrees?.length?s.git.dirtyWorktrees.join(", "):bool(true)],["Candidate 门禁",bool(Boolean(s?.deliveryGates?.candidateReady))],["预发测试证据",bool(Boolean(s?.deliveryGates?.evidenceValid))],["正式 commissioning",bool(Boolean(s?.deliveryGates?.productionCommissioning))],["同一 AppRelease",bool(Boolean(s?.drift?.sameAppRelease))],["资源差异",s?.drift?.sameAppRelease?"无":"存在(预发/正式 Release Head 不同)"],["OpenXiangda",s?.openxiangdaVersion]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${typeof v==="string"&&v.startsWith("<span")?v:esc(v)}</dd>\`).join("");
|
|
91
94
|
async function api(path,options={}){const r=await fetch(path,{...options,headers:{"content-type":"application/json","x-openxiangda-studio-token":token,...options.headers}});const j=await r.json();if(!r.ok)throw new Error(j.message||"request failed");return j}
|
|
92
|
-
async function refresh(){try{status=await api("/api/status");document.querySelector("#repo").textContent=\`\${status.git?.branch||"-"} @ \${(status.git?.commit||"-").slice(0,12)} · \${status.git?.clean?"clean":"dirty"}\`;document.querySelector("#pre").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="preproduction"));document.querySelector("#prod").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="production"));document.querySelector("#health").textContent="状态已同步";}catch(e){document.querySelector("#health").textContent="读取失败";out.textContent=e.message}}
|
|
95
|
+
async function refresh(){try{status=await api("/api/status");document.querySelector("#repo").textContent=\`\${status.git?.branch||"-"} @ \${(status.git?.commit||"-").slice(0,12)} · \${status.git?.mainlineAligned?"mainline ready":status.git?.clean?"clean / not aligned":"dirty"}\`;document.querySelector("#pre").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="preproduction"));document.querySelector("#prod").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="production"));document.querySelector("#gates").innerHTML=gateRows(status);const ready=Boolean(status.deliveryGates?.candidateReady);const badge=document.querySelector("#gate-badge");badge.textContent=ready?"READY":"BLOCKED";badge.className=\`badge \${ready?"gate-ok":"gate-bad"}\`;document.querySelector('[data-action="candidate"]').disabled=!ready;document.querySelector('[data-action="deploy"]').disabled=!ready;document.querySelector('[data-action="test"]').disabled=!status.deliveryGates?.testRegistrationReady;document.querySelector('[data-action="promote"]').disabled=!status.deliveryGates?.promotionReady;document.querySelector('[data-action="rollback"]').disabled=!status.deliveryGates?.rollbackReady;document.querySelector("#health").textContent="状态已同步";}catch(e){document.querySelector("#health").textContent="读取失败";out.textContent=e.message}}
|
|
93
96
|
document.querySelectorAll("[data-action]").forEach(b=>b.onclick=()=>{pending=b.dataset.action;if(pending==="refresh")return refresh();document.querySelector("#dialog-title").textContent=b.textContent;document.querySelector("#value").value="";document.querySelector("#details").value=pending==="test"?JSON.stringify({outcome:"passed",requiredGates:["static","schema","appFunctions","roles","browser","lifecycle","cleanup"],results:{static:{status:"passed"},schema:{status:"passed"},appFunctions:{status:"passed"},roles:{status:"passed"},browser:{status:"passed"},lifecycle:{status:"passed"},cleanup:{status:"passed"}},cleanup:{passed:true,residueCount:0}},null,2):"";dialog.showModal()});
|
|
94
97
|
document.querySelector("#confirm").onclick=async e=>{e.preventDefault();const action=pending;dialog.close();out.textContent="执行中…";try{const result=await api("/api/action",{method:"POST",body:JSON.stringify({action,value:document.querySelector("#value").value,details:document.querySelector("#details").value,confirmProduction:["promote","rollback"].includes(action)})});out.textContent=JSON.stringify(result,null,2);await refresh()}catch(error){out.textContent=error.message}};
|
|
95
98
|
refresh();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.192",
|
|
4
4
|
"description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"test:page-release-cli": "node scripts/page-release-cli-smoke.mjs",
|
|
112
112
|
"test:app-release-cli": "node scripts/app-release-cli-smoke.mjs",
|
|
113
113
|
"test:application-environments": "node scripts/application-environments-smoke.mjs",
|
|
114
|
+
"test:developer-center": "node scripts/developer-center-smoke.mjs",
|
|
114
115
|
"test:form-release-cas": "node scripts/form-release-cas-smoke.mjs",
|
|
115
116
|
"test:source-dependencies": "node scripts/source-dependencies-smoke.mjs",
|
|
116
117
|
"test:form-field-contract": "node scripts/form-field-contract-smoke.mjs",
|