IPTVnator Release Cut 发布流程实战:从变更笔记到草稿 Release 的完整契约
【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator
导读
本文基于 IPTVnator 仓库中的release-cut技能文档(.codex/skills/release-cut/SKILL.md)与其背后的完整发布管线契约(docs/architecture/release-pipeline.md),系统讲解该项目的版本发布(Release Cut)全流程:如何在打 tag 前完成预检、如何把.changes/中的变更笔记扇出到 CHANGELOG、博客、Telegram/Reddit 公告与高亮卡片等多个发布素材,如何安全推送 master 与精确 tag,以及如何用只读验证器把关草稿 Release 与 27 个发布资产。读完本文,你将掌握一套可直接照搬的、以"单份笔记、多端分发、显式排序、手工发布"为核心的发布执行方案,以及每一条约束背后的源码级理由。
说明:文中出现的
v0.24.0、v0.25.1等版本号均为技能文档中的示例值;当前仓库根 package.json 的版本为0.23.0。实际执行时以当时package.json中的版本为准。
一、理解发布管线的两个阶段
IPTVnator 的发布流程被刻意拆成两个阶段,职责完全不同:
- 日常 PR 阶段:每一个对用户可见的变更,在开发上下文还新鲜的时候,向
.changes/目录写入一个<area>-<slug>.md笔记文件,由 CI 的 "Release note gate" 强制约束。 - 发布阶段:
tools/release/build-release-notes.mjs把这些笔记一次性"扇出"到所有发布素材,然后删除它们;版本号则不做任何推导,通过手工 bump 根 package.json 中的version字段来刻意选定。
这套设计的目标很朴素:与其在发布日靠 commit 标题反推三个月的工作,不如在每次 PR 合入时顺手把面向用户的描述写好。.changes/目录中现存的大量真实笔记(如.changes/epg-programme-guide.md、.changes/portal-mark-movie-watched.md、.changes/playback-native-container-routing.md等)就是这种工作流长期运行的直接证据。
笔记的具体格式与字段约束见 .changes/README.md,核心要点是 frontmatter 中type(breaking/feature/fix/perf/internal)与area(小写 slug,与 conventional-commit 的 scope 一致)必填,issues、screenshot、highlight可选,正文面向用户书写且上限 400 字符。
二、Preflight:发布前的预检
release-cut技能文档要求从干净且最新的master分支开始工作,并把目标远端显式命名,随后确认:
- 根
package.json中是裸 semver(如0.24.0,不带v前缀); - 精确的
v<version>tag 在本地与远端都不存在; - CI 全绿;
- 所有变更笔记校验通过。
预检命令:
pnpm run release:notes:validate pnpm run i18n:checkrelease:notes:validate实际执行node tools/release/build-release-notes.mjs --validate(见根 package.json),其内部通过loadNotes()解析.changes/下每个笔记,并额外校验screenshot:字段引用的 slug 必须真实存在于 tools/release/screenshots.manifest.json 的清单中,否则报错退出(见 tools/release/build-release-notes.mjs 的校验逻辑)。i18n:check执行node tools/i18n/check-drift.mjs,确保多语言文案没有漂移。
技能文档还强调:tag 工作流会用node tools/release/extract-changelog-section.mjs --public "${VERSION}"把 CHANGELOG 中对应版本的公开段落写入 GitHub Release body。因此打 tag 之前,完整版 CHANGELOG(包括内部备注)必须已经提交——发布体内容以仓库内已提交的 CHANGELOG 为唯一事实来源。
三、Generate:从笔记到全渠道发布素材
技能文档给出了七步生成流程,其中第 5、6 步必须先于第 7 步执行(原因见下一节)。
1. 设置版本号
修改根 package.json 的version字段为裸 semver。build-release-notes.mjs的resolveVersion()默认就是从package.json读取版本,并把版本正则限制为/^\d+\.\d+\.\d+$/——这保证了"单一事实来源"(见 tools/release/build-release-notes.mjs)。如需在 bump 之前预览某个版本,可用--version 0.24.0覆盖。
2. 生成 CHANGELOG 段落
pnpm run release:notes:changelog该命令执行build-release-notes.mjs --format changelog,把当前.changes/中的全部笔记渲染为 CHANGELOG 的一个版本段落,并插入到CHANGELOG.md中<!-- next-release -->标记之下。重复运行同一版本会替换旧段落而非重复追加(upsertChangelogSection的行为)。
3. 生成博客脚手架
pnpm run release:notes:blog对应--format blog,会向apps/website/src/content/blog/<vX-Y>-release-notes.mdx写入发布博文脚手架(如v0-24)。注意两条规则:
- Minor 发布:脚手架是全新文件,需要人工补齐所有 editorial 字段(叙事引言、影响描述等,脚手架中以
TODO标记); - Patch 发布:网站每个 minor 版本只发布一篇博文,patch 版本必须编辑已有的
vX-Y博文,禁止重新脚手架或强制覆盖。实现上writeBlogScaffold()在目标文件已存在且未传--force时直接抛错拒绝覆盖(见 tools/release/build-release-notes.mjs)。
脚手架的具体排版由renderBlogScaffold(tools/release/release-notes-blog.mjs)决定:叙事引言 →ReleaseMeta→ "What changed" 表格 → 高亮##段落 → Breaking changes → 按主题分组的特性段落 →## Performance→## Everything else(把剩余修复折叠进Spoiler)→ 更新前提醒 →## Thanks→ 下载链接卡片。主题映射来自BLOG_THEMES,未映射的 area 落入 "Other changes" 而不是报错。
4. 只在 mock 服务器上采集截图
pnpm nx run electron-backend:build-e2e # 先构建 e2e 目标 pnpm run release:screenshotsrelease:screenshots执行 tools/release/capture-release-screenshots.ts,只允许对 mock 服务器截图(Xtream mock server、Stalker mock server 等),绝不允许对真实播放列表或账号截图——流、台标与元数据受版权保护,凭据也绝不能进入公开图片。截图发布到apps/website/public/blog/<vX-Y>/screenshots/。
该采集是 fail-closed 的:它会证明真实~/.iptvnator/databases目录(包括 SQLite WAL 侧文件)未被触碰、以白名单环境启动应用、记录并拦截所有非 localhost 流量、逐帧扫描外部资源与凭据形状文本,并断言 TMDB 增强功能保持关闭(详见 .changes/README.md 的 Screenshots 一节与 tools/release/screenshot-guards.mjs)。
5. 渲染公告草稿(输出保存到仓库外)
pnpm --silent run release:notes:telegram pnpm --silent run release:notes:reddit两个命令都向stdout输出可直接粘贴的公告文本,因此必须加--silent——否则 pnpm 的生命周期横幅(形如> iptvnator@0.23.0 release:notes:telegram …)会混入同一 stdout,重定向保存的帖子开头就会多出两行构建噪音。
平台长度约束由渲染器保证:
- Telegram:纯文本 ≤ 4096 字符,超出部分折叠为 "+N more" 计数器;
- Reddit:Markdown ≤ 40,000 字符(仓库累计笔记渲染已达约 37,000),建议标题受 300 字符上限约束,超出从分组列表尾部(breaking → feature → fix → perf 排序,影响最小的先丢弃)裁剪。
breaking 变更永远不会被折叠:若仅 breaking 变更就超出 Telegram 上限,渲染直接以可操作的错误失败,而不是偷偷丢弃一条。内部发布(全部笔记为type: internal)时两个格式都在 stderr 打印说明、stdout 留空并以 0 退出。发布公告本身是手工动作,发生在发布完成之后。
6. 生成高亮卡片
pnpm run release:cards:generate对应 tools/release/generate-highlight-cards.mjs(布局层在 tools/release/highlight-cards.mjs),用 sharp 渲染 1200×630(Open Graph 尺寸)的卡片:每个highlight:笔记一张卡,外加一张同时写成hero.png与博客 frontmatter 引用的hero.jpg的发布主视觉卡。输出落在dist/release-highlight-cards/v<version>/(仓库外、不入版本控制),同名重跑会先清理上一次生成的文件,避免改名或删除的高亮留下陈旧的待发布图片。生成后需要人工审查,若hero.jpg需要作为博文主图,则手工复制进博文资源目录。
7. 消费笔记(--consume)
node tools/release/build-release-notes.mjs --consume第 5、6 步必须在--consume之前完成:highlight:元数据只存在于将被删除的笔记文件中。--consume是破坏性边界——build-release-notes.mjs会遍历.changes/下每个笔记并调用rmSync(note.sourcePath)逐一删除(见 tools/release/build-release-notes.mjs 的 consume 分支),它也是整条管线中唯一会删除文件的模式。随后只暂存 release 拥有的文件(包括精确的网站博文与资源,以及git add -A -- .changes),提交并打精确 tag:
git commit -m "chore(release): v0.24.0" git tag v0.24.0四、为什么顺序是强约束:highlight:的一生
发布管线中最关键的一条排序约束,来自highlight:字段的生命周期:
highlight:是笔记 frontmatter 中的可选字段,用于命名本版本两三个头号变更,上限60 字符,且在type: internal上被拒绝。- 在普通发布素材中,
highlight:驱动三种行为:Telegram 以它打头并把其余折叠成 "+N more";Reddit 为每个高亮开辟## Highlights小节;博客脚手架在开头 "What changed" 表格中为每个高亮占一行,并生成置于其他内容之前的专属##段落。 - 高亮卡片同样只从笔记的
highlight:字段读取。 - CHANGELOG 虽然保留了每个条目的正文,但
highlight:只存在于笔记文件中,消费之后不可恢复。
因此所有读取highlight:的素材——两个公告与卡片——必须在--consume之前渲染;而卡片还要拼接截图,又必须在release:screenshots之后。这就是技能文档强调"Steps 5 and 6 must precede--consume"的源码级原因。
另一个细节:highlight:的 60 字符是创作指导而非渲染保证,因为字符数不等于渲染宽度。卡片布局层(tools/release/highlight-cards.mjs)采用刻意反向的宽度估算模型——枚举窄字符、其余一律按宽字符处理,使得估算只会偏高而不会偏低;34 个W在 font-size 52 下实测约 1948px,而可用宽度仅约 1072px,字符上限的行照样溢出画布。这一保证由 tools/release/highlight-cards.test.mjs 通过 sharp 实际渲染每个样本并断言估算值不低于实测墨迹宽度来守护。
五、推送与外部效应:master 与 tag 严格隔离
技能文档对推送有非常明确的两条命令要求:先推远端master,再以第二条独立命令只推精确的v<version>tag,绝不用宽泛的git push --tags。以远端upstream、版本v0.25.1为例:
git push upstream master git push upstream v0.25.1这样做的原因在于外部效应:master与v*推送都可能触发 Docker 镜像发布,而tag 构建会创建一个草稿 GitHub Release(对应 .github/workflows/build-and-make.yaml 中的create-releasejob)。分开推送可以精确控制每一步触发的副作用,宽泛的--tags则会把不该上线的 tag 一并推出去。
tag 工作流在发布体上的具体行为,见 .github/workflows/build-and-make.yaml:tag 事件下先用node tools/release/extract-changelog-section.mjs --public "${VERSION}"提取本地 CHANGELOG 的公开段落作为作者正文,再把 GitHub 自动生成的 commit 列表追加其后组成FULL_BODY写入草稿。也就是说,发布体永远非空——"作者正文缺失"无法用简单的空字符串检测来发现。
六、草稿验证:只读把关release:verify:draft
tag 推送后运行:
pnpm run release:verify:draft执行 tools/release/verify-draft-release.mjs。它严格只读:绝不发布、编辑或删除任何内容。验证管线分三步:
- 找到 run:
gh run list只反映"当下已索引"的 run,其--limit只是返回条数上限、并不会等待;刚推的 tag 往往还没被索引。验证器以 10 次尝试、每次间隔 6 秒的方式轮询,超时才判定"tag 从未推送"。 - 等待完成:进行中的 run 通过
gh run watch --exit-status流式跟进;已完成但结论非成功的 run 立即失败。缺失的gh二进制或被中断的 watch 会被如实报告(spawnSync将两者呈现为status: null),而不是误报为构建失败。 - 检查草稿:校验草稿状态、作者正文,以及下面完整的资产集合。
作者正文的检查方式值得注意:它把发布体与本地 CHANGELOG.md 中对应版本的段落做包含比较,而不是和空字符串比较——因为 tag 工作流总是会追加 GitHub 生成的 notes(FULL_BODY),空字符串测试永远不可能失败。内部发布(公开段落合法为空)被如实报告为"无需作者正文",而不是发出警告。已经发布的 Release 依然会得到资产审计报告(事后审计有用),但绝不会返回成功退出码——对一个前置发布门禁来说,发布后报"通过"等于声称边界已经被跨越。
七、27 资产契约:多平台矩阵完整性检查
requiredAssetRules()(见 tools/release/verify-draft-release.mjs)对着一个真实的完整矩阵构建验证过,定义了发布必须携带的 27 个资产。当构建矩阵增减目标时,必须在同一 PR 中同步更新该函数。完整清单如下:
| 平台 | 资产 |
|---|---|
| macOS | -mac-{x64,arm64}.{dmg,zip}+ 各一个.blockmap(8 个) |
| Windows | -windows-x64-setup.exe+.blockmap(2 个) |
| DEB | -linux-{amd64,arm64,armv7l}.deb(3 个) |
| AppImage | -linux-{x86_64,arm64,armv7l}.AppImage(3 个) |
| Snap | -linux-{amd64,armhf}.snap(2 个) |
| RPM | -linux-x86_64.rpm(1 个) |
| Flatpak | -linux-x86_64.flatpak(1 个) |
| Pacman | -linux-x64.pacman或-linux-x86_64.pkg.tar.*(1 个) |
| 更新器元数据 | latest.yml、latest-mac.yml、latest-linux.yml、latest-linux-arm.yml、latest-linux-arm64.yml(5 个) |
| 源码合规 | linux-frame-copy-runtime-sources.tar.xz(1 个) |
实现细节上,规则用纯字符串比较而非由版本拼接的正则——requiredAssetRules()是被导出的,对插值后的版本值做正确转义将是一个常设陷阱。Pacman 规则之所以接受两种形状,是因为 Electron Builder 历史上输出过两种 pacman 工件形态。任何没有被规则认领的资产以NOTE:报告且不导致失败:新构建目标应当浮出水面供人注意,而不是在规则更新前卡死整个发布。
八、验证与发布之后的手工动作
草稿验证通过后,发布动作是手工的:
- 人工审查 author 正文与生成的 commits;
- 手工发布 GitHub Release——发布动作会自动验证 Snap 资产并将其上传到
edge;已安装 Snap 的冒烟测试以及 candidate/stable 晋升仍然是手动的(参见 tools/packaging/validate-snap-release-boundary.mjs); - 在工件验证期间保持博文为草稿,随后用后续 commit 发布博文并验证网站部署。
技能的配套验证命令:
pnpm run release:notes:validate # 每个笔记都能解析并通过 schema pnpm nx run release-tools:test # 工具链自身的单元测试 pnpm nx run release-tools:lint九、失败安全:如何优雅回滚
技能文档给出了两个典型失败场景的处理方式:
- CHANGELOG 段落缺失:tag 构建中的
extract-changelog-section.mjs找不到对应版本段落时直接让发布失败——一个忘记执行release:notes:changelog就打的 tag 不可能静默地只发布 PR 标题级笔记(见 tools/release/extract-changelog-section.mjs,其在 section 缺失或为空时返回非零退出码并给出可操作的重试提示)。 - 恢复步骤:重新生成 → 提交 → 在确认其精确目标后先删除本地与远端坏 tag→ 重新打 tag。
除此之外还有一条硬性纪律:在源码归档与 Snap 契约通过之前,绝不发布草稿。--consume后.changes/变空本身也是一个信号——空目录意味着"这次发布没有用户可见变更",而"内部发布"与"空目录"被刻意区分为两种结局:空目录会失败,因为它几乎总是意味着某一步在--consume之后才运行,这正是整条管线要防止的唯一排序错误(详见 docs/architecture/release-pipeline.md)。
十、把发布流程串起来:完整命令序列
综合技能文档、.changes/README.md与发布管线契约,一次完整发布的标准序列是:
# 预检 pnpm run release:notes:validate pnpm run i18n:check # 1. bump 根 package.json 的 version # 2. 生成 CHANGELOG 段落 pnpm run release:notes:changelog # 3. 生成博客脚手架(patch 版本则编辑既有 vX-Y 博文) pnpm run release:notes:blog # 4. 只对 mock 服务器采集截图 pnpm nx run electron-backend:build-e2e pnpm run release:screenshots # 5. 渲染公告草稿到仓库外(--silent 防横幅污染 stdout) pnpm --silent run release:notes:telegram pnpm --silent run release:notes:reddit # 6. 生成高亮卡片并人工审查 pnpm run release:cards:generate # 7. 消费笔记(破坏性边界,必须最后) node tools/release/build-release-notes.mjs --consume # 暂存 release 拥有的文件并提交打 tag git add -A -- .changes git commit -m "chore(release): v0.24.0" git tag v0.24.0 # 先推 master,再单独推精确 tag git push upstream master git push upstream v0.24.0 # 只读验证草稿与 27 资产 pnpm run release:verify:draft核心心智模型可以浓缩为一句话:一切读取highlight:的步骤都在--consume之前,一切外部副作用(Docker、草稿 Release)都由精确推送精确触发,而发布本身永远是人的决定。这套约束既写进了技能文档,也固化在 docs/architecture/release-pipeline.md 的契约、tools/release/build-release-notes.mjs 的实现与 .github/workflows/build-and-make.yaml 的工作流中——三份证据指向同一条边界,这正是它值得信赖的原因。
【免费下载链接】iptvnator:tv: Cross-platform IPTV player application with multiple features, such as support of m3u and m3u8 playlists, favorites, TV guide, TV archive/catchup and more.项目地址: https://gitcode.com/GitHub_Trending/ip/iptvnator
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考