release: 发布墨呈 v0.6.0
新增能力:实现可编辑 DOCX 导出、通用主题样式翻译、Markdown 基础工具栏、统一导出菜单与导出进度提示;14 套主题支持纸张、页边距、字体、段落、代码、图片、图表、封面、页眉页脚和页码映射。 问题修复:修复精确预览放大后的右侧留白、保存与导出默认路径、Desktop DOCX 媒体捕获超时、品牌改名后的安装目录迁移,并统一主题展示名称。 兼容与部署:产品更名为墨呈/MorphDoc,保留 NSIS 安装身份;Desktop 内置 Pandoc 3.9.0.2,字体包支持安装向导可选与独立分发;Docker 内置 Chromium、Pandoc 和正式字体,支持离线部署。 发布规则:新增 release/v<version> 唯一产物目录、Docker 镜像 tar.gz、Web 部署 ZIP、manifest.json 和 SHA256SUMS.txt;正式字体迁入版本化仓库目录并由 Git LFS 管理。 验证结果:全项目测试、类型检查、生产构建、git diff --check、14 主题 DOCX 与视觉矩阵、Web API、Desktop 安装版、Word/WPS 可编辑性和字体门禁全部通过;离线镜像经 docker load 复验且容器保持 healthy。 发布产物:Desktop 安装器与 ZIP、字体安装器与 ZIP、Web 镜像压缩包、离线部署包、发行说明和校验清单已整理至本地 release/v0.6.0。Windows EXE 当前未进行 Authenticode 签名,仅按公司内部发布风险规则分发。
This commit is contained in:
@@ -1 +1,7 @@
|
||||
deploy/entrypoint.sh text eol=lf
|
||||
|
||||
# 正式字体资产保存在仓库固定目录,但二进制内容交由 Git LFS 管理。
|
||||
font-packs/assets/**/*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
font-packs/assets/**/*.otf filter=lfs diff=lfs merge=lfs -text
|
||||
font-packs/assets/**/*.woff filter=lfs diff=lfs merge=lfs -text
|
||||
font-packs/assets/**/*.woff2 filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -14,7 +14,10 @@ Thumbs.db
|
||||
tmp/
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
# 构建、门禁与正式发版产物只保留在本机,不进入 Git。
|
||||
output/
|
||||
/apps/desktop/out/
|
||||
/apps/desktop/.runtime/
|
||||
/release/
|
||||
*.tsbuildinfo
|
||||
|
||||
+17
-2
@@ -115,6 +115,18 @@ GitHub、Clean、Pixyll 和 Whitey,默认主题仍为 GitHub。为确保覆盖
|
||||
`typora-whitey` 等内部主题 ID、目录名和配置键保持不变;来源说明、作者
|
||||
字段及许可证审计信息也继续保留。
|
||||
|
||||
`v0.6.0` 发布收口阶段 D 已建立统一发版规则。正式分发文件只允许归集到
|
||||
被 Git 忽略的 `release/v<version>/`;统一脚本从 Desktop、字体包和
|
||||
版本化 Docker 镜像生成安装器、便携 ZIP、字体附件、离线镜像 tar.gz、
|
||||
Web 部署 ZIP、发行说明、机器可读清单及 SHA-256 校验文件,先写入临时
|
||||
目录并在全部成功后原子落盘,既有版本目录禁止覆盖。正式字体资产迁入
|
||||
`font-packs/assets/<id>/<semver>/` 并由 Git LFS 管理,构建器继续严格校验
|
||||
冻结字节数、哈希、内部字族名和嵌入权限。本次 `release/v0.6.0` 的 8 项
|
||||
校验和全部通过,707,722,584 字节的离线镜像归档已由 `docker load` 重新
|
||||
导入,镜像 ID 保持
|
||||
`sha256:53bfbc9592542d4ffe9ec3017c401c6d49a28c4ee993c6c5fc00536b1d01978a`,
|
||||
运行容器健康状态为 `healthy`。
|
||||
|
||||
`v0.6.0` 阶段 3 已完成 DOCX 共享协议:`packages/core` 统一定义请求
|
||||
Schema、资源、Pandoc capability、错误码、结果、诊断、耗时、MIME 和
|
||||
安全文件名;`packages/application` 新增 `prepareDocxExport()`,复用
|
||||
@@ -1370,9 +1382,10 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
资源哈希与容量门禁、确定性版本选择和逻辑字体面匹配;应用层已将同一
|
||||
解析结果用于主题 CSS 的 WOFF2 和 DOCX 的静态 TrueType,发行资产与
|
||||
安装逻辑留在 FP3。
|
||||
- `packages/font-pack-builder` 负责从仓库外冻结资源构建版本化字体包,
|
||||
- `packages/font-pack-builder` 负责从仓库内 Git LFS 冻结资源构建版本化字体包,
|
||||
校验输入哈希、字体格式、内部元数据、嵌入权限、同版本不可变性并生成
|
||||
可供注册器再次验证的构建报告;字体二进制和产物目录均不进入 Git。
|
||||
可供注册器再次验证的构建报告;正式字体二进制进入 LFS,构建产物、门禁
|
||||
报告和 `release/` 均不进入 Git。
|
||||
- `packages/application/src/docx-export-service.ts` 负责 DOCX capability、
|
||||
并发排队、总超时、取消、跨端转换编排、错误码和耗时诊断。
|
||||
- `apps/desktop/src/desktop-application-controller.ts` 负责 Electron
|
||||
@@ -1456,6 +1469,8 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
哈希门禁、许可证保留、打包接入、离线复验和真实 DOCX 冒烟;
|
||||
- 发布收口阶段 C:已完成确定性字体 ZIP、MorphDoc 字体安装器命名、
|
||||
Desktop 同目录可选附件、Docker 镜像内置字体和真实 DOCX 发行门禁;
|
||||
- 发布收口阶段 D:已完成统一 `release/v<version>` 产物目录、Docker 离线
|
||||
镜像封包、部署 ZIP、清单与校验和规则,并将正式字体纳入 Git LFS;
|
||||
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
||||
|
||||
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# 发版规则
|
||||
|
||||
## 唯一产物目录
|
||||
|
||||
所有正式分发文件必须统一归集到仓库根目录下的:
|
||||
|
||||
```text
|
||||
release/v<semver>/
|
||||
```
|
||||
|
||||
`release/`、`output/`、`apps/desktop/out/`、`apps/desktop/.runtime/`、
|
||||
`tmp/` 以及门禁报告均是本地中间产物或发布产物,必须保持 Git 忽略。
|
||||
大型二进制不得直接提交到普通 Git 历史。
|
||||
|
||||
正式目录只能由 `npm run release:stage` 生成。脚本先写入同一文件系统中的
|
||||
`.stage-*` 临时目录,完成镜像封包、清单和校验和后再原子改名;目标版本
|
||||
目录已经存在时必须拒绝覆盖。
|
||||
|
||||
## 标准目录内容
|
||||
|
||||
每个版本至少包含:
|
||||
|
||||
```text
|
||||
release/v<version>/
|
||||
├── MorphDoc-<version>-x86_64-Setup.exe
|
||||
├── MorphDoc-<version>-x86_64.zip
|
||||
├── MorphDoc-FontPack-<id>-<font-version>-x86_64-Setup.exe
|
||||
├── MorphDoc-FontPack-<id>-<font-version>.zip
|
||||
├── MorphDoc-Web-<version>-image.tar.gz
|
||||
├── MorphDoc-Web-<version>-deploy.zip
|
||||
├── manifest.json
|
||||
├── SHA256SUMS.txt
|
||||
└── RELEASE-NOTES.md
|
||||
```
|
||||
|
||||
Docker 镜像必须通过 `docker save` 导出并使用 gzip 压缩。Web 部署 ZIP 必须
|
||||
包含 Compose、Nginx、环境变量示例和离线加载说明。`manifest.json` 记录
|
||||
版本、Git 提交、镜像 ID、文件大小和 SHA-256;`SHA256SUMS.txt` 用于分发后
|
||||
独立校验。
|
||||
|
||||
## 正式字体资产
|
||||
|
||||
正式字体固定存放于:
|
||||
|
||||
```text
|
||||
font-packs/assets/<font-pack-id>/<semver>/
|
||||
```
|
||||
|
||||
TTF、OTF、WOFF 和 WOFF2 必须由 Git LFS 跟踪。新增或升级字体前必须完成
|
||||
许可证、来源、内部字族名、文档嵌入权限、字节数和 SHA-256 审查;任何内容
|
||||
变化都必须提升字体包 SemVer。克隆后需要先执行 `git lfs pull`,构建器不会
|
||||
下载或使用系统字体代替缺失资产。
|
||||
|
||||
## 发版步骤
|
||||
|
||||
1. 完成全量测试、类型检查、生产构建和视觉门禁。
|
||||
2. 构建 Desktop 安装包、便携 ZIP 和字体附件。
|
||||
3. 构建并验证版本化 Docker 镜像。
|
||||
4. 准备 `docs/releases/v<version>.md`。
|
||||
5. 执行:
|
||||
|
||||
```powershell
|
||||
npm run release:stage -- --version <version> --image md-to-pdf:v<version>
|
||||
```
|
||||
|
||||
6. 逐项核验 `SHA256SUMS.txt`,并从离线归档重新加载 Docker 镜像。
|
||||
7. 创建内容完整的 `release:` 提交。
|
||||
8. 创建 annotated `v<version>` 标签,标签说明与发布提交正文一致。
|
||||
|
||||
安装器公开分发前应完成 Authenticode 签名;未签名产物只能按内部发布风险
|
||||
规则分发。
|
||||
@@ -0,0 +1,38 @@
|
||||
# MorphDoc · 墨呈 v0.6.0
|
||||
|
||||
## 核心更新
|
||||
|
||||
- 新增可编辑 DOCX 导出,保留纸张、页边距、字体、段落、代码块、图片、
|
||||
Mermaid、ECharts、封面、页眉页脚和页码配置。
|
||||
- 建立通用主题到 DOCX 样式翻译引擎,14 套内置主题通过真实 Word/WPS
|
||||
无修复打开、编辑和逐页视觉门禁。
|
||||
- 新增可扩展 Markdown 基础工具栏,支持标题、强调、代码、引用、多行列表、
|
||||
任务列表、分隔线及 M×N 表格。
|
||||
- PDF 与 DOCX 统一收口到“导出”子菜单,并提供独立顶部进度提示。
|
||||
|
||||
## 发行与兼容性
|
||||
|
||||
- 产品更名为“墨呈”,英文名 MorphDoc;保留原 NSIS 安装身份并迁移到新的
|
||||
默认安装目录、快捷方式和开始菜单目录。
|
||||
- Desktop 内置固定 Pandoc 3.9.0.2 最小运行时;字体包可在安装向导中选择,
|
||||
也提供独立安装器与 ZIP。
|
||||
- Docker 镜像内置固定 Chromium、Pandoc 与正式字体包,支持完全离线部署。
|
||||
- 正式字体资产转由仓库固定目录和 Git LFS 管理。
|
||||
|
||||
## 问题修复
|
||||
|
||||
- 修复精确预览放大后右侧缺少固定留白的问题。
|
||||
- 修复保存、另存为和导出的默认路径优先级。
|
||||
- 修复 Desktop DOCX 图片捕获可能超时的问题。
|
||||
- 修复品牌改名后安装器仍继承旧安装目录的问题。
|
||||
- 统一主题展示名称并移除 Typora 字样,默认主题保持 GitHub。
|
||||
|
||||
## 验证结论
|
||||
|
||||
- 全项目测试、类型检查、生产构建和空白检查通过。
|
||||
- 14 套主题生产 PDF/DOCX 与六组纸张、方向、页边距视觉矩阵通过。
|
||||
- Web API、Desktop ZIP、覆盖安装后的真实安装版均完成 PDF/DOCX 导出。
|
||||
- Word/WPS 可编辑性、字体嵌入、封面独立分页和页眉页脚位置门禁通过。
|
||||
|
||||
当前 Windows 安装器未进行 Authenticode 签名,适用于公司内部发布;公开
|
||||
网络分发前建议增加代码签名。
|
||||
+11
-5
@@ -1,21 +1,24 @@
|
||||
# 可选字体包构建
|
||||
|
||||
本目录只保存字体包配方、许可证和来源说明,不保存字体二进制。普通
|
||||
Desktop 主安装包不包含字体;组合 Desktop 发行把字体作为同目录可选附件,
|
||||
Docker 发行构建则把本次受校验的字体包写入镜像层。
|
||||
本目录保存字体包配方、许可证、来源说明和正式冻结字体资产。字体二进制
|
||||
位于固定的 `font-packs/assets/`,统一由 Git LFS 管理,普通 Git 对象只保存
|
||||
LFS 指针。普通 Desktop 主安装包不包含字体;组合 Desktop 发行把字体作为
|
||||
同目录可选附件,Docker 发行构建则把本次受校验的字体包写入镜像层。
|
||||
|
||||
## 源目录
|
||||
|
||||
发布人员需要在 Git 忽略的本地目录中准备配方声明的冻结资源。默认目录为:
|
||||
正式发布字体必须先完成许可证、来源、内部字族名、嵌入权限和哈希审查,
|
||||
随后固定存放在以下版本化仓库目录:
|
||||
|
||||
```text
|
||||
.local/font-pack-sources/mdtp-serif-sc/1.0.0/
|
||||
font-packs/assets/mdtp-serif-sc/1.0.0/
|
||||
├── MdTPSerifSC-Regular.ttf
|
||||
├── MdTPSerifSC-Regular.woff2
|
||||
├── MdTPSerifSC-Bold.ttf
|
||||
└── MdTPSerifSC-Bold.woff2
|
||||
```
|
||||
|
||||
该目录中的 `ttf/otf/woff/woff2` 由 `.gitattributes` 强制使用 Git LFS。
|
||||
构建器不会联网下载、转换或自动替换字体。每个输入文件都必须与配方声明的
|
||||
字节数和 SHA-256 完全一致;TTF 还必须是无需运行时规范化的原生静态
|
||||
TrueType,并具有可安装或可编辑的文档嵌入权限。
|
||||
@@ -33,6 +36,9 @@ npm run verify:font-pack
|
||||
npm run build:font-pack -- --source-root D:/font-pack-sources/mdtp-serif-sc/1.0.0
|
||||
```
|
||||
|
||||
克隆仓库后应先执行 `git lfs pull`。缺少 LFS 内容时,字体构建会因文件大小
|
||||
或 SHA-256 不匹配而拒绝继续,不允许使用系统字体或相近字体静默替代。
|
||||
|
||||
默认运行时字体包根目录为 `output/font-packs/root`,构建报告写入
|
||||
`output/font-packs/build-report.json`,两者均被 Git 忽略。输出布局为:
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-1
@@ -32,6 +32,8 @@
|
||||
"build:font-pack:windows": "npm run verify:font-pack && node packages/font-pack-builder/dist/cli.js windows-installer",
|
||||
"build:font-pack:archive": "npm run verify:font-pack && node packages/font-pack-builder/dist/cli.js archive",
|
||||
"desktop:make:with-font-pack": "npm run build:font-pack:windows && npm run build:font-pack:archive && node scripts/build-desktop-with-font-pack.mjs",
|
||||
"release:stage": "node scripts/stage-release.mjs",
|
||||
"test:release-stage": "node --test scripts/stage-release.test.mjs",
|
||||
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
||||
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
||||
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
||||
@@ -44,7 +46,7 @@
|
||||
"verify:docx-page-decorations": "npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-page-decoration-visuals.mjs",
|
||||
"verify:docx-media-visuals": "npm run build:web-runtime && npm run build -w @md-to-pdf/document-visual-diff && npm run build -w @md-to-pdf/server && node scripts/verify-docx-media-visuals.mjs",
|
||||
"verify:docx-layout-visual-matrix": "npm run verify:docx-r4-matrix && npm run build -w @md-to-pdf/document-visual-diff && node scripts/verify-docx-layout-visual-matrix.mjs",
|
||||
"test": "npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/font-pack-builder && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
||||
"test": "npm run test:release-stage && npm run test -w @md-to-pdf/markdown-echarts && npm run test -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run test -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run test -w @md-to-pdf/docx-theme-engine && npm run test -w @md-to-pdf/document-visual-diff && npm run test -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/font-pack-registry && npm run test -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run test -w @md-to-pdf/font-pack-builder && npm run test -w @md-to-pdf/renderer && npm run test -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run test -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run test -w @md-to-pdf/web && npm run test -w @md-to-pdf/server && npm run test -w @md-to-pdf/desktop",
|
||||
"typecheck": "npm run typecheck -w @md-to-pdf/markdown-echarts && npm run build -w @md-to-pdf/markdown-echarts && npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/semantic-document && npm run build -w @md-to-pdf/semantic-document && npm run typecheck -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/docx-theme-engine && npm run typecheck -w @md-to-pdf/document-visual-diff && npm run typecheck -w @md-to-pdf/font-pack-registry && npm run typecheck -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/docx-engine && npm run typecheck -w @md-to-pdf/font-pack-builder && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/application && npm run build -w @md-to-pdf/application && npm run typecheck -w @md-to-pdf/preview-engine && npm run build -w @md-to-pdf/preview-engine && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server && npm run typecheck -w @md-to-pdf/desktop"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -44,7 +44,7 @@ async function main() {
|
||||
option(
|
||||
values,
|
||||
"source-root",
|
||||
".local/font-pack-sources/mdtp-serif-sc/1.0.0"
|
||||
"font-packs/assets/mdtp-serif-sc/1.0.0"
|
||||
)
|
||||
),
|
||||
outputRoot: resolve(
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import {
|
||||
copyFile,
|
||||
mkdir,
|
||||
open,
|
||||
readFile,
|
||||
rename,
|
||||
rm,
|
||||
stat,
|
||||
writeFile
|
||||
} from "node:fs/promises";
|
||||
import { createReadStream, createWriteStream } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createGzip } from "node:zlib";
|
||||
import { zipSync } from "fflate";
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
export function normalizeVersion(value) {
|
||||
const normalized = String(value ?? "").trim().replace(/^v/u, "");
|
||||
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(normalized)) {
|
||||
throw new Error(`无效发布版本:${value ?? ""}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function parseArguments(values) {
|
||||
const result = new Map();
|
||||
for (let index = 0; index < values.length; index += 2) {
|
||||
const key = values[index];
|
||||
const value = values[index + 1];
|
||||
if (!key?.startsWith("--") || !value || value.startsWith("--")) {
|
||||
throw new Error(`无效命令参数:${key ?? ""}`);
|
||||
}
|
||||
result.set(key.slice(2), value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function sha256(path) {
|
||||
const hash = createHash("sha256");
|
||||
for await (const chunk of createReadStream(path)) {
|
||||
hash.update(chunk);
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
async function run(command, args, options = {}) {
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: repositoryRoot,
|
||||
env: process.env,
|
||||
stdio: options.capture ? ["ignore", "pipe", "inherit"] : "inherit",
|
||||
windowsHide: true
|
||||
});
|
||||
let stdout = "";
|
||||
if (options.capture) {
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
}
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolvePromise(stdout.trim());
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`${command} 执行失败:${signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deploymentFiles(version) {
|
||||
return {
|
||||
"compose.yaml": readFile(join(repositoryRoot, "deploy", "compose.yaml")),
|
||||
"nginx.conf": readFile(join(repositoryRoot, "deploy", "nginx.conf")),
|
||||
"README.md": readFile(join(repositoryRoot, "deploy", "README.md")),
|
||||
".env.example": Promise.resolve(
|
||||
Buffer.from(
|
||||
[
|
||||
`MD_TO_PDF_IMAGE=md-to-pdf:v${version}`,
|
||||
"MD_TO_PDF_APP_VERSION=" + version,
|
||||
"MD_TO_PDF_PORT=8080",
|
||||
"PDF_CONCURRENCY=1",
|
||||
"PDF_MAX_QUEUE=4",
|
||||
"PDF_TIMEOUT_MS=120000",
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
),
|
||||
"OFFLINE-README.md": Promise.resolve(
|
||||
Buffer.from(
|
||||
[
|
||||
`# MorphDoc Web v${version} 离线部署`,
|
||||
"",
|
||||
`1. 将 \`MorphDoc-Web-${version}-image.tar.gz\` 放在本目录上一级。`,
|
||||
`2. 执行 \`docker load --input MorphDoc-Web-${version}-image.tar.gz\`。`,
|
||||
"3. 复制 `.env.example` 为 `.env` 并按需调整端口与并发。",
|
||||
"4. 执行 `docker compose --env-file .env up -d --no-build`。",
|
||||
"5. 访问 `/api/health`,确认状态为 `ok` 后再开放服务。",
|
||||
"",
|
||||
"镜像已经内置固定 Pandoc、Chromium 与正式字体包,不需要额外下载。",
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
export async function createDeploymentArchive(version, outputPath) {
|
||||
const entries = {};
|
||||
for (const [name, content] of Object.entries(deploymentFiles(version))) {
|
||||
entries[`MorphDoc-Web-${version}-deploy/${name}`] = new Uint8Array(
|
||||
await content
|
||||
);
|
||||
}
|
||||
await writeFile(outputPath, zipSync(entries, { level: 9 }));
|
||||
}
|
||||
|
||||
export async function describeArtifacts(directory, names) {
|
||||
const entries = [];
|
||||
for (const name of names) {
|
||||
const path = join(directory, name);
|
||||
const info = await stat(path);
|
||||
entries.push({
|
||||
name,
|
||||
bytes: info.size,
|
||||
sha256: await sha256(path)
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function checksumText(entries) {
|
||||
return `${entries.map((entry) => `${entry.sha256} ${entry.name}`).join("\n")}\n`;
|
||||
}
|
||||
|
||||
async function assertReadableFile(path) {
|
||||
const handle = await open(path, "r");
|
||||
await handle.close();
|
||||
const info = await stat(path);
|
||||
if (!info.isFile() || info.size === 0) {
|
||||
throw new Error(`发布源文件无效:${relative(repositoryRoot, path)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function stageRelease(options = {}) {
|
||||
const rootManifest = JSON.parse(
|
||||
await readFile(join(repositoryRoot, "package.json"), "utf8")
|
||||
);
|
||||
const version = normalizeVersion(options.version ?? rootManifest.version);
|
||||
const image = options.image ?? `md-to-pdf:v${version}`;
|
||||
const releaseRoot = resolve(options.releaseRoot ?? join(repositoryRoot, "release"));
|
||||
const finalDirectory = join(releaseRoot, `v${version}`);
|
||||
const stageDirectory = join(
|
||||
releaseRoot,
|
||||
`.stage-v${version}-${randomUUID()}`
|
||||
);
|
||||
try {
|
||||
await stat(finalDirectory);
|
||||
throw new Error(`发布目录已存在,拒绝覆盖:${finalDirectory}`);
|
||||
} catch (error) {
|
||||
if (error?.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const desktopDirectory = join(
|
||||
repositoryRoot,
|
||||
"apps",
|
||||
"desktop",
|
||||
"out",
|
||||
`v${version}`
|
||||
);
|
||||
const desktopNames = [
|
||||
`MorphDoc-${version}-x86_64-Setup.exe`,
|
||||
`MorphDoc-${version}-x86_64.zip`,
|
||||
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe",
|
||||
"MorphDoc-FontPack-mdtp-serif-sc-1.0.0.zip"
|
||||
];
|
||||
const releaseNotesSource = join(
|
||||
repositoryRoot,
|
||||
"docs",
|
||||
"releases",
|
||||
`v${version}.md`
|
||||
);
|
||||
for (const name of desktopNames) {
|
||||
await assertReadableFile(join(desktopDirectory, name));
|
||||
}
|
||||
await assertReadableFile(releaseNotesSource);
|
||||
|
||||
await mkdir(stageDirectory, { recursive: true });
|
||||
try {
|
||||
for (const name of desktopNames) {
|
||||
await copyFile(join(desktopDirectory, name), join(stageDirectory, name));
|
||||
}
|
||||
await copyFile(releaseNotesSource, join(stageDirectory, "RELEASE-NOTES.md"));
|
||||
|
||||
const deployName = `MorphDoc-Web-${version}-deploy.zip`;
|
||||
await createDeploymentArchive(version, join(stageDirectory, deployName));
|
||||
|
||||
const imageName = `MorphDoc-Web-${version}-image.tar.gz`;
|
||||
const imageOutput = join(stageDirectory, imageName);
|
||||
const imageTar = join(stageDirectory, `.image-${randomUUID()}.tar`);
|
||||
await run("docker", ["image", "inspect", image], { capture: true });
|
||||
await run("docker", ["save", "--output", imageTar, image]);
|
||||
await pipeline(
|
||||
createReadStream(imageTar),
|
||||
createGzip({ level: 9 }),
|
||||
createWriteStream(imageOutput)
|
||||
);
|
||||
await rm(imageTar, { force: true });
|
||||
|
||||
const primaryNames = [
|
||||
...desktopNames,
|
||||
imageName,
|
||||
deployName,
|
||||
"RELEASE-NOTES.md"
|
||||
];
|
||||
const artifacts = await describeArtifacts(stageDirectory, primaryNames);
|
||||
const gitCommit = await run("git", ["rev-parse", "HEAD"], { capture: true });
|
||||
const imageId = JSON.parse(
|
||||
await run("docker", ["image", "inspect", image, "--format", "{{json .Id}}"], {
|
||||
capture: true
|
||||
})
|
||||
);
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
product: "MorphDoc",
|
||||
displayName: "墨呈",
|
||||
version,
|
||||
gitCommit,
|
||||
docker: { image, imageId },
|
||||
artifacts
|
||||
};
|
||||
await writeFile(
|
||||
join(stageDirectory, "manifest.json"),
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
const checksumEntries = await describeArtifacts(stageDirectory, [
|
||||
...primaryNames,
|
||||
"manifest.json"
|
||||
]);
|
||||
await writeFile(
|
||||
join(stageDirectory, "SHA256SUMS.txt"),
|
||||
checksumText(checksumEntries),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
await mkdir(releaseRoot, { recursive: true });
|
||||
await rename(stageDirectory, finalDirectory);
|
||||
return { version, image, directory: finalDirectory, artifacts };
|
||||
} catch (error) {
|
||||
await rm(stageDirectory, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const values = parseArguments(process.argv.slice(2));
|
||||
const result = await stageRelease({
|
||||
version: values.get("version"),
|
||||
image: values.get("image")
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { unzipSync } from "fflate";
|
||||
import {
|
||||
checksumText,
|
||||
createDeploymentArchive,
|
||||
normalizeVersion,
|
||||
parseArguments
|
||||
} from "./stage-release.mjs";
|
||||
|
||||
test("normalizeVersion 接受语义版本并移除 v 前缀", () => {
|
||||
assert.equal(normalizeVersion("v0.6.0"), "0.6.0");
|
||||
assert.equal(normalizeVersion("1.2.3-beta.1"), "1.2.3-beta.1");
|
||||
assert.throws(() => normalizeVersion("latest"), /无效发布版本/u);
|
||||
});
|
||||
|
||||
test("parseArguments 拒绝不完整参数", () => {
|
||||
assert.deepEqual(
|
||||
[...parseArguments(["--version", "0.6.0", "--image", "test:1"])],
|
||||
[
|
||||
["version", "0.6.0"],
|
||||
["image", "test:1"]
|
||||
]
|
||||
);
|
||||
assert.throws(() => parseArguments(["--version"]), /无效命令参数/u);
|
||||
});
|
||||
|
||||
test("checksumText 生成标准 SHA256SUMS 格式", () => {
|
||||
assert.equal(
|
||||
checksumText([{ name: "artifact.zip", sha256: "abc" }]),
|
||||
"abc artifact.zip\n"
|
||||
);
|
||||
});
|
||||
|
||||
test("Web 离线部署 ZIP 包含 Compose 和导入说明", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "morphdoc-release-test-"));
|
||||
const output = join(directory, "deploy.zip");
|
||||
try {
|
||||
await createDeploymentArchive("0.6.0", output);
|
||||
const entries = unzipSync(new Uint8Array(await readFile(output)));
|
||||
const names = Object.keys(entries).sort();
|
||||
assert.ok(names.includes("MorphDoc-Web-0.6.0-deploy/compose.yaml"));
|
||||
assert.ok(names.includes("MorphDoc-Web-0.6.0-deploy/.env.example"));
|
||||
assert.ok(names.includes("MorphDoc-Web-0.6.0-deploy/OFFLINE-README.md"));
|
||||
assert.match(
|
||||
Buffer.from(
|
||||
entries["MorphDoc-Web-0.6.0-deploy/OFFLINE-README.md"]
|
||||
).toString("utf8"),
|
||||
/docker load/u
|
||||
);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user