forked from wangziqi/gongxue-base
197 lines
10 KiB
JavaScript
197 lines
10 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const root = process.cwd();
|
|
|
|
function read(relativePath) {
|
|
return fs.readFileSync(path.join(root, relativePath), 'utf8').replace(/\r\n/g, '\n');
|
|
}
|
|
|
|
function section(source, startMarker, endMarker) {
|
|
const start = source.indexOf(startMarker);
|
|
assert.notEqual(start, -1, `missing section start: ${startMarker}`);
|
|
const end = source.indexOf(endMarker, start + startMarker.length);
|
|
assert.notEqual(end, -1, `missing section end: ${endMarker}`);
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
function ordered(source, markers, label) {
|
|
let cursor = 0;
|
|
for (const marker of markers) {
|
|
const index = source.indexOf(marker, cursor);
|
|
assert.notEqual(index, -1, `${label}: missing or out-of-order marker: ${marker}`);
|
|
cursor = index + marker.length;
|
|
}
|
|
}
|
|
|
|
const rootDeploy = read('deploy.sh');
|
|
const compatDeploy = read('scripts/deploy/bin/deploy.sh');
|
|
const rootEnv = read('deploy.env.example');
|
|
const compatEnv = read('scripts/deploy/env/deploy.env.example');
|
|
const packageJson = read('package.json');
|
|
const taroPackageJson = read('apps/taro/package.json');
|
|
const apiService = read('scripts/deploy/systemd/tiku-api.service');
|
|
const workerService = read('scripts/deploy/systemd/tiku-worker@.service');
|
|
const workerTarget = read('scripts/deploy/systemd/tiku-workers.target');
|
|
const apiEnv = read('scripts/deploy/env/api.env.example');
|
|
const workerEnv = read('scripts/deploy/env/worker.env.example');
|
|
const runtimeRoleBootstrap = read('scripts/deploy/sql/bootstrap-backend-runtime-roles.sql');
|
|
const deployReadme = read('scripts/deploy/README.md');
|
|
|
|
assert.match(rootDeploy, /: "\$\{WWW_ROOT:=\/srv\/tiku-saas\/www\}"/);
|
|
assert.match(rootDeploy, /: "\$\{SERVICE_REPO_DIR:=\/opt\/tiku-saas\/repo\}"/);
|
|
assert.match(rootDeploy, /: "\$\{SERVICE_MODE:=systemd\}"/);
|
|
assert.match(rootDeploy, /: "\$\{HEALTHCHECK_URL:=http:\/\/127\.0\.0\.1:8787\/health\}"/);
|
|
assert.match(rootDeploy, /Production deployment requires a service restart strategy/);
|
|
assert.match(rootDeploy, /Production deployment requires HEALTHCHECK_URL/);
|
|
assert.match(rootDeploy, /Production systemd deployment requires SYNC_SERVICE_REPO=true/);
|
|
assert.match(rootDeploy, /Production deployment requires RUN_DB_READINESS=true before launch gate/);
|
|
assert.match(compatDeploy, /Production deployment requires RUN_DB_READINESS=true before launch gate/);
|
|
assert.match(rootDeploy, /Production deployment requires RUN_TARO_SUPPLY_CHAIN_AUDIT=true/);
|
|
assert.match(compatDeploy, /Production deployment requires RUN_TARO_SUPPLY_CHAIN_AUDIT=true/);
|
|
assert.match(rootDeploy, /Production database migrations require a separate DATABASE_MIGRATION_URL/);
|
|
assert.match(compatDeploy, /Production database migrations require a separate DATABASE_MIGRATION_URL/);
|
|
assert.match(rootDeploy, /supabase db push --db-url \\"\\\$DATABASE_MIGRATION_URL\\"/);
|
|
assert.match(compatDeploy, /supabase db push --db-url \\"\\\$DATABASE_MIGRATION_URL\\"/);
|
|
assert.match(compatDeploy, /source "\$API_ENV_FILE"/);
|
|
assert.ok(rootDeploy.includes('LOCK_DIR="$DEPLOY_ROOT/.deploy.lock"'));
|
|
assert.ok(compatDeploy.includes('LOCK_DIR="${LOCK_DIR:-$APP_ROOT/.deploy.lock}"'));
|
|
assert.match(rootDeploy, /LOCK_ACQUIRED=false/);
|
|
assert.match(compatDeploy, /LOCK_ACQUIRED=false/);
|
|
assert.match(compatDeploy, /export GIT_TERMINAL_PROMPT=0/);
|
|
assert.match(rootDeploy, /NPM_INSTALL_COMMAND:=npm ci --workspaces --include-workspace-root --include=dev/);
|
|
assert.match(rootDeploy, /NPM_INSTALL_COMMAND must allow the reviewed Taro workspace postinstall patches/);
|
|
assert.doesNotMatch(rootEnv, /--ignore-scripts/);
|
|
assert.match(rootEnv, /^NPM_INSTALL_COMMAND="npm ci --workspaces --include-workspace-root --include=dev"$/m);
|
|
assert.match(compatDeploy, /npm --prefix "\$SOURCE_REPO_DIR" ci[\s\S]*--workspaces[\s\S]*--include-workspace-root[\s\S]*--include=dev/);
|
|
assert.doesNotMatch(compatDeploy, /npm --prefix "\$SOURCE_REPO_DIR" ci[\s\S]*--ignore-scripts/);
|
|
assert.match(taroPackageJson, /"postinstall": "node \.\.\/\.\.\/scripts\/taro-components-h5-runtime-patch\.js --apply"/);
|
|
assert.match(apiService, /WorkingDirectory=\/opt\/tiku-saas\/repo/);
|
|
assert.match(workerService, /WorkingDirectory=\/opt\/tiku-saas\/repo/);
|
|
assert.match(workerService, /ExecStart=.*--loop --job %i/);
|
|
assert.match(workerTarget, /Requires=tiku-worker@crm\.service/);
|
|
assert.match(workerTarget, /Wants=tiku-worker-monthly-usage\.timer/);
|
|
|
|
for (const [dist, target] of [
|
|
['h5-student', 'student'],
|
|
['h5-tenant-admin', 'tenant-admin'],
|
|
['h5-platform-admin', 'platform-admin'],
|
|
]) {
|
|
assert.ok(
|
|
rootDeploy.includes(`$release/apps/taro/dist/${dist}/\" \"$staging/${target}/`),
|
|
`root deploy must stage ${dist} as ${target}`,
|
|
);
|
|
assert.ok(
|
|
compatDeploy.includes(`$CANDIDATE_RELEASE/apps/taro/dist/${dist}/\" \"$staging/${target}/`),
|
|
`compat deploy must stage ${dist} as ${target}`,
|
|
);
|
|
}
|
|
|
|
const rootMain = section(rootDeploy, 'main() {', '\n}\n\nmain "$@"');
|
|
ordered(rootMain, [
|
|
'run_build_and_checks "$NEW_RELEASE"',
|
|
'stage_h5_release "$NEW_RELEASE"',
|
|
'ROLLBACK_ARMED=true',
|
|
'switch_current "$NEW_RELEASE"',
|
|
'sync_service_repo "$NEW_RELEASE"',
|
|
'restart_services',
|
|
'healthcheck',
|
|
'switch_www_release',
|
|
'verify_live_h5_release "$NEW_RELEASE"',
|
|
], 'root activation order');
|
|
assert.ok(rootDeploy.includes('DEPLOY_RELEASE_ROOT="$release"'), 'root launch gate must bind the candidate release root');
|
|
assert.ok(rootDeploy.includes('--verify-live-h5'), 'root deploy must verify the activated production H5 release');
|
|
|
|
const candidateChecks = section(compatDeploy, 'build_and_validate_candidate() {', '\n}\n\nstage_www_candidate() {');
|
|
ordered(candidateChecks, [
|
|
'npm run audit:taro:supply-chain',
|
|
'npm run build:taro:h5:student',
|
|
'npm run build:taro:h5:platform',
|
|
'install_runtime_config "$RUNTIME_CONFIG_DIR/h5-student.runtime-config.json"',
|
|
'install_runtime_config "$RUNTIME_CONFIG_DIR/h5-tenant-admin.runtime-config.json"',
|
|
'install_runtime_config "$RUNTIME_CONFIG_DIR/h5-platform-admin.runtime-config.json"',
|
|
'node scripts/taro-h5-release-guardrails-test.js --require-dist --require-runtime-config',
|
|
'npm run manifest:taro:h5 -- --require-dist --require-runtime-config',
|
|
'npm run smoke:taro:h5',
|
|
'npm run audit:runtime',
|
|
'load_runtime_env',
|
|
'npm run readiness:production',
|
|
'run_shell "$DB_MIGRATION_COMMAND"',
|
|
'npm run readiness:production:db',
|
|
'npm run launch:gate',
|
|
], 'compat candidate validation order');
|
|
assert.ok(compatDeploy.includes('DEPLOY_RELEASE_ROOT="$CANDIDATE_RELEASE"'), 'compat launch gate must bind the candidate release root');
|
|
|
|
const rootChecks = section(rootDeploy, 'run_build_and_checks() {', '\n}\n\nverify_live_h5_release() {');
|
|
ordered(rootChecks, [
|
|
'npm run audit:taro:supply-chain',
|
|
'npm run build:taro:h5:student',
|
|
'npm run audit:runtime',
|
|
'link_shared_env "$release"',
|
|
'load_runtime_env',
|
|
'npm run readiness:production',
|
|
'run_shell "$DB_MIGRATION_COMMAND"',
|
|
'npm run readiness:production:db',
|
|
'npm run launch:gate',
|
|
], 'root database gate order');
|
|
|
|
const compatMain = section(compatDeploy, 'main() {', '\n}\n\nmain "$@"');
|
|
assert.doesNotMatch(
|
|
compatMain,
|
|
/load_runtime_env/,
|
|
'production secrets must not be loaded before dependency installation and candidate builds',
|
|
);
|
|
ordered(compatMain, [
|
|
'stage_candidate "$release_name"',
|
|
'build_and_validate_candidate',
|
|
'stage_www_candidate "$release_name"',
|
|
'ROLLBACK_ARMED=true',
|
|
'atomic_symlink "$CANDIDATE_RELEASE" "$CURRENT_LINK"',
|
|
'sync_service_repo "$CANDIDATE_RELEASE"',
|
|
'restart_services',
|
|
'healthcheck',
|
|
'switch_www_release "$candidate_www"',
|
|
'verify_live_h5_release',
|
|
], 'compat activation order');
|
|
assert.ok(compatDeploy.includes('--verify-live-h5'), 'compat deploy must verify the activated production H5 release');
|
|
assert.ok(!compatDeploy.includes('"$WWW_ROOT/student/"'), 'compat deploy must not overwrite the live student directory');
|
|
assert.match(compatDeploy, /WWW_RELEASES_DIR:\=\$\{WWW_ROOT%\/\}-releases/);
|
|
assert.match(compatDeploy, /\.tmp-\$release_name/);
|
|
|
|
const compatRollback = section(compatDeploy, 'rollback() {', '\n}\n\ncleanup() {');
|
|
assert.match(compatRollback, /atomic_symlink "\$PREVIOUS_WWW_RELEASE" "\$WWW_ROOT"/);
|
|
assert.match(compatRollback, /sync_service_repo "\$PREVIOUS_APP_RELEASE"/);
|
|
assert.match(compatRollback, /restart_services/);
|
|
|
|
assert.match(rootEnv, /^WWW_ROOT=\/srv\/tiku-saas\/www$/m);
|
|
assert.match(rootEnv, /^SERVICE_REPO_DIR=\/opt\/tiku-saas\/repo$/m);
|
|
assert.match(rootEnv, /^SERVICE_MODE=systemd$/m);
|
|
assert.match(rootEnv, /^SYSTEMD_UNITS="tiku-api\.service tiku-workers\.target"$/m);
|
|
assert.match(rootEnv, /^HEALTHCHECK_URL=http:\/\/127\.0\.0\.1:8787\/health$/m);
|
|
assert.match(compatEnv, /^SOURCE_REPO_DIR=\/opt\/tiku-saas\/source$/m);
|
|
assert.match(compatEnv, /^REPO_DIR=\/opt\/tiku-saas\/repo$/m);
|
|
assert.match(rootEnv, /^NPM_AUDIT_REGISTRY=https:\/\/registry\.npmjs\.org\/$/m);
|
|
assert.match(compatEnv, /^NPM_AUDIT_REGISTRY=https:\/\/registry\.npmjs\.org\/$/m);
|
|
assert.match(rootEnv, /^RUN_TARO_SUPPLY_CHAIN_AUDIT=true$/m);
|
|
assert.match(compatEnv, /^RUN_TARO_SUPPLY_CHAIN_AUDIT=true$/m);
|
|
assert.match(compatEnv, /^SYSTEMD_UNITS="tiku-api\.service tiku-workers\.target"$/m);
|
|
assert.match(workerEnv, /^STORAGE_DEFAULT_BUCKET=/m);
|
|
assert.match(workerEnv, /^WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=/m);
|
|
assert.match(workerEnv, /^WORKER_CRM_POLL_INTERVAL_MS=/m);
|
|
assert.match(apiEnv, /chown root:deploy, and chmod 640/);
|
|
assert.doesNotMatch(workerEnv, /^ALIYUN_OSS_BUCKET=/m);
|
|
assert.doesNotMatch(workerEnv, /^ASSET_SECURITY_SCAN_ENDPOINT=/m);
|
|
assert.doesNotMatch(workerEnv, /^WORKER_POLL_INTERVAL_MS=/m);
|
|
assert.match(runtimeRoleBootstrap, /alter role tiku_api[\s\S]*bypassrls/i);
|
|
assert.match(runtimeRoleBootstrap, /alter role tiku_worker[\s\S]*bypassrls/i);
|
|
assert.doesNotMatch(runtimeRoleBootstrap, /password\s+['"]/i);
|
|
assert.match(deployReadme, /bootstrap:db-runtime-roles/);
|
|
assert.match(deployReadme, /BOOTSTRAP_BACKEND_RUNTIME_ROLES/);
|
|
assert.ok(
|
|
packageJson.includes('npm audit --registry=${NPM_AUDIT_REGISTRY:-https://registry.npmjs.org/}'),
|
|
'npm audit scripts must default to the official audit registry while remaining configurable',
|
|
);
|
|
|
|
console.log('deploy contract: ok');
|