@wdyy/skills 0.1.10 → 0.1.12
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/.well-known/skills/index.json +2 -2
- package/.well-known/skills/wdyy-deployment-standard/SKILL.md +33 -44
- package/.well-known/skills/wdyy-deployment-standard/agents/openai.yaml +2 -2
- package/.well-known/skills/wdyy-deployment-standard/reference/linux-deployment-rules.md +27 -63
- package/.well-known/skills/wdyy-deployment-standard/scripts/generate-deployment-files.mjs +19 -9
- package/.well-known/skills/wdyy-deployment-standard/scripts/generate-deployment-files.test.mjs +16 -2
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.mjs +61 -123
- package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.test.mjs +145 -347
- package/.well-known/skills/wdyy-deployment-standard/templates/deploy.sh.template +184 -675
- package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.yml +35 -0
- package/.well-known/skills/wdyy-deployment-standard/templates/env.example.template +0 -7
- package/.well-known/skills/wdyy-deployment-standard/templates/frontend-container.conf.template +2 -2
- package/.well-known/skills/wdyy-logging-standard/SKILL.md +2 -2
- package/.well-known/skills/wdyy-logging-standard/agents/openai.yaml +1 -1
- package/.well-known/skills/wdyy-logging-standard/reference/logging-rules.md +2 -2
- package/.well-known/skills/wdyy-logging-standard/scripts/validate-log-entry.test.mjs +1 -1
- package/README.md +4 -2
- package/lib/wdyy-cli.js +3 -2
- package/package.json +1 -1
- package/.well-known/skills/wdyy-deployment-standard/templates/docker-compose.blue-green.yml +0 -70
- package/.well-known/skills/wdyy-deployment-standard/templates/nginx-upstream.template.conf +0 -26
package/.well-known/skills/wdyy-deployment-standard/scripts/validate-deployment-package.test.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import {
|
|
4
|
-
access, chmod, cp, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, symlink, unlink, writeFile,
|
|
5
|
-
} from 'node:fs/promises';
|
|
3
|
+
import { access, chmod, cp, copyFile, mkdir, mkdtemp, readFile, readdir, rm, symlink, unlink, writeFile } from 'node:fs/promises';
|
|
6
4
|
import { tmpdir } from 'node:os';
|
|
7
5
|
import { dirname, join, resolve } from 'node:path';
|
|
8
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -14,12 +12,7 @@ const skillRoot = resolve(scriptsRoot, '..');
|
|
|
14
12
|
const templatesRoot = join(skillRoot, 'templates');
|
|
15
13
|
const validator = join(scriptsRoot, 'validate-deployment-package.mjs');
|
|
16
14
|
const deployTemplate = join(templatesRoot, 'deploy.sh.template');
|
|
17
|
-
const composeTemplate = join(templatesRoot, 'docker-compose.
|
|
18
|
-
const nginxTemplate = join(templatesRoot, 'nginx-upstream.template.conf');
|
|
19
|
-
const dockerignoreTemplate = join(templatesRoot, 'dockerignore.template');
|
|
20
|
-
const frontendDockerfileTemplate = join(templatesRoot, 'frontend.Dockerfile.template');
|
|
21
|
-
const backendDockerfileTemplate = join(templatesRoot, 'Dockerfile.template');
|
|
22
|
-
const frontendNginxTemplate = join(templatesRoot, 'frontend-container.conf.template');
|
|
15
|
+
const composeTemplate = join(templatesRoot, 'docker-compose.yml');
|
|
23
16
|
const temporaryDirectories = [];
|
|
24
17
|
|
|
25
18
|
afterEach(async () => {
|
|
@@ -32,7 +25,7 @@ async function temporary(prefix) {
|
|
|
32
25
|
return path;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
function envText(
|
|
28
|
+
function envText(extra = '') {
|
|
36
29
|
return `# 前端
|
|
37
30
|
FRONTEND_URL=0.0.0.0
|
|
38
31
|
FRONTEND_PORT=5173
|
|
@@ -53,61 +46,46 @@ DB_SCHEMA=
|
|
|
53
46
|
|
|
54
47
|
# 部署
|
|
55
48
|
PROJECT_NAME=fixture
|
|
56
|
-
SERVER_PROJECTS_ROOT=${serverProjectsRoot}
|
|
57
|
-
PROJECT_HTTP_PORT=9099
|
|
58
49
|
DOCKER_BIND_IP=127.0.0.1
|
|
59
|
-
FRONTEND_BLUE_PORT=19091
|
|
60
|
-
FRONTEND_GREEN_PORT=19092
|
|
61
|
-
BACKEND_BLUE_PORT=19093
|
|
62
|
-
BACKEND_GREEN_PORT=19094
|
|
63
50
|
FRONTEND_BASE_IMAGE=nginx:stable
|
|
64
51
|
BACKEND_BASE_IMAGE=node:24-alpine3.24
|
|
65
|
-
DATABASE_MIGRATION_MODE=${mode}
|
|
66
52
|
${extra}`;
|
|
67
53
|
}
|
|
68
54
|
|
|
69
|
-
async function
|
|
70
|
-
|
|
55
|
+
async function writeExecutable(path, content) {
|
|
56
|
+
await writeFile(path, content);
|
|
57
|
+
await chmod(path, 0o755);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function createFixture({ agents = '# Target project\n', extraEnv = '' } = {}) {
|
|
61
|
+
const root = await temporary('wdyy-direct-deploy-');
|
|
71
62
|
const fakeBin = join(root, 'fake-bin');
|
|
72
|
-
const serverProjectsRoot = join(root, 'servers');
|
|
73
63
|
const commandLog = join(root, 'commands.log');
|
|
74
64
|
await Promise.all([
|
|
75
65
|
mkdir(fakeBin, { recursive: true }),
|
|
76
66
|
mkdir(join(root, 'src/frontend'), { recursive: true }),
|
|
77
67
|
mkdir(join(root, 'src/backend'), { recursive: true }),
|
|
78
68
|
mkdir(join(root, 'scripts/deployment'), { recursive: true }),
|
|
79
|
-
mkdir(serverProjectsRoot, { recursive: true }),
|
|
80
69
|
]);
|
|
81
70
|
await Promise.all([
|
|
82
71
|
copyFile(deployTemplate, join(root, 'deploy.sh')),
|
|
83
|
-
copyFile(
|
|
84
|
-
copyFile(
|
|
85
|
-
copyFile(composeTemplate, join(root, 'scripts/deployment/docker-compose.
|
|
86
|
-
copyFile(
|
|
87
|
-
copyFile(
|
|
88
|
-
copyFile(dockerignoreTemplate, join(root, '.dockerignore')),
|
|
72
|
+
copyFile(join(templatesRoot, 'frontend.Dockerfile.template'), join(root, 'src/frontend/Dockerfile')),
|
|
73
|
+
copyFile(join(templatesRoot, 'Dockerfile.template'), join(root, 'src/backend/Dockerfile')),
|
|
74
|
+
copyFile(composeTemplate, join(root, 'scripts/deployment/docker-compose.yml')),
|
|
75
|
+
copyFile(join(templatesRoot, 'frontend-container.conf.template'), join(root, 'scripts/deployment/frontend-container.conf.template')),
|
|
76
|
+
copyFile(join(templatesRoot, 'dockerignore.template'), join(root, '.dockerignore')),
|
|
89
77
|
writeFile(join(root, 'AGENTS.md'), agents),
|
|
90
|
-
writeFile(join(root, '.env'), envText(
|
|
78
|
+
writeFile(join(root, '.env'), envText(extraEnv)),
|
|
91
79
|
writeFile(commandLog, ''),
|
|
92
80
|
]);
|
|
93
81
|
await chmod(join(root, 'deploy.sh'), 0o755);
|
|
94
82
|
await chmod(join(root, '.env'), 0o600);
|
|
95
83
|
|
|
96
|
-
if (mode === 'manual') {
|
|
97
|
-
await mkdir(join(root, 'database/migrations'), { recursive: true });
|
|
98
|
-
await writeFile(join(root, 'database/migrations/V001__baseline.sql'), 'SELECT 1;\n');
|
|
99
|
-
await writeFile(join(root, 'scripts/apply-migrations.sh'), '#!/usr/bin/env bash\nset -euo pipefail\n');
|
|
100
|
-
await chmod(join(root, 'scripts/apply-migrations.sh'), 0o755);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
84
|
await writeExecutable(join(fakeBin, 'pnpm'), `#!/usr/bin/env bash
|
|
104
85
|
set -euo pipefail
|
|
105
86
|
printf 'pnpm %s\n' "$*" >> "$COMMAND_LOG"
|
|
106
87
|
[[ "\${FAIL_GATE:-}" != "\${1:-}" ]] || exit 9
|
|
107
|
-
if [[ "\${1:-}" == build ]]; then
|
|
108
|
-
mkdir -p src/frontend/dist
|
|
109
|
-
printf '<!doctype html><div id="app"></div>\n' > src/frontend/dist/index.html
|
|
110
|
-
fi
|
|
88
|
+
if [[ "\${1:-}" == build ]]; then mkdir -p src/frontend/dist; printf '<div id="app"></div>\n' > src/frontend/dist/index.html; fi
|
|
111
89
|
`);
|
|
112
90
|
await writeExecutable(join(fakeBin, 'docker'), `#!/usr/bin/env bash
|
|
113
91
|
set -euo pipefail
|
|
@@ -115,89 +93,41 @@ printf 'docker %s\n' "$*" >> "$COMMAND_LOG"
|
|
|
115
93
|
case "\${1:-}" in
|
|
116
94
|
build) exit 0 ;;
|
|
117
95
|
image)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
fixture_backend:20260808-001
|
|
121
|
-
other_project_frontend:shared}"; fi
|
|
96
|
+
[[ "\${2:-}" == inspect ]] || exit 10
|
|
97
|
+
tag="\${!#}"; printf '%s\n' "\${tag##*:}"
|
|
122
98
|
;;
|
|
123
99
|
save)
|
|
124
100
|
shift; output=; tag=
|
|
125
|
-
while [[ "$#" -gt 0 ]]; do
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
work="$(mktemp -d)"
|
|
129
|
-
printf '[{"Config":"config.json","RepoTags":["%s"],"Layers":[]}]\n' "$tag" > "$work/manifest.json"
|
|
130
|
-
printf '{}\n' > "$work/config.json"
|
|
131
|
-
/usr/bin/tar -cf "$output" -C "$work" manifest.json config.json
|
|
132
|
-
/bin/rm -rf "$work"
|
|
101
|
+
while [[ "$#" -gt 0 ]]; do if [[ "$1" == -o ]]; then output="$2"; shift 2; else tag="$1"; shift; fi; done
|
|
102
|
+
work="$(mktemp -d)"; printf '[{"Config":"config.json","RepoTags":["%s"],"Layers":[]}]\n' "$tag" > "$work/manifest.json"; printf '{}\n' > "$work/config.json"
|
|
103
|
+
/usr/bin/tar -cf "$output" -C "$work" manifest.json config.json; /bin/rm -rf "$work"
|
|
133
104
|
;;
|
|
134
105
|
load) exit 0 ;;
|
|
135
106
|
compose) exit 0 ;;
|
|
136
|
-
ps)
|
|
137
|
-
if [[ "\${2:-}" == -aq ]]; then
|
|
138
|
-
printf '%s\n' "\${DOCKER_CONTAINER_IDS:-c111
|
|
139
|
-
c222}"
|
|
140
|
-
elif [[ "\${2:-}" == --format && -n "\${DOCKER_RUNNING_PORTS:-}" ]]; then
|
|
141
|
-
printf '%s\n' "$DOCKER_RUNNING_PORTS"
|
|
142
|
-
fi
|
|
143
|
-
;;
|
|
144
|
-
network)
|
|
145
|
-
if [[ "\${2:-}" == ls ]]; then printf '%s\n' "\${DOCKER_NETWORK_IDS:-n111}"; fi
|
|
146
|
-
;;
|
|
147
|
-
rm) exit 0 ;;
|
|
148
107
|
*) echo "unexpected docker command: $*" >&2; exit 10 ;;
|
|
149
108
|
esac
|
|
150
109
|
`);
|
|
151
110
|
await writeExecutable(join(fakeBin, 'curl'), `#!/usr/bin/env bash
|
|
152
111
|
set -euo pipefail
|
|
153
112
|
url="\${!#}"
|
|
154
|
-
if [[ "\${
|
|
113
|
+
if [[ "\${FAIL_HEALTH:-}" == frontend && "$url" == *:5173/* ]]; then exit 26; fi
|
|
114
|
+
if [[ "\${FAIL_HEALTH:-}" == backend && "$url" == *:3000/* ]]; then exit 27; fi
|
|
155
115
|
if [[ "$url" == */version ]]; then printf '%s' "\${EXPECTED_VERSION:?}"; else printf 'ok'; fi
|
|
156
116
|
`);
|
|
157
|
-
|
|
158
|
-
set -euo pipefail
|
|
159
|
-
printf 'nginx %s\n' "$*" >> "$COMMAND_LOG"
|
|
160
|
-
if [[ "\${1:-}" == -t ]]; then [[ "\${FAIL_NGINX_TEST:-0}" != 1 ]] || exit 23; exit 0; fi
|
|
161
|
-
if [[ "\${1:-}" == -T ]]; then
|
|
162
|
-
[[ ! -f "$SERVER_ROOT/nginx/site.conf" ]] || cat "$SERVER_ROOT/nginx/site.conf"
|
|
163
|
-
[[ -z "\${EXTRA_NGINX_CONFIG:-}" ]] || printf '%s\n' "$EXTRA_NGINX_CONFIG"
|
|
164
|
-
exit 0
|
|
165
|
-
fi
|
|
166
|
-
if [[ "\${1:-}" == -s && "\${2:-}" == reload && "\${FAIL_NGINX_RELOAD_ONCE:-0}" == 1 && ! -f "$NGINX_FAIL_MARKER" ]]; then
|
|
167
|
-
: > "$NGINX_FAIL_MARKER"; exit 24
|
|
168
|
-
fi
|
|
169
|
-
`);
|
|
170
|
-
await writeExecutable(join(fakeBin, 'flock'), '#!/usr/bin/env bash\nexit 0\n');
|
|
171
|
-
await writeExecutable(join(fakeBin, 'install'), `#!/usr/bin/env bash
|
|
172
|
-
set -euo pipefail
|
|
173
|
-
target="\${!#}"
|
|
174
|
-
if [[ "\${FAIL_STATE_INSTALL:-0}" == 1 && "$target" == */state/active.env ]]; then exit 25; fi
|
|
175
|
-
exec /usr/bin/install "$@"
|
|
176
|
-
`);
|
|
177
|
-
|
|
178
|
-
return { root, fakeBin, serverProjectsRoot, commandLog, mode };
|
|
117
|
+
return { root, fakeBin, commandLog };
|
|
179
118
|
}
|
|
180
119
|
|
|
181
|
-
|
|
182
|
-
await writeFile(path, content);
|
|
183
|
-
await chmod(path, 0o755);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function run(root, args, fixture, extraEnv = {}) {
|
|
120
|
+
function run(fixture, args, { input, extraEnv = {}, cwd = fixture.root } = {}) {
|
|
187
121
|
return spawnSync('bash', args, {
|
|
188
|
-
cwd
|
|
122
|
+
cwd,
|
|
189
123
|
encoding: 'utf8',
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
PATH: `${fixture.fakeBin}:${process.env.PATH}`,
|
|
193
|
-
COMMAND_LOG: fixture.commandLog,
|
|
194
|
-
...extraEnv,
|
|
195
|
-
},
|
|
124
|
+
input,
|
|
125
|
+
env: { ...process.env, PATH: `${fixture.fakeBin}:${process.env.PATH}`, COMMAND_LOG: fixture.commandLog, ...extraEnv },
|
|
196
126
|
});
|
|
197
127
|
}
|
|
198
128
|
|
|
199
129
|
function runBuild(fixture, extraEnv = {}, ...extraArgs) {
|
|
200
|
-
return run(fixture
|
|
130
|
+
return run(fixture, ['deploy.sh', 'build', ...extraArgs], { extraEnv });
|
|
201
131
|
}
|
|
202
132
|
|
|
203
133
|
async function releaseVersion(root) {
|
|
@@ -208,30 +138,44 @@ function runValidator(root, version) {
|
|
|
208
138
|
return spawnSync(process.execPath, [
|
|
209
139
|
validator,
|
|
210
140
|
join(root, 'deploy/deploy.sh'),
|
|
211
|
-
join(root, `deploy/${version}/
|
|
212
|
-
join(root, `deploy/${version}/docker/docker-compose.blue-green.yml`),
|
|
141
|
+
join(root, `deploy/${version}/docker/docker-compose.yml`),
|
|
213
142
|
join(root, 'deploy'),
|
|
214
143
|
], { encoding: 'utf8' });
|
|
215
144
|
}
|
|
216
145
|
|
|
217
146
|
async function installOnServer(fixture) {
|
|
218
|
-
const serverRoot =
|
|
147
|
+
const serverRoot = await temporary('wdyy-server-root-');
|
|
219
148
|
await cp(join(fixture.root, 'deploy'), serverRoot, { recursive: true, force: true });
|
|
220
149
|
await chmod(join(serverRoot, 'deploy.sh'), 0o755);
|
|
221
150
|
return serverRoot;
|
|
222
151
|
}
|
|
223
152
|
|
|
224
|
-
function runServer(fixture, serverRoot,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
NGINX_FAIL_MARKER: join(fixture.root, 'nginx-failed-once'),
|
|
230
|
-
...extraEnv,
|
|
153
|
+
function runServer(fixture, serverRoot, choice, extraEnv = {}, args = []) {
|
|
154
|
+
return run(fixture, [join(serverRoot, 'deploy.sh'), ...args], {
|
|
155
|
+
cwd: fixture.root,
|
|
156
|
+
input: `${choice}\n`,
|
|
157
|
+
extraEnv,
|
|
231
158
|
});
|
|
232
159
|
}
|
|
233
160
|
|
|
234
|
-
|
|
161
|
+
async function rewriteManifest(versionRoot) {
|
|
162
|
+
const files = [];
|
|
163
|
+
async function walk(current) {
|
|
164
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
165
|
+
const path = join(current, entry.name);
|
|
166
|
+
if (entry.isDirectory()) await walk(path);
|
|
167
|
+
else if (entry.isFile() && entry.name !== 'manifest.sha256') files.push(path);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
await walk(versionRoot);
|
|
171
|
+
const lines = await Promise.all(files.sort().map(async (path) => {
|
|
172
|
+
const relativePath = path.slice(versionRoot.length + 1).split('\\').join('/');
|
|
173
|
+
return `${createHash('sha256').update(await readFile(path)).digest('hex')} ${relativePath}`;
|
|
174
|
+
}));
|
|
175
|
+
await writeFile(join(versionRoot, 'manifest.sha256'), `${lines.join('\n')}\n`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
test('build 导出带版本号双镜像和双服务完整包', async () => {
|
|
235
179
|
const fixture = await createFixture();
|
|
236
180
|
const result = runBuild(fixture);
|
|
237
181
|
assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
@@ -243,64 +187,27 @@ test('build 生成根 .env、双镜像和四服务完整包', async () => {
|
|
|
243
187
|
]);
|
|
244
188
|
const validation = runValidator(fixture.root, version);
|
|
245
189
|
assert.equal(validation.status, 0, validation.stderr);
|
|
246
|
-
assert.match(validation.stdout, /valid dual-image deployment package/);
|
|
190
|
+
assert.match(validation.stdout, /valid direct dual-image deployment package/);
|
|
247
191
|
const log = await readFile(fixture.commandLog, 'utf8');
|
|
248
192
|
assert.match(log, new RegExp(`--tag fixture_frontend:${version}`));
|
|
249
193
|
assert.match(log, new RegExp(`--tag fixture_backend:${version}`));
|
|
250
|
-
|
|
251
|
-
assert.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
test('none 模式不伪造数据库文件,manual 模式只打包人工脚本', async () => {
|
|
255
|
-
const none = await createFixture();
|
|
256
|
-
assert.equal(runBuild(none).status, 0);
|
|
257
|
-
const noneVersion = await releaseVersion(none.root);
|
|
258
|
-
await assert.rejects(stat(join(none.root, `deploy/${noneVersion}/database`)));
|
|
259
|
-
|
|
260
|
-
const manual = await createFixture({ mode: 'manual' });
|
|
261
|
-
const built = runBuild(manual);
|
|
262
|
-
assert.equal(built.status, 0, built.stderr);
|
|
263
|
-
assert.match(built.stdout, /migration was not executed/);
|
|
264
|
-
const manualVersion = await releaseVersion(manual.root);
|
|
265
|
-
assert.ok((await stat(join(manual.root, `deploy/${manualVersion}/database/apply-migrations.sh`))).mode & 0o111);
|
|
266
|
-
assert.doesNotMatch(await readFile(join(manual.root, 'deploy/deploy.sh'), 'utf8'), /\bpsql\b/);
|
|
194
|
+
const compose = await readFile(join(fixture.root, `deploy/${version}/docker/docker-compose.yml`), 'utf8');
|
|
195
|
+
assert.deepEqual([...compose.matchAll(/^ ([a-z-]+):$/gm)].map((match) => match[1]), ['frontend', 'backend']);
|
|
196
|
+
assert.doesNotMatch(compose, /blue|green|PROJECT_HTTP_PORT|nginx\/site/);
|
|
267
197
|
});
|
|
268
198
|
|
|
269
|
-
test('
|
|
199
|
+
test('连续构建保持 deploy.sh 与 Compose 字节一致', async () => {
|
|
270
200
|
const fixture = await createFixture();
|
|
271
201
|
assert.equal(runBuild(fixture).status, 0);
|
|
272
|
-
const
|
|
273
|
-
const
|
|
202
|
+
const first = await releaseVersion(fixture.root);
|
|
203
|
+
const hashes = await Promise.all(['deploy/deploy.sh', `deploy/${first}/docker/docker-compose.yml`].map(async (path) => createHash('sha256').update(await readFile(join(fixture.root, path))).digest('hex')));
|
|
274
204
|
assert.equal(runBuild(fixture).status, 0);
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
assert.deepEqual(
|
|
205
|
+
const second = await releaseVersion(fixture.root);
|
|
206
|
+
assert.notEqual(second, first);
|
|
207
|
+
const nextHashes = await Promise.all(['deploy/deploy.sh', `deploy/${second}/docker/docker-compose.yml`].map(async (path) => createHash('sha256').update(await readFile(join(fixture.root, path))).digest('hex')));
|
|
208
|
+
assert.deepEqual(nextHashes, hashes);
|
|
279
209
|
});
|
|
280
210
|
|
|
281
|
-
async function configurationHashes(root, version) {
|
|
282
|
-
const paths = ['deploy/deploy.sh', `deploy/${version}/docker/docker-compose.blue-green.yml`, `deploy/${version}/nginx/site.conf`];
|
|
283
|
-
return Promise.all(paths.map(async (path) => createHash('sha256').update(await readFile(join(root, path))).digest('hex')));
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function rewriteManifest(versionRoot, directory = versionRoot) {
|
|
287
|
-
const files = [];
|
|
288
|
-
async function walk(current) {
|
|
289
|
-
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
290
|
-
const path = join(current, entry.name);
|
|
291
|
-
if (entry.isDirectory()) await walk(path);
|
|
292
|
-
else if (entry.isFile() && entry.name !== 'manifest.sha256') files.push(path);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
await walk(directory);
|
|
296
|
-
const lines = await Promise.all(files.sort().map(async (path) => {
|
|
297
|
-
const relative = path.slice(versionRoot.length + 1).split('\\').join('/');
|
|
298
|
-
const hash = createHash('sha256').update(await readFile(path)).digest('hex');
|
|
299
|
-
return `${hash} ${relative}`;
|
|
300
|
-
}));
|
|
301
|
-
await writeFile(join(versionRoot, 'manifest.sha256'), `${lines.join('\n')}\n`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
211
|
test('失败构建保留上一发布包和发布账本', async () => {
|
|
305
212
|
const fixture = await createFixture();
|
|
306
213
|
assert.equal(runBuild(fixture).status, 0);
|
|
@@ -312,257 +219,148 @@ test('失败构建保留上一发布包和发布账本', async () => {
|
|
|
312
219
|
assert.equal(await readFile(join(fixture.root, 'AGENTS.md'), 'utf8'), agents);
|
|
313
220
|
});
|
|
314
221
|
|
|
315
|
-
test('
|
|
316
|
-
const fixture = await createFixture({ extraEnv: 'UNUSED_VALUE=$(touch should-not-exist)\nPATH=/malicious\n' });
|
|
317
|
-
const result = runBuild(fixture);
|
|
318
|
-
assert.equal(result.status, 0, result.stderr);
|
|
319
|
-
await assert.rejects(access(join(fixture.root, 'should-not-exist')));
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test('build 接受带标签基础镜像并拒绝重复键和额外版本参数', async () => {
|
|
323
|
-
const duplicate = await createFixture({ extraEnv: 'PROJECT_NAME=duplicate\n' });
|
|
324
|
-
assert.notEqual(runBuild(duplicate).status, 0);
|
|
325
|
-
const tagged = await createFixture();
|
|
326
|
-
assert.equal(runBuild(tagged).status, 0);
|
|
222
|
+
test('build 拒绝额外参数、旧部署键和日志目录配置', async () => {
|
|
327
223
|
const extra = await createFixture();
|
|
328
224
|
assert.equal(runBuild(extra, {}, '20260808-001').status, 2);
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
assert.notEqual(legacyRejected.status, 0);
|
|
336
|
-
assert.match(legacyRejected.stderr, /legacy duplicate environment key/);
|
|
337
|
-
const reverseInclude = await createFixture();
|
|
338
|
-
await writeFile(join(reverseInclude.root, '.dockerignore'), `${await readFile(join(reverseInclude.root, '.dockerignore'), 'utf8')}!.env\n`);
|
|
339
|
-
const reverseRejected = runBuild(reverseInclude);
|
|
340
|
-
assert.notEqual(reverseRejected.status, 0);
|
|
341
|
-
assert.match(reverseRejected.stderr, /forbidden reverse include/);
|
|
225
|
+
for (const setting of ['PROJECT_HTTP_PORT=9099\n', 'FRONTEND_BLUE_PORT=19091\n', 'LOG_DIR=/tmp/logs\n', 'DATABASE_MIGRATION_MODE=none\n']) {
|
|
226
|
+
const fixture = await createFixture({ extraEnv: setting });
|
|
227
|
+
const result = runBuild(fixture);
|
|
228
|
+
assert.notEqual(result.status, 0);
|
|
229
|
+
assert.match(result.stderr, /removed deployment configuration/);
|
|
230
|
+
}
|
|
342
231
|
});
|
|
343
232
|
|
|
344
|
-
test('validator
|
|
233
|
+
test('validator 拒绝镜像 fallback、嵌套日志和符号链接', async () => {
|
|
345
234
|
const fixture = await createFixture();
|
|
346
235
|
assert.equal(runBuild(fixture).status, 0);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
assert.notEqual(runValidator(fixture.root, version).status, 0);
|
|
350
|
-
|
|
351
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
352
|
-
const next = await releaseVersion(fixture.root);
|
|
353
|
-
const composePath = join(fixture.root, `deploy/${next}/docker/docker-compose.blue-green.yml`);
|
|
236
|
+
let version = await releaseVersion(fixture.root);
|
|
237
|
+
let composePath = join(fixture.root, `deploy/${version}/docker/docker-compose.yml`);
|
|
354
238
|
await writeFile(composePath, (await readFile(composePath, 'utf8')).replace('${FRONTEND_IMAGE:?FRONTEND_IMAGE is required}', '${FRONTEND_IMAGE:-fallback}').replace('../../logs:/app/logs', '../../logs/frontend:/app/logs'));
|
|
355
|
-
|
|
239
|
+
let validation = runValidator(fixture.root, version);
|
|
356
240
|
assert.notEqual(validation.status, 0);
|
|
357
241
|
assert.match(validation.stderr, /fallback|logs/);
|
|
358
|
-
});
|
|
359
242
|
|
|
360
|
-
test('validator 拒绝版本目录符号链接', async () => {
|
|
361
|
-
const fixture = await createFixture();
|
|
362
243
|
assert.equal(runBuild(fixture).status, 0);
|
|
363
|
-
|
|
244
|
+
version = await releaseVersion(fixture.root);
|
|
364
245
|
await symlink('../outside', join(fixture.root, `deploy/${version}/linked-file`));
|
|
365
|
-
|
|
246
|
+
validation = runValidator(fixture.root, version);
|
|
366
247
|
assert.notEqual(validation.status, 0);
|
|
367
248
|
assert.match(validation.stderr, /symbolic links/);
|
|
368
249
|
});
|
|
369
250
|
|
|
370
|
-
test('
|
|
251
|
+
test('菜单可独立运行或替换前端且记录成功日志', async () => {
|
|
371
252
|
const fixture = await createFixture();
|
|
372
253
|
assert.equal(runBuild(fixture).status, 0);
|
|
373
254
|
const version = await releaseVersion(fixture.root);
|
|
374
|
-
const versionRoot = join(fixture.root, `deploy/${version}`);
|
|
375
|
-
const badArchiveRoot = await temporary('bad-image-');
|
|
376
|
-
await writeFile(join(badArchiveRoot, 'manifest.json'), `[{"Config":"config.json","RepoTags":["fixture_frontend:${version}","other_frontend:bad"],"Layers":[]}]\n`);
|
|
377
|
-
await writeFile(join(badArchiveRoot, 'config.json'), '{}\n');
|
|
378
|
-
const tarResult = spawnSync('/usr/bin/tar', ['-cf', join(versionRoot, 'frontend-image.tar'), '-C', badArchiveRoot, 'manifest.json', 'config.json'], { encoding: 'utf8' });
|
|
379
|
-
assert.equal(tarResult.status, 0, tarResult.stderr);
|
|
380
|
-
await rewriteManifest(versionRoot);
|
|
381
255
|
const serverRoot = await installOnServer(fixture);
|
|
382
256
|
await writeFile(fixture.commandLog, '');
|
|
383
|
-
const
|
|
384
|
-
assert.
|
|
385
|
-
|
|
386
|
-
assert.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
392
|
-
const first = await releaseVersion(fixture.root);
|
|
393
|
-
const serverRoot = await installOnServer(fixture);
|
|
394
|
-
const started = runServer(fixture, serverRoot, 'start', { EXPECTED_VERSION: first });
|
|
395
|
-
assert.equal(started.status, 0, started.stderr);
|
|
396
|
-
assert.match(await readFile(join(serverRoot, 'state/active.env'), 'utf8'), new RegExp(`ACTIVE_COLOR=blue[\\s\\S]*${first}`));
|
|
397
|
-
|
|
398
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
399
|
-
const second = await releaseVersion(fixture.root);
|
|
400
|
-
await cp(join(fixture.root, 'deploy'), serverRoot, { recursive: true, force: true });
|
|
401
|
-
const replaced = runServer(fixture, serverRoot, 'replace', { EXPECTED_VERSION: second });
|
|
402
|
-
assert.equal(replaced.status, 0, replaced.stderr);
|
|
403
|
-
assert.match(await readFile(join(serverRoot, 'state/active.env'), 'utf8'), /ACTIVE_COLOR=green/);
|
|
404
|
-
assert.match(await readFile(join(serverRoot, 'state/previous.env'), 'utf8'), new RegExp(first));
|
|
405
|
-
const upstream = await readFile(join(serverRoot, 'nginx/active-upstreams.conf'), 'utf8');
|
|
406
|
-
assert.match(upstream, /fixture_frontend_active/);
|
|
407
|
-
assert.match(upstream, /fixture_backend_active/);
|
|
408
|
-
|
|
409
|
-
const rolledBack = runServer(fixture, serverRoot, 'rollback', { EXPECTED_VERSION: first }, first);
|
|
410
|
-
assert.equal(rolledBack.status, 0, rolledBack.stderr);
|
|
411
|
-
assert.match(await readFile(join(serverRoot, 'state/active.env'), 'utf8'), new RegExp(first));
|
|
257
|
+
const result = runServer(fixture, serverRoot, '1', { EXPECTED_VERSION: version });
|
|
258
|
+
assert.equal(result.status, 0, result.stderr);
|
|
259
|
+
const commands = await readFile(fixture.commandLog, 'utf8');
|
|
260
|
+
assert.match(commands, /docker load -i .*frontend-image\.tar/);
|
|
261
|
+
assert.doesNotMatch(commands, /docker load -i .*backend-image\.tar/);
|
|
262
|
+
assert.match(commands, /compose .* up -d --force-recreate --no-deps frontend/);
|
|
263
|
+
assert.doesNotMatch(commands, /--no-deps backend/);
|
|
264
|
+
assert.match(await readFile(join(serverRoot, 'logs/deploy.log'), 'utf8'), new RegExp(`version=${version} operation=run-or-replace-frontend result=success`));
|
|
412
265
|
});
|
|
413
266
|
|
|
414
|
-
test('
|
|
267
|
+
test('菜单可独立运行后端、同时运行前后端并同时停止', async () => {
|
|
415
268
|
const fixture = await createFixture();
|
|
416
269
|
assert.equal(runBuild(fixture).status, 0);
|
|
417
270
|
const version = await releaseVersion(fixture.root);
|
|
418
271
|
const serverRoot = await installOnServer(fixture);
|
|
419
|
-
|
|
272
|
+
|
|
420
273
|
await writeFile(fixture.commandLog, '');
|
|
421
|
-
assert.equal(runServer(fixture, serverRoot, '
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
assert.
|
|
425
|
-
assert.match(
|
|
426
|
-
const log = await readFile(fixture.commandLog, 'utf8');
|
|
427
|
-
assert.match(log, /up -d --force-recreate frontend-blue backend-blue/);
|
|
428
|
-
assert.match(log, /stop frontend-blue backend-blue/);
|
|
429
|
-
});
|
|
274
|
+
assert.equal(runServer(fixture, serverRoot, '2', { EXPECTED_VERSION: version }).status, 0);
|
|
275
|
+
let commands = await readFile(fixture.commandLog, 'utf8');
|
|
276
|
+
assert.match(commands, /docker load -i .*backend-image\.tar/);
|
|
277
|
+
assert.doesNotMatch(commands, /docker load -i .*frontend-image\.tar/);
|
|
278
|
+
assert.match(commands, /--no-deps backend/);
|
|
430
279
|
|
|
431
|
-
test('remove 只删除本项目容器、网络和两个镜像仓库并保留日志', async () => {
|
|
432
|
-
const fixture = await createFixture();
|
|
433
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
434
|
-
const serverRoot = await installOnServer(fixture);
|
|
435
|
-
await mkdir(join(serverRoot, 'logs'), { recursive: true });
|
|
436
|
-
await writeFile(join(serverRoot, 'logs/backend.log'), 'keep\n');
|
|
437
280
|
await writeFile(fixture.commandLog, '');
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
assert.match(log, /docker rm -f c111 c222/);
|
|
442
|
-
assert.match(log, /docker network rm n111/);
|
|
443
|
-
assert.match(log, /docker image rm -f fixture_backend:20260808-001 fixture_frontend:20260808-001|docker image rm -f fixture_frontend:20260808-001 fixture_backend:20260808-001/);
|
|
444
|
-
assert.doesNotMatch(log, /other_project_frontend:shared/);
|
|
445
|
-
assert.equal(await readFile(join(serverRoot, 'logs/backend.log'), 'utf8'), 'keep\n');
|
|
446
|
-
await access(join(serverRoot, 'release.env'));
|
|
447
|
-
});
|
|
281
|
+
assert.equal(runServer(fixture, serverRoot, '3', { EXPECTED_VERSION: version }).status, 0);
|
|
282
|
+
commands = await readFile(fixture.commandLog, 'utf8');
|
|
283
|
+
assert.match(commands, /up -d --force-recreate frontend backend/);
|
|
448
284
|
|
|
449
|
-
test('端口冲突在加载镜像或启动容器前失败', async () => {
|
|
450
|
-
const fixture = await createFixture();
|
|
451
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
452
|
-
const version = await releaseVersion(fixture.root);
|
|
453
|
-
const serverRoot = await installOnServer(fixture);
|
|
454
|
-
await mkdir(join(fixture.serverProjectsRoot, 'other/nginx'), { recursive: true });
|
|
455
|
-
await writeFile(join(fixture.serverProjectsRoot, 'other/nginx/site.conf'), '# wdyy-project: other\n# wdyy-port: 9099\nserver { listen 9099; }\n');
|
|
456
285
|
await writeFile(fixture.commandLog, '');
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
assert.match(
|
|
460
|
-
const
|
|
461
|
-
assert.
|
|
286
|
+
assert.equal(runServer(fixture, serverRoot, '4').status, 0);
|
|
287
|
+
commands = await readFile(fixture.commandLog, 'utf8');
|
|
288
|
+
assert.match(commands, /stop frontend backend/);
|
|
289
|
+
const operationLog = await readFile(join(serverRoot, 'logs/deploy.log'), 'utf8');
|
|
290
|
+
assert.match(operationLog, /operation=run-or-replace-backend result=success/);
|
|
291
|
+
assert.match(operationLog, /operation=run-or-replace-both result=success/);
|
|
292
|
+
assert.match(operationLog, /operation=stop-both result=success/);
|
|
462
293
|
});
|
|
463
294
|
|
|
464
|
-
test('
|
|
295
|
+
test('健康检查失败返回非零并记录失败,不自动回滚', async () => {
|
|
465
296
|
const fixture = await createFixture();
|
|
466
297
|
assert.equal(runBuild(fixture).status, 0);
|
|
467
298
|
const version = await releaseVersion(fixture.root);
|
|
468
299
|
const serverRoot = await installOnServer(fixture);
|
|
469
|
-
|
|
470
|
-
const result = runServer(fixture, serverRoot, 'start', {
|
|
471
|
-
EXPECTED_VERSION: version,
|
|
472
|
-
EXTRA_NGINX_CONFIG: 'server {\n listen 9099;\n}',
|
|
473
|
-
});
|
|
300
|
+
const result = runServer(fixture, serverRoot, '2', { EXPECTED_VERSION: version, FAIL_HEALTH: 'backend' });
|
|
474
301
|
assert.notEqual(result.status, 0);
|
|
475
|
-
|
|
476
|
-
assert.
|
|
302
|
+
const commands = await readFile(fixture.commandLog, 'utf8');
|
|
303
|
+
assert.match(commands, /up -d --force-recreate --no-deps backend/);
|
|
304
|
+
assert.doesNotMatch(commands, /docker .*rollback|nginx -(?:t|s)/);
|
|
305
|
+
assert.match(await readFile(join(serverRoot, 'logs/deploy.log'), 'utf8'), /operation=run-or-replace-backend result=failure/);
|
|
477
306
|
});
|
|
478
307
|
|
|
479
|
-
test('
|
|
308
|
+
test('非法菜单输入和额外命令参数不执行 Docker 状态变更', async () => {
|
|
480
309
|
const fixture = await createFixture();
|
|
481
310
|
assert.equal(runBuild(fixture).status, 0);
|
|
482
|
-
const version = await releaseVersion(fixture.root);
|
|
483
311
|
const serverRoot = await installOnServer(fixture);
|
|
484
|
-
await mkdir(join(fixture.serverProjectsRoot, 'other/nginx'), { recursive: true });
|
|
485
|
-
await writeFile(
|
|
486
|
-
join(fixture.serverProjectsRoot, 'other/nginx/site.conf'),
|
|
487
|
-
'# wdyy-project: other\n# wdyy-port: 9100\n# wdyy-docker-ports: 19091,29092,29093,29094\nserver { listen 9100; }\n',
|
|
488
|
-
);
|
|
489
312
|
await writeFile(fixture.commandLog, '');
|
|
490
|
-
const
|
|
491
|
-
assert.notEqual(
|
|
492
|
-
assert.
|
|
493
|
-
|
|
313
|
+
const invalid = runServer(fixture, serverRoot, '9');
|
|
314
|
+
assert.notEqual(invalid.status, 0);
|
|
315
|
+
assert.doesNotMatch(await readFile(fixture.commandLog, 'utf8'), /docker (?:load|compose)/);
|
|
316
|
+
const extra = runServer(fixture, serverRoot, '1', {}, ['start']);
|
|
317
|
+
assert.equal(extra.status, 2);
|
|
494
318
|
});
|
|
495
319
|
|
|
496
|
-
test('
|
|
320
|
+
test('服务器拒绝标签不匹配的镜像归档且不执行 docker load', async () => {
|
|
497
321
|
const fixture = await createFixture();
|
|
498
322
|
assert.equal(runBuild(fixture).status, 0);
|
|
499
323
|
const version = await releaseVersion(fixture.root);
|
|
324
|
+
const versionRoot = join(fixture.root, `deploy/${version}`);
|
|
325
|
+
const badArchiveRoot = await temporary('bad-image-');
|
|
326
|
+
await writeFile(join(badArchiveRoot, 'manifest.json'), `[{"Config":"config.json","RepoTags":["fixture_frontend:${version}","other_frontend:bad"],"Layers":[]}]\n`);
|
|
327
|
+
await writeFile(join(badArchiveRoot, 'config.json'), '{}\n');
|
|
328
|
+
assert.equal(spawnSync('/usr/bin/tar', ['-cf', join(versionRoot, 'frontend-image.tar'), '-C', badArchiveRoot, 'manifest.json', 'config.json']).status, 0);
|
|
329
|
+
await rewriteManifest(versionRoot);
|
|
500
330
|
const serverRoot = await installOnServer(fixture);
|
|
501
331
|
await writeFile(fixture.commandLog, '');
|
|
502
|
-
const result = runServer(fixture, serverRoot, '
|
|
503
|
-
EXPECTED_VERSION: version,
|
|
504
|
-
DOCKER_RUNNING_PORTS: 'other_project|0.0.0.0:19093->3000/tcp, [::]:19093->3000/tcp',
|
|
505
|
-
});
|
|
332
|
+
const result = runServer(fixture, serverRoot, '1', { EXPECTED_VERSION: version });
|
|
506
333
|
assert.notEqual(result.status, 0);
|
|
507
|
-
assert.match(result.stderr, /
|
|
508
|
-
assert.doesNotMatch(await readFile(fixture.commandLog, 'utf8'), /docker load
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
test('replace 的 Nginx reload 失败时恢复旧 upstream 和状态', async () => {
|
|
512
|
-
const fixture = await createFixture();
|
|
513
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
514
|
-
const first = await releaseVersion(fixture.root);
|
|
515
|
-
const serverRoot = await installOnServer(fixture);
|
|
516
|
-
assert.equal(runServer(fixture, serverRoot, 'start', { EXPECTED_VERSION: first }).status, 0);
|
|
517
|
-
const oldUpstream = await readFile(join(serverRoot, 'nginx/active-upstreams.conf'), 'utf8');
|
|
518
|
-
const oldState = await readFile(join(serverRoot, 'state/active.env'), 'utf8');
|
|
519
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
520
|
-
const second = await releaseVersion(fixture.root);
|
|
521
|
-
await cp(join(fixture.root, 'deploy'), serverRoot, { recursive: true, force: true });
|
|
522
|
-
const failed = runServer(fixture, serverRoot, 'replace', { EXPECTED_VERSION: second, FAIL_NGINX_RELOAD_ONCE: '1' });
|
|
523
|
-
assert.notEqual(failed.status, 0);
|
|
524
|
-
assert.equal(await readFile(join(serverRoot, 'nginx/active-upstreams.conf'), 'utf8'), oldUpstream);
|
|
525
|
-
assert.equal(await readFile(join(serverRoot, 'state/active.env'), 'utf8'), oldState);
|
|
334
|
+
assert.match(result.stderr, /exactly one expected tag/);
|
|
335
|
+
assert.doesNotMatch(await readFile(fixture.commandLog, 'utf8'), /docker load/);
|
|
526
336
|
});
|
|
527
337
|
|
|
528
|
-
test('
|
|
338
|
+
test('服务器部署根由脚本物理路径确定', async () => {
|
|
529
339
|
const fixture = await createFixture();
|
|
530
340
|
assert.equal(runBuild(fixture).status, 0);
|
|
531
341
|
const version = await releaseVersion(fixture.root);
|
|
532
342
|
const serverRoot = await installOnServer(fixture);
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
assert.
|
|
536
|
-
await
|
|
537
|
-
await assert.rejects(access(join(
|
|
538
|
-
await assert.rejects(access(join(serverRoot, 'nginx/active-upstreams.conf')));
|
|
343
|
+
const unrelated = await temporary('unrelated-cwd-');
|
|
344
|
+
const result = run(fixture, [join(serverRoot, 'deploy.sh')], { cwd: unrelated, input: '1\n', extraEnv: { EXPECTED_VERSION: version } });
|
|
345
|
+
assert.equal(result.status, 0, result.stderr);
|
|
346
|
+
await access(join(serverRoot, 'logs/deploy.log'));
|
|
347
|
+
await assert.rejects(access(join(unrelated, 'logs/deploy.log')));
|
|
539
348
|
});
|
|
540
349
|
|
|
541
|
-
test('
|
|
350
|
+
test('validator 拒绝缺失镜像和宿主机 Nginx 文件', async () => {
|
|
542
351
|
const fixture = await createFixture();
|
|
543
352
|
assert.equal(runBuild(fixture).status, 0);
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
assert.
|
|
547
|
-
const oldUpstream = await readFile(join(serverRoot, 'nginx/active-upstreams.conf'), 'utf8');
|
|
548
|
-
const oldState = await readFile(join(serverRoot, 'state/active.env'), 'utf8');
|
|
549
|
-
assert.equal(runBuild(fixture).status, 0);
|
|
550
|
-
const second = await releaseVersion(fixture.root);
|
|
551
|
-
await cp(join(fixture.root, 'deploy'), serverRoot, { recursive: true, force: true });
|
|
552
|
-
const failed = runServer(fixture, serverRoot, 'replace', { EXPECTED_VERSION: second, FAIL_STATE_INSTALL: '1' });
|
|
553
|
-
assert.notEqual(failed.status, 0);
|
|
554
|
-
assert.equal(await readFile(join(serverRoot, 'nginx/active-upstreams.conf'), 'utf8'), oldUpstream);
|
|
555
|
-
assert.equal(await readFile(join(serverRoot, 'state/active.env'), 'utf8'), oldState);
|
|
556
|
-
});
|
|
353
|
+
let version = await releaseVersion(fixture.root);
|
|
354
|
+
await unlink(join(fixture.root, `deploy/${version}/frontend-image.tar`));
|
|
355
|
+
assert.notEqual(runValidator(fixture.root, version).status, 0);
|
|
557
356
|
|
|
558
|
-
test('服务器命令拒绝额外参数和错误部署根', async () => {
|
|
559
|
-
const fixture = await createFixture();
|
|
560
357
|
assert.equal(runBuild(fixture).status, 0);
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
await
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
assert.
|
|
358
|
+
version = await releaseVersion(fixture.root);
|
|
359
|
+
const nginxDir = join(fixture.root, `deploy/${version}/nginx`);
|
|
360
|
+
await mkdir(nginxDir);
|
|
361
|
+
await writeFile(join(nginxDir, 'site.conf'), 'server {}\n');
|
|
362
|
+
await rewriteManifest(join(fixture.root, `deploy/${version}`));
|
|
363
|
+
const validation = runValidator(fixture.root, version);
|
|
364
|
+
assert.notEqual(validation.status, 0);
|
|
365
|
+
assert.match(validation.stderr, /must not contain host Nginx/);
|
|
568
366
|
});
|