IPTVnator Release Cut 发布流程实战:从变更笔记到草稿 Release 的完整契约
2026/9/17 3:19:15 网站建设 项目流程

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.0v0.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 中typebreaking/feature/fix/perf/internal)与area(小写 slug,与 conventional-commit 的 scope 一致)必填,issuesscreenshothighlight可选,正文面向用户书写且上限 400 字符。

二、Preflight:发布前的预检

release-cut技能文档要求从干净且最新的master分支开始工作,并把目标远端显式命名,随后确认:

  1. package.json中是裸 semver(如0.24.0,不带v前缀);
  2. 精确的v<version>tag 在本地与远端都不存在
  3. CI 全绿;
  4. 所有变更笔记校验通过。

预检命令:

pnpm run release:notes:validate pnpm run i18n:check
  • release: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.mjsresolveVersion()默认就是从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:screenshots

release: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:字段的生命周期:

  1. highlight:是笔记 frontmatter 中的可选字段,用于命名本版本两三个头号变更,上限60 字符,且在type: internal上被拒绝。
  2. 在普通发布素材中,highlight:驱动三种行为:Telegram 以它打头并把其余折叠成 "+N more";Reddit 为每个高亮开辟## Highlights小节;博客脚手架在开头 "What changed" 表格中为每个高亮占一行,并生成置于其他内容之前的专属##段落。
  3. 高亮卡片同样只从笔记的highlight:字段读取。
  4. 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

这样做的原因在于外部效应:masterv*推送都可能触发 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。它严格只读:绝不发布、编辑或删除任何内容。验证管线分三步:

  1. 找到 rungh run list只反映"当下已索引"的 run,其--limit只是返回条数上限、并不会等待;刚推的 tag 往往还没被索引。验证器以 10 次尝试、每次间隔 6 秒的方式轮询,超时才判定"tag 从未推送"。
  2. 等待完成:进行中的 run 通过gh run watch --exit-status流式跟进;已完成但结论非成功的 run 立即失败。缺失的gh二进制或被中断的 watch 会被如实报告(spawnSync将两者呈现为status: null),而不是误报为构建失败。
  3. 检查草稿:校验草稿状态、作者正文,以及下面完整的资产集合。

作者正文的检查方式值得注意:它把发布体与本地 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.ymllatest-mac.ymllatest-linux.ymllatest-linux-arm.ymllatest-linux-arm64.yml(5 个)
源码合规linux-frame-copy-runtime-sources.tar.xz(1 个)

实现细节上,规则用纯字符串比较而非由版本拼接的正则——requiredAssetRules()是被导出的,对插值后的版本值做正确转义将是一个常设陷阱。Pacman 规则之所以接受两种形状,是因为 Electron Builder 历史上输出过两种 pacman 工件形态。任何没有被规则认领的资产以NOTE:报告且导致失败:新构建目标应当浮出水面供人注意,而不是在规则更新前卡死整个发布。

八、验证与发布之后的手工动作

草稿验证通过后,发布动作是手工的:

  1. 人工审查 author 正文与生成的 commits;
  2. 手工发布 GitHub Release——发布动作会自动验证 Snap 资产并将其上传到edge;已安装 Snap 的冒烟测试以及 candidate/stable 晋升仍然是手动的(参见 tools/packaging/validate-snap-release-boundary.mjs);
  3. 在工件验证期间保持博文为草稿,随后用后续 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询