版本、模板与升级窗口治理
周一早晨,开发机上的 IDE 扩展自动升级,格式化结果突然变化;脚手架生成的新项目使用另一套运行时;CI 中的构建 Action 仍指向可移动标签;项目模板新增了必填配置,却没有迁移旧仓库。单台机器看起来只是“插件坏了”,整个团队看到的却是四条版本链同时漂移:客户端、插件、自动化依赖和模板生成物没有共同基线,升级也没有可重放的证据。
版本治理要保护的不是某个数字,而是团队对同一输入得到可解释结果的能力。资产清单回答“现在运行什么”,批准基线回答“允许运行什么”,兼容矩阵回答“这些版本能否组合”,升级批次回答“谁先变、何时停止”,迁移记录回答“持久数据和项目文件改变了什么”,回滚包则回答“失败后怎样恢复”。少一环,所谓升级窗口就只是日历邀请。
先把版本事故还原成四类证据
不要先让所有人重装。先采集故障机和正常机的版本快照,并把自动更新、配置来源和项目模板修订号一起记录。仅比较应用版本会漏掉扩展、运行时、锁文件、远程开发端以及模板生成后的差异。
# 只读采集示例;输出中不应包含 token、代理口令或用户主目录文件
code --version
code --list-extensions --show-versions | Sort-Object
node --version
npm --version
git rev-parse --short HEAD
git status --short预期证据是可排序的 publisher.extension@version 列表、工具版本、仓库修订和干净状态。若故障机多出预发布扩展、扩展版本高于批准值、项目锁文件发生未解释变化,或同一命令在本地与 CI 解析出不同运行时,先把差异归类,不能用“都更新到最新”覆盖现场。
事故复盘至少区分四种漂移:二进制或客户端漂移、插件漂移、自动化依赖漂移、模板与生成物漂移。它们的回滚手段不同。客户端可能需要安装包,扩展需要旧版本包或禁用,CI Action 需要恢复不可变提交,模板升级则可能涉及文件合并和数据迁移,不能靠重装解决。
资产清单让每个版本都有 owner 和证据
清单不是采购表。一个条目必须能定位实际对象、版本来源、批准范围、自动更新策略、兼容对象、风险等级、owner、回滚材料和下次复查时间。版本值应来自可验证命令、锁文件、制品摘要或供应商 API,不能只由使用者手填。
# governance/tool-assets.yaml:所有名称和地址均为合成示例
schema: 1
assets:
- id: ide.vscode
kind: desktop-client
owner: developer-experience
source: vendor-stable-channel
approved: "<approved-version>"
observed_by: "code --version"
update_mode: staged
risk: medium
compatible_with: [runtime.node, extension.linter]
rollback_artifact: "artifact://tool-baseline/ide/vscode/<version>"
- id: extension.linter
kind: ide-extension
owner: frontend-platform
source: approved-marketplace
approved: "publisher.extension@<approved-version>"
observed_by: "code --list-extensions --show-versions"
update_mode: manual
risk: high
changes_project_files: true
- id: template.service
kind: project-template
owner: architecture-enablement
source: "git@example.invalid:templates/service.git"
approved: "<immutable-commit>"
update_mode: pull-request
migration_required: trueapproved 和 observed 必须分开:批准值是控制面,观测值是运行事实。两者不一致时产生漂移事件,而不是悄悄覆盖批准值。source 还要记录稳定通道、预发布通道、内部镜像或不可变提交,避免相同版本名指向不同内容。高风险资产还应保存 SHA-256、签名验证结果或内部制品坐标。
版本号只能表达发布者承诺的兼容语义。Semantic Versioning 的前提是发布者已经声明清晰的公共 API;主、次、修订号描述的是这套公共 API 的不兼容变化、向后兼容功能和向后兼容修复,不是对操作系统、配置默认值、数据格式、性能或安全性的全量担保。0.y.z 仍处于初始开发阶段,兼容性不能按稳定版本推断;预发布版本可能不满足对应普通版本的兼容要求;构建元数据不参与版本优先级比较。插件、桌面客户端和模板还可能根本没有承诺 SemVer。因此 major/minor/patch 只能帮助分级,不能替代 release notes、制品摘要、兼容实验和回滚判断。
批准基线不是“最新版本”这一句话
批准基线是一组可共同运行的版本和策略。它应同时标识最低允许值、推荐值、禁止值、到期日和例外。最低值阻止已知漏洞或不再支持版本,推荐值控制团队默认环境,禁止值用于紧急撤回,例外则必须有 owner、原因和失效时间。
{
"baselineId": "dev-tooling-stable",
"status": "approved",
"effectiveAt": "<window-start>",
"expiresAt": "<review-deadline>",
"rules": [
{
"asset": "extension.linter",
"recommended": "<target-version>",
"minimum": "<minimum-safe-version>",
"blocked": ["<withdrawn-version>"],
"autoUpdate": false
},
{
"asset": "template.service",
"recommended": "<immutable-commit>",
"migration": "migrations/template/<change-id>.md"
}
]
}VS Code 的扩展管理文档说明了安装指定扩展版本、关闭全部或单个扩展自动更新以及查看待更新扩展的入口;企业策略还可以按发布者、扩展、版本和平台建立允许规则,见企业扩展管理。组织准入能力有明确版本门槛,逐版本规则不接受版本范围,stable 只排除预发布版,并不代表该版本已完成安全或兼容审查。策略值若存在语法错误,规则可能完全不生效;本地用户设置也不能代替设备管理下发的组织策略。基线记录因此要同时保存客户端版本、策略原文、下发范围、窗口日志和一条应被拒绝的安装证据,不能把“管理员应该配置”或设置页显示某个值当成已生效。
正向验证是在隔离配置目录中安装批准版本,执行最小功能测试并确认观测值等于批准值。反向验证则放入一个明确禁止版本,预期门禁返回非零退出码并指出资产、观测值和规则来源。若门禁只是告警,团队必须明确哪些环境允许继续,生产构建和模板发布通常不应带着未知版本继续。
Release notes 必须转成兼容矩阵
Release notes 是输入,不是结论。升级负责人要把变化映射到团队实际使用面:操作系统与 CPU、运行时、IDE、插件 API、远程开发端、项目配置、文件格式、网络端点、身份权限、许可证、遥测、模板变量和 CI 镜像。没有使用到的功能可以标记不适用,但不能整份 release notes 只写“已阅读”。
change: UPG-DEMO-042
target: extension.linter@<target-version>
release_notes: "https://vendor.example.invalid/releases/<target-version>"
compatibility:
- dimension: ide
candidates: [stable-windows, stable-macos, remote-linux]
result: pass
evidence: artifacts/UPG-DEMO-042/ide-matrix.json
- dimension: extension-host-api
candidates: [desktop-local, remote-extension-host, web-extension-host]
result: conditional
condition: "manifest engines.vscode 与实际宿主版本同时满足,且远端宿主单独通过"
evidence: artifacts/UPG-DEMO-042/extension-host.json
- dimension: runtime-and-runner
candidates: [node-lts, ci-hosted-runner, ci-self-hosted-runner]
result: pass
evidence: artifacts/UPG-DEMO-042/runtime-runner.json
- dimension: project-config
candidates: [legacy-config, flat-config]
result: conditional
condition: "legacy repository keeps compatibility adapter"
evidence: artifacts/UPG-DEMO-042/project-config.json
- dimension: template-engine
candidates: [old-renderer, candidate-renderer]
result: pass
evidence: artifacts/UPG-DEMO-042/template-engine.json
- dimension: generated-schema
candidates: [new-project, migrated-project, customized-project]
result: conditional
condition: "迁移器遇到项目自定义块时停止并生成冲突报告"
evidence: artifacts/UPG-DEMO-042/generated-schema.json
- dimension: output-format
candidates: [baseline-fixtures]
result: fail
evidence: artifacts/UPG-DEMO-042/format.diff
disposition: block矩阵的单元格必须是 pass、fail、conditional 或 not-applicable,并带证据路径。空白不是“尚未发现问题”,而是未验证。conditional 要写出条件和检查方法,例如只在特定运行时、远程端版本或旧配置适配器存在时通过。插件不能只测“能安装”:宿主声明允许加载,只说明版本约束满足;激活、命令注册、语言服务、工作区文件写入、远程扩展宿主和卸载后残留都要分别取证。一个插件还可能携带原生模块或调用外部 CLI,宿主兼容不代表操作系统、CPU 和运行时 ABI 兼容。
兼容矩阵还要计算依赖闭包。候选插件依赖的扩展、模板引用的基础镜像、CI 调用的 Action、生成项目锁定的包以及迁移器自身运行时,都是升级对象。只把顶层版本写入矩阵,会出现顶层版本未变、可移动标签或传递依赖已经变化的假稳定。制品证据应保存解析后的完整依赖、来源仓库、不可变摘要和签名结果;同名同版本但摘要不同必须阻断,而不是任选一个继续。
模板尤其要比较生成前后的语义。固定同一组答案分别用旧模板和候选模板生成项目,对依赖清单、工作流、权限、端口、配置键和测试输出做结构化 diff。大量格式噪声应先归一化,否则真正的权限扩大或默认值变化会被淹没。
用可复制实验验证模板迁移而不是只生成新项目
模板升级最常见的误判是“新建项目成功,所以升级安全”。真实团队还有数十个已存在仓库。候选模板必须同时验证新建路径和旧项目迁移路径,并明确哪些文件由模板拥有、哪些文件允许项目自主修改。
$ErrorActionPreference = "Stop"
$work = Join-Path $env:TEMP "template-upgrade-demo"
Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $work | Out-Null
# 这里用受控脚本模拟旧/新模板;真实项目替换为已锁定提交的生成器
git clone --depth 1 --branch "<old-tag>" https://example.invalid/templates/service "$work/old-template"
git clone --depth 1 --branch "<candidate-tag>" https://example.invalid/templates/service "$work/new-template"
& "$work/old-template/render.ps1" -Name demo-service -Output "$work/baseline"
Copy-Item "$work/baseline" "$work/migrated" -Recurse
& "$work/new-template/migrate.ps1" -Project "$work/migrated" -DryRun
git diff --no-index -- "$work/baseline" "$work/migrated"这些地址是占位符,命令不会在当前仓库中执行。落地时应把模板源替换为内部只读镜像,并把标签解析成不可变提交。预期正向结果是 dry-run 只列出模板拥有文件,生成迁移报告且不触碰业务源码。反向样本可在业务文件中加入与模板同名的自定义段落;若迁移脚本直接覆盖而没有冲突退出码,候选升级应被阻断。
模板变更还要定义“删除语义”。模板删除一个配置键,不代表所有项目都能立即删除;旧运行时可能仍读取它。迁移计划应写明读取兼容期、双写或适配器、验证查询、最终删除条件和回滚时是否能恢复原值。不可逆的数据格式变更必须在升级前完成备份与恢复演练。
模板长期演进还需要一份机器可读的所有权清单,至少记录模板修订、渲染参数摘要、受管文件、受管区块和项目明确接管的路径。迁移器以“旧模板基线、项目当前状态、候选模板”做三方比较:候选只改了模板受管内容时可以自动应用;项目和候选同时改了同一区块时必须输出冲突并停止;项目已接管的文件不得重新夺回。若仓库没有旧基线,迁移器只能生成建议补丁,不能把当前项目误当成未修改模板。
正向实验应把同一迁移连续执行两次。第一次产生预期补丁并把模板修订从旧值推进到候选值,第二次应得到空变更;这证明迁移具有幂等性。反向实验先修改一个受管区块,再让候选模板修改同一位置,预期得到稳定的冲突退出码、文件路径和基线摘要。迁移器若第二次仍持续改文件,或冲突后仍更新模板修订号,就会制造“记录已升级、生成物仍漂移”的假完成,必须阻断批次。
灰度批次要按风险隔离而不是随机抽人
灰度不是把新版本先发给几位同事。应选择能覆盖风险维度、又不会同时破坏关键交付的批次:工具维护者和沙箱项目为第零批;不同操作系统、远程开发和典型仓库为第一批;非关键团队为第二批;其余团队最后推进。每批都设置观测时长、成功阈值、停止条件和明确 owner。
upgrade:
id: UPG-DEMO-042
asset: extension.linter
from: "<approved-version>"
to: "<candidate-version>"
generation: 7
state: canary
batches:
- name: sandbox
population: 5
duration: 2d
- name: representative
population: 20
dimensions: [windows, macos, remote-linux, monorepo]
duration: 5d
success:
install_rate: ">= 98%"
rollback_rate: "< 2%"
format_diff_unexplained: 0
critical_incidents: 0
stop:
- project-file-corruption
- credential-prompt-regression
- build-failure-rate-above-baseline成功指标必须有采集位置。安装率来自设备或扩展清单,构建失败来自 CI,回滚率来自升级代理,格式差异来自固定 fixtures。用户口头说“没问题”只能作为补充。若无法采集关键指标,就缩小批次并增加人工验证,不能放宽停止条件。
批次推进应是带版本号的状态机,而不是多位管理员共同改一个 status:prepared -> canary -> observing -> expanding -> completed 是正向路径,任一运行态都可以进入 paused 或 rolling-back。控制器每次提交都比较 generation;只有读到第 7 代记录的执行者才能提交第 8 代,过期执行者必须失败。这样可以避免一个人触发回滚时,另一个自动任务仍按旧记录扩大批次。completed 只表示目标批次结束,旧制品和旧配置仍要保留到观察期关闭。
canary 的停止判断应同时使用绝对阻断项和相对基线。凭据泄漏、项目文件损坏和不可恢复迁移一例即停;构建失败率、启动耗时和资源消耗则与同类仓库的升级前基线比较。固定写成“失败率低于某个万能百分比”会在低样本批次中失真。样本不足时延长观察或人工复核,不得用零告警推断零故障。
自动化依赖还要考虑供应链。GitHub 建议第三方 Action 使用完整提交 SHA,因为 SHA 是不可变引用;标签和分支可能移动,具体取舍见 Secure use reference。锁到 SHA 会失去自动接收更新的便利,因此资产清单必须同时记录对应发布标签和升级检查任务,不能锁住后永远不再复查。
冻结窗口保护的是可诊断性
冻结不是永久拒绝升级。它在发布高峰、重大迁移、事故处理中或关键人员不可用时,暂停非紧急变化,减少同时变量。冻结记录应包含开始和结束、覆盖资产、例外批准者、允许的安全修复等级、验证要求和窗口结束后的积压处理人。
紧急安全升级不能绕过证据链。它可以缩短灰度时长、减少矩阵维度,但必须保留候选摘要、漏洞影响、最小实验、回滚包和事后补验计划。若漏洞已被利用,回滚到易受攻击版本可能比继续运行候选版本更危险,此时回滚动作应改为禁用功能、隔离网络或切换受控替代品。
反向实验可以在合成升级请求中把 window.status 设为 frozen,同时不给 emergencyApproval。预期门禁拒绝变更并输出冻结记录 ID。再加入有效期内、双人批准且绑定具体资产的紧急例外,门禁才允许进入沙箱批次;通配符例外或无到期时间应继续失败。
回滚要覆盖二进制、配置、模板和数据
“重新安装旧版本”只覆盖一部分。完整回滚清单包括旧安装包或制品摘要、扩展包、配置快照、模板提交、生成物反向补丁、数据备份、迁移版本、凭据与权限变化,以及恢复后的验证命令。回滚材料必须在升级前取得,失败后再寻找旧包通常已经太晚。
rollback/UPG-DEMO-042/
manifest.yaml # 资产、版本、SHA-256、owner
packages/ # 经批准的离线或内部镜像坐标
config/before/ # 脱敏配置快照
template/reverse.patch # 可审查的生成物反向补丁
migration/restore.md # 数据恢复与兼容读路径
verify/commands.md # 恢复后必须执行的检查
evidence/ # 退出码、报告、diff 与签收正向回滚演练先在隔离仓库安装候选版本,制造一个无害失败,再恢复旧版本和配置。预期结果不仅是命令退出为零,还要确认项目文件哈希、测试结果和关键设置回到基线。反向演练故意删掉旧扩展包或反向迁移脚本;门禁应在升级前报告 rollback-not-ready,而不是等事故发生才暴露缺口。
有些迁移不可逆,例如候选工具升级了共享索引或重写项目元数据。此时不能宣称“一键回滚”,而应采用向前修复、双版本读取、蓝绿数据副本或先导出再切换。决策记录必须明确最大可接受数据损失、恢复时长和谁有权触发。
回滚还要处理“客户端已经回退,但模板生成物和共享数据仍是新格式”的混合状态。恢复步骤应按依赖逆序执行:停止继续扩批,冻结写入,恢复兼容读路径或旧副本,回退生成物和配置,最后回退客户端与插件。恢复后重新采集版本、模板修订、文件摘要和数据 schema;任何一项仍处于候选状态,都只能标记为部分回滚,不能解除事故状态。
用门禁把升级记录接入仓库和 CI
人工评审负责判断,脚本负责拒绝明显缺口。门禁至少检查:资产存在、目标版本不在禁用列表、release notes 已关联、兼容矩阵无空白和未处置失败、灰度批次与停止条件存在、冻结例外有效、回滚清单完整、迁移有 dry-run 或恢复证据。
// scripts/check-upgrade-record.mjs 的核心判定示例
const required = ["id", "asset", "from", "to", "owner", "releaseNotes", "rollback"];
const missing = required.filter((key) => !record[key]);
const unresolved = record.compatibility.filter((item) =>
!["pass", "conditional", "not-applicable"].includes(item.result)
);
if (missing.length || unresolved.length || record.rollback.status !== "ready") {
console.error(JSON.stringify({ missing, unresolved, rollback: record.rollback.status }, null, 2));
process.exit(1);
}CI 的输出应指出缺失字段和失败维度,避免只给 validation failed。升级 PR 合并后,自动化把候选基线变为批准基线,并保留旧基线到回滚观察期结束。不能让 CI 使用能改写资产仓库、批准策略和制品库的同一个永久凭据;读取、提交候选、批准和发布应分权,短期凭据还要限制仓库与环境。
长期维护看漂移、积压、成本与退出
团队每个周期至少复查四类指标:批准值与观测值的漂移率、超过支持期限的资产数、待升级积压年龄、升级与回滚失败率。还要看制品镜像和旧安装包的存储成本,以及维持多版本兼容的工程成本。长期保留所有版本看似稳妥,实际会扩大漏洞面和恢复选择困难。
版本 owner 负责解释 release notes、维护矩阵与回滚材料;平台团队维护采集和门禁;安全人员定义高风险例外;项目 owner 验证业务 fixtures;服务台负责终端分发证据。任何角色离开时,资产记录和回滚权限都要交接,不能只转移文档链接。
退出一个工具时,先冻结新安装和新模板生成,再导出批准基线与历史升级证据,替换项目集成,撤销更新源和发布凭据,删除不再需要的内部包,最后验证设备、CI 和模板仓库都不再引用它。只有资产清单中的观测值归零、例外关闭、费用停止且审计仍可查询,退出才算完成。
升级前后的工程检查单
资产清单能同时给出批准值、观测值、来源、摘要、owner 和回滚材料。release notes 已映射到操作系统、运行时、插件、项目配置、权限、网络和数据格式。兼容矩阵没有空白;每个条件通过项都有条件与检查方法。
新建模板与旧项目迁移都做了结构化 diff,业务自定义文件不会被静默覆盖。灰度批次覆盖真实风险维度,并有可采集的成功阈值和停止条件。冻结窗口与紧急例外都有到期时间、批准人和资产边界。
回滚包在升级前完成验证;不可逆迁移写明向前恢复和数据保护策略。CI 只使用最小权限凭据,候选、批准、发布和回滚动作可审计。观察期结束后关闭临时权限、清理候选制品、更新基线并安排下次复查。
