完成资源管理阶段七验收
Project CI / Repository checks (pull_request) Successful in 1m1s
Project CI / Backend tests (pull_request) Successful in 3m43s
Project CI / Native shell tests (pull_request) Successful in 10m47s
Project CI / Frontend tests (pull_request) Failing after 1m41s

补齐视频与资源读取失败空态测试
完成全量前后端和 Rust CI 验收
修复 macOS/BSD 跨平台门禁与测试隔离
更新阶段七验收与共享项目记忆
This commit is contained in:
2026-08-04 10:06:12 +08:00
parent df4e61a208
commit 80bcb4ba0e
26 changed files with 233 additions and 45 deletions
+8
View File
@@ -68,11 +68,19 @@ function validateRuntimePageLifecycle() {
const sourcePageFile = path.join(tempRoot, 'announcement.html');
const onScript = path.join(repoRoot, 'scripts/deploy/maintenance-on.sh');
const offScript = path.join(repoRoot, 'scripts/deploy/maintenance-off.sh');
const onScriptSource = readFileSync(onScript, 'utf8');
const env = {
GENARRATIVE_MAINTENANCE_FILE: markerFile,
GENARRATIVE_MAINTENANCE_PAGE_FILE: runtimePageFile,
};
if (!onScriptSource.includes('replace_file_atomically')) {
fail('maintenance-on 必须通过统一 helper 原子替换公告页和 marker。');
}
if (/\bmv\s+-[^\s]*T\b/u.test(onScriptSource)) {
fail('maintenance-on 不得使用 GNU mv 专属的 -T 参数。');
}
try {
const announcement = '<!doctype html><title>planned maintenance</title>\n';
writeFileSync(sourcePageFile, announcement);
+2 -1
View File
@@ -165,7 +165,8 @@ function parseArchiveObjectMembers(artifact) {
}
memberName = artifact
.subarray(contentStart, contentStart + nameLength)
.toString('utf8');
.toString('utf8')
.replace(/\0+$/u, '');
contentStart += nameLength;
}
+56 -4
View File
@@ -15,6 +15,22 @@ import { tmpdir } from 'node:os';
import path from 'node:path';
const failures = [];
const systemCpPath = ['/usr/bin/cp', '/bin/cp'].find((candidate) =>
existsSync(candidate),
);
const systemMvPath = ['/usr/bin/mv', '/bin/mv'].find((candidate) =>
existsSync(candidate),
);
const systemLnPath = ['/usr/bin/ln', '/bin/ln'].find((candidate) =>
existsSync(candidate),
);
const systemChmodPath = ['/usr/bin/chmod', '/bin/chmod'].find((candidate) =>
existsSync(candidate),
);
const systemStatModeCommand =
process.platform === 'darwin'
? '/usr/bin/stat -f %Lp'
: '/usr/bin/stat -c %a --';
const tmpRoot = mkdtempSync(
path.join(tmpdir(), 'genarrative-production-api-deploy-'),
);
@@ -2421,7 +2437,7 @@ function prepareFixture(name) {
[
'#!/usr/bin/env bash',
'set -euo pipefail',
'/usr/bin/cp "$@"',
`${shellQuote(systemCpPath ?? 'cp')} "$@"`,
'if [[ "${FAKE_CREATE_RELEASE_DURING_COPY:-false}" == "true" ]]; then',
' marker="${FAKE_RELEASE_ROOT}/.${FAKE_RELEASE_VERSION}.race-created"',
' if [[ ! -e "${marker}" ]]; then',
@@ -2434,6 +2450,40 @@ function prepareFixture(name) {
].join('\n'),
'utf8',
);
writeFileSync(
path.join(fakeBin, 'mv'),
[
'#!/usr/bin/env bash',
'set -euo pipefail',
'if [[ "${1:-}" == "-T" ]]; then',
' shift',
' if [[ "$#" -ne 2 || ( -d "$2" && ! -L "$2" ) ]]; then',
' exit 1',
' fi',
` exec ${shellQuote(systemMvPath ?? 'mv')} "$1" "$2"`,
'fi',
`exec ${shellQuote(systemMvPath ?? 'mv')} "$@"`,
'',
].join('\n'),
'utf8',
);
writeFileSync(
path.join(fakeBin, 'ln'),
[
'#!/usr/bin/env bash',
'set -euo pipefail',
'if [[ "${1:-}" == "-sfnT" ]]; then',
' shift',
' if [[ "$#" -ne 2 || ( -d "$2" && ! -L "$2" ) ]]; then',
' exit 1',
' fi',
` exec ${shellQuote(systemLnPath ?? 'ln')} -sfn "$1" "$2"`,
'fi',
`exec ${shellQuote(systemLnPath ?? 'ln')} "$@"`,
'',
].join('\n'),
'utf8',
);
writeFileSync(
path.join(fakeBin, 'install'),
[
@@ -2479,15 +2529,15 @@ function prepareFixture(name) {
' IFS="|" read -r -a env_files <<< "${FAKE_SUDO_ENV_FILES}"',
' modes=()',
' for env_file in "${env_files[@]}"; do',
' modes+=("$(/usr/bin/stat -c %a -- "${env_file}")")',
' /usr/bin/chmod u+rw -- "${env_file}"',
` modes+=("$(${systemStatModeCommand} "\${env_file}")")`,
` ${shellQuote(systemChmodPath ?? 'chmod')} u+rw "\${env_file}"`,
' done',
' set +e',
' "$@"',
' status=$?',
' set -e',
' for index in "${!env_files[@]}"; do',
' /usr/bin/chmod "${modes[$index]}" -- "${env_files[$index]}"',
` ${shellQuote(systemChmodPath ?? 'chmod')} "\${modes[$index]}" "\${env_files[$index]}"`,
' done',
' exit "${status}"',
'fi',
@@ -2501,6 +2551,8 @@ function prepareFixture(name) {
chmodExecutable(path.join(fakeBin, 'sleep'));
chmodExecutable(path.join(fakeBin, 'stat'));
chmodExecutable(path.join(fakeBin, 'cp'));
chmodExecutable(path.join(fakeBin, 'mv'));
chmodExecutable(path.join(fakeBin, 'ln'));
chmodExecutable(path.join(fakeBin, 'install'));
chmodExecutable(path.join(fakeBin, 'sudo'));
+15 -2
View File
@@ -7,6 +7,19 @@ MAINTENANCE_PAGE_FILE="${GENARRATIVE_MAINTENANCE_PAGE_FILE:-/var/lib/genarrative
PAGE_SOURCE=""
REASON_PARTS=()
replace_file_atomically() {
local source_file="$1"
local target_file="$2"
if [[ -d "${target_file}" && ! -L "${target_file}" ]]; then
echo "[maintenance] 原子替换目标不能是目录: ${target_file}" >&2
exit 1
fi
# 源文件与目标文件位于同一目录,POSIX rename 语义即可保证原子替换。
# 不使用 GNU mv 专属的 -T,确保 macOS/BSD 本地门禁也能执行。
mv -f "${source_file}" "${target_file}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--page-file)
@@ -41,7 +54,7 @@ if [[ -n "${PAGE_SOURCE}" ]]; then
page_temp="$(mktemp "${MAINTENANCE_PAGE_FILE}.tmp.XXXXXX")"
trap 'rm -f "${page_temp:-}" "${marker_temp:-}"' EXIT
install -m 0644 -- "${PAGE_SOURCE}" "${page_temp}"
mv -fT -- "${page_temp}" "${MAINTENANCE_PAGE_FILE}"
replace_file_atomically "${page_temp}" "${MAINTENANCE_PAGE_FILE}"
page_temp=""
echo "[maintenance] 已安装本次运行态公告页: ${MAINTENANCE_PAGE_FILE}"
elif [[ ! -f "${MAINTENANCE_FILE}" && ( -e "${MAINTENANCE_PAGE_FILE}" || -L "${MAINTENANCE_PAGE_FILE}" ) ]]; then
@@ -55,7 +68,7 @@ marker_temp="$(mktemp "${MAINTENANCE_FILE}.tmp.XXXXXX")"
} >"${marker_temp}"
chmod 0644 "${marker_temp}"
mv -fT -- "${marker_temp}" "${MAINTENANCE_FILE}"
replace_file_atomically "${marker_temp}" "${MAINTENANCE_FILE}"
marker_temp=""
trap - EXIT
echo "[maintenance] 已进入维护模式: ${MAINTENANCE_FILE}"
+9 -3
View File
@@ -892,7 +892,9 @@ ensure_default_worker_service() {
return 1
fi
mapfile -t services < <(list_worker_services "${pattern}")
while IFS= read -r service; do
services+=("${service}")
done < <(list_worker_services "${pattern}")
if [[ "${#services[@]}" -gt 0 ]]; then
return 0
fi
@@ -1033,7 +1035,9 @@ restart_worker_services() {
fi
ensure_default_worker_service "${pattern}"
mapfile -t services < <(list_worker_services "${pattern}")
while IFS= read -r service; do
services+=("${service}")
done < <(list_worker_services "${pattern}")
if [[ "${#services[@]}" -eq 0 ]]; then
echo "[production-api-deploy] 未发现已加载的外部生成 worker 单元: ${pattern}" >&2
return 1
@@ -1052,7 +1056,9 @@ wait_for_worker_services() {
return 0
fi
mapfile -t services < <(list_worker_services "${pattern}")
while IFS= read -r service; do
services+=("${service}")
done < <(list_worker_services "${pattern}")
if [[ "${#services[@]}" -eq 0 ]]; then
echo "[production-api-deploy] 外部生成 worker 单元不存在,发布失败: ${pattern}" >&2
return 1
@@ -2,6 +2,7 @@
import { createHash } from 'node:crypto';
import { lstat, readFile, realpath } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -85,15 +86,20 @@ export async function readRepairPlan(planFile, { repoRoot = REPO_ROOT } = {}) {
const resolvedPath = path.resolve(planFile);
const resolvedRepoRoot = path.resolve(repoRoot);
let canonicalPath;
let canonicalRepoRoot;
try {
canonicalPath = await realpath(resolvedPath);
canonicalRepoRoot = await realpath(resolvedRepoRoot);
} catch {
throw new Error('--plan-file 无法解析或不存在。');
}
if (canonicalPath !== resolvedPath) {
const pathWithoutSystemTempAlias = await normalizeSystemTempAlias(
resolvedPath,
);
if (canonicalPath !== pathWithoutSystemTempAlias) {
throw new Error('--plan-file 路径链不能包含符号链接。');
}
if (isPathInside(canonicalPath, resolvedRepoRoot)) {
if (isPathInside(canonicalPath, canonicalRepoRoot)) {
throw new Error('--plan-file 必须位于仓库外,避免真实 ID 进入工作区。');
}
@@ -539,6 +545,16 @@ function isPathInside(candidate, root) {
);
}
async function normalizeSystemTempAlias(candidate) {
const resolvedTempRoot = path.resolve(tmpdir());
if (!isPathInside(candidate, resolvedTempRoot)) {
return candidate;
}
const canonicalTempRoot = await realpath(resolvedTempRoot);
return path.join(canonicalTempRoot, path.relative(resolvedTempRoot, candidate));
}
function assertPlainObject(value, label) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${label} 必须是对象。`);