feat(build): astro 构建管线与媒体落盘

Plan 4 Task 5:build.service snapshotting 后接真实构建——building 态进
pipeline.ts(spawn `pnpm --filter @sf/theme-pilot-minimal exec astro build`
(SF_BUILD_INPUT env;非零退出+stderr 标记判据)→ dist 定位与 target_file
完整性 → 媒体 copyPath→dist/public_path(sha256 全量校验)→ sitemap.xml/
robots.txt(编码 URL 原样)→ route-manifest.json(§12.1 字段面、哈希链复用)
→ build_artifacts dist/manifest 登记)→ validating(空,Task 6) → 报告+report
行 → succeeded;失败任何一步 failed+error_code/error_summary(stderr 摘要)
+input/dist 清残。

dist 双位置纪律:主题包 dist 是暂存,产物汇齐后拷出 <artifactsRoot>/jobs/
<jobId>/dist(job 级权威),暂存随即清空。无网证明:ASTRO_TELEMETRY_DISABLED=1
(实测 @astrojs/telemetry 构建期真调 fetch)+ NODE_OPTIONS 透传(测试注入
fetch/net stub 构建仍成功)。主题集成补面:ProductList 模板 + /products/
主列表分派特判(DB 物化形状 targetContentId=null,Task 4 夹具曾被占位绕过)。

测试:tests/integration/build-pipeline.test.ts 五用例(全链/夹具直跑/五禁
断言/失败路径);build-snapshot 状态机补 building + 清场补 build_artifacts。
root 359 + theme 26 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 10:52:13 +08:00
parent 107642c1b3
commit 1ed3ac062a
8 changed files with 1200 additions and 30 deletions
+12 -2
View File
@@ -16,6 +16,7 @@ import { fileURLToPath } from 'node:url'
import { Command } from 'commander'
import { renderReportHtml, runWxrAdapterReport, type WxrAdapterReport } from '@sf/adapter-wp'
import { getDb } from '../apps/api/src/db/client.js'
import { PipelineError } from '../apps/api/src/modules/build/pipeline.js'
import { BuildError, createBuildJob } from '../apps/api/src/modules/build/build.service.js'
import {
ImportError,
@@ -301,7 +302,7 @@ program
program
.command('build:create')
.description('冻结 Render Snapshot:物化六文件(site/content/taxonomies/routes/assets/render-manifest)+ build_input_snapshots 行;dist 产物是 Task 5')
.description('冻结 Render Snapshot 并构建:物化六文件 + build_input_snapshots 行 → astro 构建(媒体落盘/sitemap/robots/route-manifest)→ dist 产物 + build_artifacts 登记')
.requiredOption('--site <key>', '站点 key')
.option('--key <key>', '幂等键(缺省 = site+published 集合哈希+主题版本 自动派生;重复提交同 key 复用既有 job 不重跑)')
.option('--artifacts <dir>', '产物根目录(缺省 repoRoot/artifacts)')
@@ -335,6 +336,12 @@ program
` hashes: content=${result.sourceHashes.content.slice(0, 12)}… route=${result.sourceHashes.route.slice(0, 12)}… asset=${result.sourceHashes.asset.slice(0, 12)}…`,
)
console.log(` report: ${result.reportPath}`)
if (result.artifacts !== undefined) {
console.log(
` dist: ${result.artifacts.distPath}(${result.artifacts.distFileCount} 文件 sha256=${result.artifacts.distSha256.slice(0, 12)}…)`,
)
console.log(` route-manifest: ${result.artifacts.routeManifestPath}`)
}
console.log(` total ${result.durationMs.toFixed(1)}ms`)
if (result.status === 'failed') process.exitCode = 1
} finally {
@@ -343,7 +350,7 @@ program
})
// parseAsync:action 内抛错走此 catch(commander 默认会 process.exit(1) 并打印堆栈,
// 这里收敛为单行错误信息 + exitCode——SourceRegisterError/ImportError/PublishError/BuildError 打 code,其余原样呈现)
// 这里收敛为单行错误信息 + exitCode——SourceRegisterError/ImportError/PublishError/BuildError/PipelineError 打 code,其余原样呈现)
program.parseAsync(process.argv).catch((err: unknown) => {
if (err instanceof SourceRegisterError) {
console.error(`[sf] ${err.code}: ${err.message}`)
@@ -356,6 +363,9 @@ program.parseAsync(process.argv).catch((err: unknown) => {
console.error(`[sf] ${err.code}: ${err.message}`)
} else if (err instanceof BuildError) {
console.error(`[sf] ${err.code}: ${err.message}`)
} else if (err instanceof PipelineError) {
// 构建管线阶段失败(spawn/媒体/manifest)——code + stderr 摘要(job 行已有 error 面)
console.error(`[sf] ${err.code}: ${err.message}`)
} else {
console.error(`[sf] ${err instanceof Error ? err.message : String(err)}`)
}