@agile-team/wl-skills-gate 0.13.2 → 0.13.3
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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/01-quick-start.md +1 -1
- package/docs/07-jenkins.md +1 -1
- package/docs/README.md +1 -1
- package/lib/jenkins-node-bootstrap.js +1 -1
- package/lib/xml.js +8 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/lib-tests/jenkins-native.test.mjs +2 -2
- package/scripts/lib-tests/jenkins-xml-roundtrip.test.mjs +156 -0
- package/templates/ci/check-project.config.json +1 -1
- package/templates/ci/jenkins-bootstrap.py +1 -1
- package/templates/onboarding/backend/quality-tools.package.json +1 -1
- package/templates/quality/rules.lock.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
package/docs/01-quick-start.md
CHANGED
|
@@ -27,7 +27,7 @@ wl-quality init
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
npm exec --yes --ignore-scripts --registry=https://registry.npmjs.org/ \
|
|
30
|
-
--package=@agile-team/wl-skills-gate@0.13.
|
|
30
|
+
--package=@agile-team/wl-skills-gate@0.13.3 -- wl-quality init
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
向导展示差异与实际安装计划,确认后执行;检查异常不能当成零问题。接入后日常由Hook自动检查;正式Review、远端强制检查、群消息与报告权限仍按现场清单验收。
|
package/docs/07-jenkins.md
CHANGED
|
@@ -63,7 +63,7 @@ node('wl-quality-router') { load('/opt/wl-quality/gate-validation/jenkins/entry.
|
|
|
63
63
|
|
|
64
64
|
## 当前交付边界
|
|
65
65
|
|
|
66
|
-
本轮以0.13.
|
|
66
|
+
本轮以0.13.3固定正式版本交付。包发布和隔离测试不代表两业务仓库或远端已接通。正式Review后的自动续建、验证构建结果/制品采集、紧凑检查卡片可靠投递已通过隔离贯通测试;真实项目配置、中央服务、TEST任务和飞书身份仍需现场交接/验收。
|
|
67
67
|
|
|
68
68
|
团队操作以交付包根目录的完整或运维接入操作说明为准,本文供维护者排查底层契约。
|
|
69
69
|
|
package/docs/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# wl-skills-gate 技术手册索引
|
|
2
2
|
|
|
3
|
-
当前正式版本0.13.
|
|
3
|
+
当前正式版本0.13.3。团队操作以交付包根目录主说明为准,见[16 项目与运维入口](16-project-ci-integration.md);本索引提供工具契约与参数参考。
|
|
4
4
|
|
|
5
5
|
授权声明见 [LICENSE](../LICENSE);公开获取不授予使用、复制、修改或再分发授权,申请入口为 [仓库 Issues](https://github.com/ChenyCHENYU/wl-skills-gate/issues)。
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ export async function planJenkinsBootstrap({configFile,env=process.env,fetchImpl
|
|
|
14
14
|
const entries=[];
|
|
15
15
|
for(const [role,job] of Object.entries(MAINTENANCE_JOBS)){
|
|
16
16
|
const before=await client.config(job);let action='create',reason='';
|
|
17
|
-
if(before!==null){try{const v=jobContract(before);const schedule=role==='node'?'H/5 * * * *':'* * * * *';const ownedSchedule=['node','automation','review'].includes(role)&&v.triggers.length===1&&v.triggers[0].type==='hudson.triggers.TimerTrigger'&&v.triggers[0].fields.spec===schedule;assert(v.script===script.trim()&&v.sandbox==='true'&&v.disabled==='false'&&(!v.triggers.length||ownedSchedule),'同名任务存在自定义脚本/触发器或已禁用');action='unchanged';}catch(e){action='blocked';reason=e.message;}}
|
|
17
|
+
if(before!==null){try{const v=jobContract(before);const schedule=role==='node'?'H/5 * * * *':'* * * * *';const ownedSchedule=['node','automation','review'].includes(role)&&v.triggers.length===1&&v.triggers[0].type==='hudson.triggers.TimerTrigger'&&v.triggers[0].fields.spec===schedule;assert(v.script===script.trim()&&v.sandbox==='true'&&v.disabled==='false'&&v.concurrent&&(!v.triggers.length||ownedSchedule),'同名任务存在自定义脚本/触发器或已禁用');action='unchanged';}catch(e){action='blocked';reason=e.message;}}
|
|
18
18
|
// The operator creates this single initial job; never mutate a running self.
|
|
19
19
|
if(role==='fleet'&&before===null){action='blocked';reason='先创建wl-quality-fleet并填入生成的一行入口';}
|
|
20
20
|
entries.push({role,job,action,...(reason?{reason}:{}),beforeHash:before===null?null:digest(before)});
|
package/lib/xml.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 接受完整、无 DTD 的 UTF-8 XML 1.0/1.1 声明;仍使用严格受限字符与元素子集,不猜测测试结果。
|
|
2
2
|
function decode(text) {
|
|
3
3
|
if (/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[\da-fA-F]+);)/.test(text)) throw new Error("XML 包含未知或未闭合实体");
|
|
4
4
|
return text.replace(/&([^;]+);/g, (_, entity) => {
|
|
@@ -26,6 +26,12 @@ function opening(tag) {
|
|
|
26
26
|
|
|
27
27
|
export function parseXml(xml) {
|
|
28
28
|
xml = xml.replace(/^\uFEFF/, "");
|
|
29
|
+
// Normalize physical line endings before decoding references (XML 1.0/1.1 §2.11).
|
|
30
|
+
const xml11 = /^<\?xml\s+version\s*=\s*(?:"1\.1"|'1\.1')(?=\s|\?>)/.test(xml);
|
|
31
|
+
const declaration = /^<\?xml[\s\S]*?\?>/.exec(xml)?.[0] || "";
|
|
32
|
+
if (xml11 && /[\u0085\u2028]/.test(declaration)) throw new Error("XML 1.1 声明包含非法换行");
|
|
33
|
+
if (xml11 && /[\u007f-\u0084\u0086-\u009f]/.test(xml)) throw new Error("XML 1.1 包含非法字面控制字符");
|
|
34
|
+
xml = xml.replace(xml11 ? /\r\n|\r\u0085|\r|\u0085|\u2028/g : /\r\n|\r/g, "\n");
|
|
29
35
|
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(xml)) throw new Error("XML 包含非法控制字符");
|
|
30
36
|
const token = /<!--[\s\S]*?-->|<\?[^?]*(?:\?(?!>)[^?]*)*\?>|<!\[CDATA\[[\s\S]*?\]\]>|<(?:(?:[^<>"']|"[^"<]*"|'[^'<]*')+)>|[^<]+/gy;
|
|
31
37
|
const stack = [];
|
|
@@ -42,7 +48,7 @@ export function parseXml(xml) {
|
|
|
42
48
|
continue;
|
|
43
49
|
}
|
|
44
50
|
if (value.startsWith("<?")) {
|
|
45
|
-
if (m.index !== 0 || !/^<\?xml\s+version\s*=\s*(?:"1\.
|
|
51
|
+
if (m.index !== 0 || !/^<\?xml\s+version\s*=\s*(?:"1\.[01]"|'1\.[01]')(?:\s+encoding\s*=\s*(?:"UTF-8"|'UTF-8'))?(?:\s+standalone\s*=\s*(?:"(?:yes|no)"|'(?:yes|no)'))?\s*\?>$/i.test(value)) throw new Error("仅支持开头完整的 UTF-8 XML 1.0/1.1 声明");
|
|
46
52
|
continue;
|
|
47
53
|
}
|
|
48
54
|
if (value.startsWith("<![CDATA[")) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agile-team/wl-skills-gate",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@agile-team/wl-skills-gate",
|
|
9
|
-
"version": "0.13.
|
|
9
|
+
"version": "0.13.3",
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@secretlint/node": "13.0.7",
|
package/package.json
CHANGED
|
@@ -33,7 +33,7 @@ test('真实扫描入口生成本轮公共入口工件;旧Gate或不同SHA不
|
|
|
33
33
|
});
|
|
34
34
|
test('公共任务首次只建四个、逐个回读;重跑零写入,同名自定义不覆盖,超时先对账',async t=>{
|
|
35
35
|
const f=fixture(t),v=installed(f),script=nativePipelineEntry(v.result.nativeEntry);const jobs=new Map(),env={WL_JENKINS_CONFIG_USER:'service',WL_JENKINS_CONFIG_TOKEN:'fixture'},posts=[];let unknown=true;
|
|
36
|
-
const xml=s=>`<flow-definition><properties
|
|
36
|
+
const xml=s=>`<flow-definition><properties><org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty><abortPrevious>false</abortPrevious></org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty></properties><definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition"><script>${s}</script><sandbox>true</sandbox></definition><disabled>false</disabled></flow-definition>`;
|
|
37
37
|
jobs.set('wl-quality-fleet',xml(script));const fetchImpl=async(url,o)=>{const u=new URL(url);if(o.method==='GET'){const name=decodeURIComponent(u.pathname.split('/')[2]);return jobs.has(name)?new Response(jobs.get(name)):new Response('',{status:404});}assert.equal(u.pathname,'/createItem');const name=u.searchParams.get('name');posts.push(name);jobs.set(name,o.body);if(unknown){unknown=false;throw Error('timeout after create');}return new Response('');};
|
|
38
38
|
const options={configFile:v.result.clusterFile,env,fetchImpl};let p=await planJenkinsBootstrap(options);assert.equal(p.entries.filter(e=>e.action==='create').length,4);await assert.rejects(applyJenkinsBootstrap({...options,confirm:true,planHash:'0'.repeat(64)}),/预览/);let result=await applyJenkinsBootstrap({...options,confirm:true,planHash:p.planHash});assert.equal(result.status,'common-jobs-verified',JSON.stringify(result));assert.equal(posts.length,4);assert(result.entries.some(e=>e.writeReconciled));assert(!result.businessBuildsTriggered);assert(!result.schedulesEnabled);assert.equal(jobs.size,5);p=await planJenkinsBootstrap(options);result=await applyJenkinsBootstrap({...options,confirm:true,planHash:p.planHash});assert.equal(result.status,'common-jobs-verified');assert.equal(posts.length,4);jobs.set('wl-quality-review',xml('echo custom'));p=await planJenkinsBootstrap(options);assert.equal(p.status,'blocked');await assert.rejects(applyJenkinsBootstrap({...options,confirm:true,planHash:p.planHash}),/阻断/);assert.equal(jobContract(jobs.get('wl-quality-review')).script,'echo custom');
|
|
39
39
|
});
|
|
@@ -41,7 +41,7 @@ test('公共入口配置拒绝符号链接和注入路径',t=>{const f=fixture(t
|
|
|
41
41
|
|
|
42
42
|
test('公共任务已启用包内定时后仍可重复核验;未知定时和禁用任务拒绝冒充可用',async t=>{
|
|
43
43
|
const f=fixture(t),v=installed(f),script=nativePipelineEntry(v.result.nativeEntry);let spec='* * * * *',disabled=false;
|
|
44
|
-
const fetchImpl=async url=>{const role=decodeURIComponent(new URL(url).pathname.split('/')[2]);const trigger=role==='wl-quality-central'?`<org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty><triggers><hudson.triggers.TimerTrigger><spec>${spec}</spec></hudson.triggers.TimerTrigger></triggers></org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>`:'';return new Response(`<flow-definition><properties>${trigger}</properties><definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition"><script>${script}</script><sandbox>true</sandbox></definition><disabled>${disabled}</disabled></flow-definition>`);};
|
|
44
|
+
const fetchImpl=async url=>{const role=decodeURIComponent(new URL(url).pathname.split('/')[2]);const trigger=role==='wl-quality-central'?`<org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty><triggers><hudson.triggers.TimerTrigger><spec>${spec}</spec></hudson.triggers.TimerTrigger></triggers></org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>`:'';return new Response(`<flow-definition><properties><org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty><abortPrevious>false</abortPrevious></org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty>${trigger}</properties><definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition"><script>${script}</script><sandbox>true</sandbox></definition><disabled>${disabled}</disabled></flow-definition>`);};
|
|
45
45
|
const options={configFile:v.result.clusterFile,env:{WL_JENKINS_CONFIG_USER:'service',WL_JENKINS_CONFIG_TOKEN:'fixture'},fetchImpl};assert.equal((await planJenkinsBootstrap(options)).status,'preview');spec='H * * * *';assert.equal((await planJenkinsBootstrap(options)).status,'blocked');spec='* * * * *';disabled=true;assert.equal((await planJenkinsBootstrap(options)).status,'blocked');
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {test} from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtempSync,mkdirSync,readFileSync,rmSync,realpathSync} from 'node:fs';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {parseXml} from '../../lib/xml.js';
|
|
7
|
+
import {jobContract} from '../../lib/jenkins-job-config.js';
|
|
8
|
+
import {GATE_VERSION} from '../../lib/jenkins-integration.js';
|
|
9
|
+
import {nativePipelineEntry} from '../../lib/jenkins-entry-contract.js';
|
|
10
|
+
import {planJenkinsInstallation,applyJenkinsInstallation} from '../../lib/jenkins-installation.js';
|
|
11
|
+
import {planJenkinsBootstrap,applyJenkinsBootstrap} from '../../lib/jenkins-node-bootstrap.js';
|
|
12
|
+
import {MAINTENANCE_JOBS} from '../../lib/jenkins-cluster.js';
|
|
13
|
+
|
|
14
|
+
// Jenkins switched its XML output to 1.1 in 2.107.1. A protocol mock which
|
|
15
|
+
// echoes createItem's request body never exercises the real config.xml format.
|
|
16
|
+
// This fixture independently rebuilds Jenkins-style persisted XML from the
|
|
17
|
+
// approved script; it never stores or returns the submitted request body.
|
|
18
|
+
const declaration=quote=>`<?xml version=${quote}1.1${quote} encoding=${quote}UTF-8${quote}?>`;
|
|
19
|
+
const text=value=>String(value).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",''').replaceAll('\n','
');
|
|
20
|
+
function persistedJob(script,{quote='"',scriptOverride,sandbox='true',disabled='false',concurrent=true,spec=null}={}) {
|
|
21
|
+
const properties=[
|
|
22
|
+
'<hudson.model.ParametersDefinitionProperty><parameterDefinitions/></hudson.model.ParametersDefinitionProperty>',
|
|
23
|
+
concurrent?'<org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty><abortPrevious>false</abortPrevious></org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty>':'',
|
|
24
|
+
'<org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty><triggers>'+(spec===null?'':`<hudson.triggers.TimerTrigger><spec>${text(spec)}</spec></hudson.triggers.TimerTrigger>` )+'</triggers></org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>'
|
|
25
|
+
].filter(Boolean).reverse().join('\n ');
|
|
26
|
+
return `${declaration(quote)}\n<flow-definition plugin="workflow-job@1505.fixture">
|
|
27
|
+
<actions/>
|
|
28
|
+
<description>WL Gate common task & regression</description>
|
|
29
|
+
<keepDependencies>false</keepDependencies>
|
|
30
|
+
<properties>
|
|
31
|
+
${properties}
|
|
32
|
+
</properties>
|
|
33
|
+
<definition plugin='workflow-cps@4218.fixture' class='org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition'>
|
|
34
|
+
<script>${text(scriptOverride??script)}</script>
|
|
35
|
+
<sandbox>${sandbox}</sandbox>
|
|
36
|
+
</definition>
|
|
37
|
+
<triggers/>
|
|
38
|
+
<disabled>${disabled}</disabled>
|
|
39
|
+
</flow-definition>\n`;
|
|
40
|
+
}
|
|
41
|
+
const jobNames=Object.values(MAINTENANCE_JOBS);
|
|
42
|
+
function installed(t) {
|
|
43
|
+
const base=mkdtempSync(path.join(realpathSync(tmpdir()),'wl-xml-roundtrip-'));
|
|
44
|
+
t.after(()=>rmSync(base,{recursive:true,force:true}));
|
|
45
|
+
const configDir=path.join(base,'config');mkdirSync(configDir);
|
|
46
|
+
const input={prefix:path.join(base,'install space'),configDir,registryFiles:[path.join(configDir,'bindings.json')],jenkinsOrigin:'https://jenkins.example.invalid/',centralAgentLabel:'central',reviewSignerAgentLabel:'signer'};
|
|
47
|
+
const plan=planJenkinsInstallation(input);
|
|
48
|
+
const node=applyJenkinsInstallation({...input,confirm:true,planHash:plan.planHash});
|
|
49
|
+
const script=nativePipelineEntry(node.nativeEntry),jobs=new Map([['wl-quality-fleet',persistedJob(script)]]);
|
|
50
|
+
const calls=[],created=[],submitted=[];
|
|
51
|
+
const fixture={node,script,jobs,calls,created,submitted,quote:'"',lostReply:false,mutateCreated:null};
|
|
52
|
+
fixture.fetchImpl=async(endpoint,request)=>{
|
|
53
|
+
const url=new URL(endpoint);assert.equal(url.origin,'https://jenkins.example.invalid');
|
|
54
|
+
calls.push({method:request.method,path:url.pathname});
|
|
55
|
+
if(request.method==='GET'){
|
|
56
|
+
const match=/^\/job\/([^/]+)\/config\.xml$/.exec(url.pathname);assert.ok(match,'only expected metadata requests');
|
|
57
|
+
const name=decodeURIComponent(match[1]);
|
|
58
|
+
return jobs.has(name)?new Response(jobs.get(name),{headers:{'Content-Type':'application/xml; charset=UTF-8'}}):new Response('',{status:404});
|
|
59
|
+
}
|
|
60
|
+
assert.equal(request.method,'POST');assert.equal(url.pathname,'/createItem');
|
|
61
|
+
const name=url.searchParams.get('name');assert.ok(jobNames.includes(name));
|
|
62
|
+
assert.ok(!jobs.has(name),'create-only must not overwrite an existing job');
|
|
63
|
+
assert.equal(jobContract(request.body).script,script.trim(),'submit the approved native entry');
|
|
64
|
+
submitted.push(request.body);created.push(name);
|
|
65
|
+
let saved=persistedJob(script,{quote:fixture.quote});
|
|
66
|
+
if(fixture.mutateCreated)saved=fixture.mutateCreated(name,saved);
|
|
67
|
+
assert.notEqual(saved,request.body,'persisted Jenkins response must not echo request XML');
|
|
68
|
+
jobs.set(name,saved);
|
|
69
|
+
if(fixture.lostReply){fixture.lostReply=false;throw Error('fixture response lost after Jenkins saved config');}
|
|
70
|
+
return new Response('',{status:200});
|
|
71
|
+
};
|
|
72
|
+
fixture.options={configFile:node.clusterFile,env:{WL_JENKINS_CONFIG_USER:'fixture-admin',WL_JENKINS_CONFIG_TOKEN:'fixture-not-real'},fetchImpl:fixture.fetchImpl};
|
|
73
|
+
return fixture;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for(const [name,header] of [
|
|
77
|
+
['XML 1.0 UTF-8 declaration','<?xml version="1.0" encoding="UTF-8"?>'],
|
|
78
|
+
['Jenkins XML 1.1 double-quoted declaration',declaration('"')],
|
|
79
|
+
['Jenkins XML 1.1 single-quoted declaration',declaration("'")]
|
|
80
|
+
])test(name,()=>assert.equal(parseXml(header+'\n<root>正常 & safe</root>').text,'正常 & safe'));
|
|
81
|
+
|
|
82
|
+
for(const [label,header,lineEnding] of [
|
|
83
|
+
['1.0 CRLF','<?xml version="1.0"?>','\r\n'],
|
|
84
|
+
['1.1 CRLF',declaration('"'),'\r\n'],
|
|
85
|
+
['1.1 CR+NEL',declaration('"'),'\r\u0085'],
|
|
86
|
+
['1.1 NEL',declaration('"'),'\u0085'],
|
|
87
|
+
['1.1 Unicode line separator',declaration("'"),'\u2028']
|
|
88
|
+
])test(`XML physical line endings normalize before contract comparison: ${label}`,()=>{
|
|
89
|
+
assert.equal(parseXml(header+lineEnding+'<root>a'+lineEnding+'b</root>').text,'a\nb');
|
|
90
|
+
});
|
|
91
|
+
test('XML character references remain explicit characters after physical newline normalization',()=>{
|
|
92
|
+
assert.equal(parseXml(declaration('"')+'<root>a b…c
d e</root>').text,'a\rb\u0085c\u2028d\ne');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
for(const [label,xml] of [
|
|
96
|
+
['unsupported XML version','<?xml version="1.2"?><root/>'],
|
|
97
|
+
['unsupported encoding','<?xml version="1.1" encoding="UTF-16"?><root/>'],
|
|
98
|
+
['second declaration',declaration('"')+declaration('"')+'<root/>'],
|
|
99
|
+
['external DTD',declaration('"')+'<!DOCTYPE root SYSTEM "file:///not-read"><root/>'],
|
|
100
|
+
['internal entity declaration',declaration('"')+'<!DOCTYPE root [<!ENTITY x "private">]><root>&x;</root>'],
|
|
101
|
+
['unknown entity',declaration('"')+'<root>¬Declared;</root>'],
|
|
102
|
+
['literal control character',declaration('"')+'<root>\u0001</root>'],
|
|
103
|
+
['zero character reference',declaration('"')+'<root>�</root>']
|
|
104
|
+
])test(`XML 1.1 support stays fail-closed: ${label}`,()=>assert.throws(()=>parseXml(xml)));
|
|
105
|
+
|
|
106
|
+
for(const quote of ['"',"'"])test(`real bootstrap plan/apply verifies all five normalized Jenkins jobs (${quote} declaration) and reuses them`,async t=>{
|
|
107
|
+
const f=installed(t);f.quote=quote;
|
|
108
|
+
f.jobs.set('wl-quality-fleet',persistedJob(f.script,{quote}));
|
|
109
|
+
assert.equal(JSON.parse(readFileSync(new URL('../../package.json',import.meta.url))).version,GATE_VERSION);
|
|
110
|
+
let p=await planJenkinsBootstrap(f.options);
|
|
111
|
+
assert.equal(p.status,'preview');assert.equal(p.entries.filter(e=>e.action==='create').length,4);
|
|
112
|
+
let result=await applyJenkinsBootstrap({...f.options,confirm:true,planHash:p.planHash});
|
|
113
|
+
assert.equal(result.status,'common-jobs-verified',JSON.stringify(result));
|
|
114
|
+
assert.equal(result.entries.length,5);assert.ok(result.entries.every(e=>e.status==='verified'));
|
|
115
|
+
assert.equal(result.businessBuildsTriggered,false);assert.equal(result.schedulesEnabled,false);
|
|
116
|
+
assert.equal(f.created.length,4);assert.equal(f.jobs.size,5);
|
|
117
|
+
for(const xml of f.jobs.values())assert.ok(xml.startsWith(declaration(quote)));
|
|
118
|
+
const persisted=[...f.jobs];p=await planJenkinsBootstrap(f.options);
|
|
119
|
+
assert.ok(p.entries.every(e=>e.action==='unchanged'));
|
|
120
|
+
result=await applyJenkinsBootstrap({...f.options,confirm:true,planHash:p.planHash});
|
|
121
|
+
assert.equal(result.status,'common-jobs-verified');assert.equal(f.created.length,4);assert.deepEqual([...f.jobs],persisted);
|
|
122
|
+
assert.ok(f.calls.every(c=>c.method==='GET'||c.path==='/createItem'),'no overwrite, build, credential, or secret endpoint');
|
|
123
|
+
});
|
|
124
|
+
test('Jenkins-normalized readback reconciles a lost create response without duplicating the saved job',async t=>{
|
|
125
|
+
const f=installed(t);f.lostReply=true;
|
|
126
|
+
const p=await planJenkinsBootstrap(f.options),result=await applyJenkinsBootstrap({...f.options,confirm:true,planHash:p.planHash});
|
|
127
|
+
assert.equal(result.status,'common-jobs-verified',JSON.stringify(result));
|
|
128
|
+
assert.equal(result.entries.filter(e=>e.writeReconciled).length,1);assert.equal(f.created.length,4);
|
|
129
|
+
});
|
|
130
|
+
test('owned maintenance schedules survive normalized property/trigger serialization without extra writes',async t=>{
|
|
131
|
+
const f=installed(t);
|
|
132
|
+
for(const [role,name] of Object.entries(MAINTENANCE_JOBS))f.jobs.set(name,persistedJob(f.script,{spec:role==='node'?'H/5 * * * *':['review','automation'].includes(role)?'* * * * *':null}));
|
|
133
|
+
const p=await planJenkinsBootstrap(f.options);assert.equal(p.status,'preview',JSON.stringify(p));
|
|
134
|
+
const r=await applyJenkinsBootstrap({...f.options,confirm:true,planHash:p.planHash});
|
|
135
|
+
assert.equal(r.status,'common-jobs-verified');assert.equal(f.created.length,0);
|
|
136
|
+
});
|
|
137
|
+
for(const [label,changes] of [
|
|
138
|
+
['changed executable script',{scriptOverride:"println 'unexpected owner'"}],
|
|
139
|
+
['sandbox disabled',{sandbox:'false'}],
|
|
140
|
+
['job disabled',{disabled:'true'}],
|
|
141
|
+
['concurrency protection removed',{concurrent:false}],
|
|
142
|
+
['unapproved timer trigger',{spec:'H * * * *'}]
|
|
143
|
+
])test(`existing normalized job stays blocked for ${label}`,async t=>{
|
|
144
|
+
const f=installed(t),original=persistedJob(f.script,changes);f.jobs.set('wl-quality-fleet',original);
|
|
145
|
+
const p=await planJenkinsBootstrap(f.options);assert.equal(p.status,'blocked',JSON.stringify(p));
|
|
146
|
+
await assert.rejects(applyJenkinsBootstrap({...f.options,confirm:true,planHash:p.planHash}),/阻断/);
|
|
147
|
+
assert.equal(f.jobs.get('wl-quality-fleet'),original);assert.equal(f.created.length,0);
|
|
148
|
+
});
|
|
149
|
+
test('a genuinely changed script after create is attention, never a verified bootstrap or automatic overwrite',async t=>{
|
|
150
|
+
const f=installed(t);f.mutateCreated=(name,saved)=>name==='wl-quality-doctor'?persistedJob(f.script,{scriptOverride:"println 'changed after save'"}):saved;
|
|
151
|
+
const p=await planJenkinsBootstrap(f.options),result=await applyJenkinsBootstrap({...f.options,confirm:true,planHash:p.planHash});
|
|
152
|
+
assert.equal(result.status,'attention');assert.equal(result.entries.find(e=>e.job==='wl-quality-doctor').status,'attention');
|
|
153
|
+
assert.equal(result.businessBuildsTriggered,false);assert.equal(result.schedulesEnabled,false);
|
|
154
|
+
const writes=f.created.length,next=await planJenkinsBootstrap(f.options);assert.equal(next.status,'blocked');
|
|
155
|
+
await assert.rejects(applyJenkinsBootstrap({...f.options,confirm:true,planHash:next.planHash}),/阻断/);assert.equal(f.created.length,writes);
|
|
156
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"kind": "wl-quality-ci-check",
|
|
4
4
|
"project": "REPLACE_WITH_ACTUAL_PROJECT",
|
|
5
|
-
"gateVersion": "0.13.
|
|
5
|
+
"gateVersion": "0.13.3",
|
|
6
6
|
"workspaceHash": "REPLACE_WITH_APPROVED_WORKSPACE_SHA256",
|
|
7
7
|
"lockHash": "REPLACE_WITH_APPROVED_LOCK_SHA256",
|
|
8
8
|
"policyHash": "REPLACE_WITH_APPROVED_POLICY_SHA256",
|
|
@@ -55,7 +55,7 @@ def preflight(env):
|
|
|
55
55
|
valid_root = root.is_absolute() and root.is_dir()
|
|
56
56
|
check("gate-root", valid_root, "固定 Gate 目录可读取" if valid_root else "固定 Gate 目录不存在或不可读取",
|
|
57
57
|
"在该Jenkins节点运行本包的接入Jenkins脚本;使用安装回执中的 gateHome。包内入口为 bin/wl-skills-gate.js。")
|
|
58
|
-
expected = env.get("WL_BOOTSTRAP_GATE_VERSION", "0.13.
|
|
58
|
+
expected = env.get("WL_BOOTSTRAP_GATE_VERSION", "0.13.3")
|
|
59
59
|
try:
|
|
60
60
|
package = json.loads((root / "package.json").read_text())
|
|
61
61
|
correct = package.get("name") == "@agile-team/wl-skills-gate" and package.get("version") == expected
|