diff --git a/.github/scripts/test_maintenance_workflow.py b/.github/scripts/test_maintenance_workflow.py index a86b37919..4220b6938 100644 --- a/.github/scripts/test_maintenance_workflow.py +++ b/.github/scripts/test_maintenance_workflow.py @@ -148,6 +148,21 @@ def test_each_workflow_uses_only_approved_action_majors(self) -> None: ) self.assert_action_matrix(self.stale_workflow, {"actions/stale"}) + def test_node24_actions_stay_on_github_hosted_runners(self) -> None: + for workflow in ( + self.workflow, + self.release_workflow, + self.stale_workflow, + ): + self.assertNotIn("self-hosted", workflow) + runs_on = { + line.strip() + for line in workflow.splitlines() + if line.strip().startswith("runs-on:") + } + self.assertGreater(len(runs_on), 0) + self.assertEqual({"runs-on: ubuntu-latest"}, runs_on) + def test_all_pull_requests_and_master_pushes_trigger(self) -> None: self.assertIn("pull_request:", self.workflow) self.assertIn("push:", self.workflow) diff --git a/openspec/changes/upgrade-github-actions-v6/.openspec.yaml b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/.openspec.yaml similarity index 100% rename from openspec/changes/upgrade-github-actions-v6/.openspec.yaml rename to openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/.openspec.yaml diff --git a/openspec/changes/upgrade-github-actions-v6/design.md b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/design.md similarity index 100% rename from openspec/changes/upgrade-github-actions-v6/design.md rename to openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/design.md diff --git a/openspec/changes/upgrade-github-actions-v6/proposal.md b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/proposal.md similarity index 100% rename from openspec/changes/upgrade-github-actions-v6/proposal.md rename to openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/proposal.md diff --git a/openspec/changes/upgrade-github-actions-v6/specs/maintenance-quality-baseline/spec.md b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/specs/maintenance-quality-baseline/spec.md similarity index 100% rename from openspec/changes/upgrade-github-actions-v6/specs/maintenance-quality-baseline/spec.md rename to openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/specs/maintenance-quality-baseline/spec.md diff --git a/openspec/changes/upgrade-github-actions-v6/specs/release-verification/spec.md b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/specs/release-verification/spec.md similarity index 100% rename from openspec/changes/upgrade-github-actions-v6/specs/release-verification/spec.md rename to openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/specs/release-verification/spec.md diff --git a/openspec/changes/upgrade-github-actions-v6/specs/repository-maintenance-governance/spec.md b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/specs/repository-maintenance-governance/spec.md similarity index 100% rename from openspec/changes/upgrade-github-actions-v6/specs/repository-maintenance-governance/spec.md rename to openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/specs/repository-maintenance-governance/spec.md diff --git a/openspec/changes/upgrade-github-actions-v6/tasks.md b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/tasks.md similarity index 98% rename from openspec/changes/upgrade-github-actions-v6/tasks.md rename to openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/tasks.md index b3d8d3665..ec3547724 100644 --- a/openspec/changes/upgrade-github-actions-v6/tasks.md +++ b/openspec/changes/archive/2026-09-01-upgrade-github-actions-v6/tasks.md @@ -80,7 +80,7 @@ Android 确实使用 `--no-build-cache` 编译源码。 - [x] 6.3 核对替代 Pull Request 没有触发 `Release Build`,没有读取正式签名 Secrets、推送 tag、创建草稿 Release 或改变 stale 实际对象;任何失败均保持 #73 打开并停止合并。 -- [ ] 6.4 人工评审通过后合并替代 Pull Request,重新读取 `origin/master` SHA、合并提交检查、 +- [x] 6.4 人工评审通过后合并替代 Pull Request,重新读取 `origin/master` SHA、合并提交检查、 三个工作流和 OpenSpec 状态;只有读回结果完整时才以替代 Pull Request 链接关闭 #73。 -- [ ] 6.5 按验证与归档流程同步主规范、归档 `upgrade-github-actions-v6`,在归档提交和合并后的 +- [x] 6.5 按验证与归档流程同步主规范、归档 `upgrade-github-actions-v6`,在归档提交和合并后的 `master` 上再次严格校验,并只按精确 ref/SHA 清理已合并临时分支;不触发正式发布。 diff --git a/openspec/specs/maintenance-quality-baseline/spec.md b/openspec/specs/maintenance-quality-baseline/spec.md index 6811ed224..5b48085d6 100644 --- a/openspec/specs/maintenance-quality-baseline/spec.md +++ b/openspec/specs/maintenance-quality-baseline/spec.md @@ -92,3 +92,56 @@ Pull Request 和 `master` 的适用验证 MUST 执行 Android 单元测试、And - **WHEN** 某项建议修复需要提高最低 SDK、改变持久化格式或突破明确固定的依赖版本 - **THEN** 系统不得把该修改作为机械 warning 清理合并 - **AND** 必须将其延期或建立独立兼容性变更 + +### Requirement: 工作流依赖运行时和关键安全合同必须失败即停止 +Pull Request、`master`、发布和 stale 工作流使用的 JavaScript Action MUST 与执行 runner +兼容;本次目标 Action 运行于 Node 24,runner 版本 MUST 不低于 `2.327.1`。仓库契约 MUST +精确覆盖三个工作流中的目标 Action 大版本,并继续覆盖 CodeQL v4、Android CodeQL +`--no-build-cache`、JDK 17、Node.js 22、pnpm 9.15.9、只读 Web 构建、正式签名 Secrets +隔离和稳定聚合门禁。任一版本或安全边界漂移 MUST 使仓库检查失败,不能以部分工作成功代替。 + +#### Scenario: 目标 Action 版本与契约不一致 +- **WHEN** 任一工作流使用的目标 Action 大版本与批准组合不一致 +- **THEN** 仓库契约检查失败并指出发生漂移的工作流和 Action +- **AND** 稳定聚合门禁报告失败 + +#### Scenario: runner 不满足 Node 24 下限 +- **WHEN** 工作流在低于 `2.327.1` 的 runner 上执行 Node 24 Action +- **THEN** 对应检查必须失败或被明确阻止 +- **AND** 系统不得把未执行的后续构建描述为通过 + +#### Scenario: 维护契约真实 RED 被同步修复 +- **WHEN** Action 大版本升级使旧版本精确断言失败 +- **THEN** 系统只将断言更新为逐工作流批准的新版本并补齐缺失覆盖 +- **AND** 不删除 JDK、Node.js、pnpm、CodeQL、缓存、签名隔离或聚合门禁断言 + +#### Scenario: 工作流实现文件发生变化 +- **WHEN** Pull Request 修改测试、发布、stale 工作流或其仓库契约 +- **THEN** 变更范围识别必须触发 Android、Web、CodeQL、OpenSpec 和仓库验证的完整适用门禁 +- **AND** 每个实际运行的失败均阻止稳定聚合门禁通过 + +### Requirement: Gradle Action 缓存升级必须显式选择开放边界并保留可审计失败 +使用 `gradle/actions/setup-gradle@v6` 的工作流 MUST 显式选择 100% MIT 的 `basic` 缓存 +provider,不得加载默认的 enhanced 商业缓存组件,并将升级后的首次缓存未命中视为预期冷启动 +而非构建失败。缓存保存或恢复异常 MUST 在作业摘要或日志中可见,且 Android CodeQL 构建 +MUST 继续使用 `--no-build-cache`,不得用缓存命中替代 Java/Kotlin 源码编译证据。 + +#### Scenario: 缓存 provider 配置漂移 +- **WHEN** `setup-gradle@v6` 没有显式选择 `basic` provider 或改为 enhanced provider +- **THEN** 仓库契约检查失败 +- **AND** 变更不得在缺少新的独立评审时合并 + +#### Scenario: 升级后首次运行缓存未命中 +- **WHEN** 新缓存协议使既有 Gradle 缓存失效 +- **THEN** 工作流重新填充缓存并继续执行真实构建 +- **AND** 不将预期缓存未命中报告为应用兼容性回归 + +#### Scenario: CodeQL Android 任务执行 +- **WHEN** CodeQL 分析 Java/Kotlin 源码 +- **THEN** Gradle 构建禁用 build cache 并实际编译目标源码 +- **AND** 缓存恢复成功不得代替 CodeQL 可处理源码的证据 + +#### Scenario: 缓存操作失败 +- **WHEN** Gradle 缓存保存或恢复失败 +- **THEN** 工作流日志或摘要必须暴露该状态 +- **AND** 不得伪造缓存成功或隐藏后续真实构建结果 diff --git a/openspec/specs/release-verification/spec.md b/openspec/specs/release-verification/spec.md index 923e8fe36..ffb860069 100644 --- a/openspec/specs/release-verification/spec.md +++ b/openspec/specs/release-verification/spec.md @@ -129,3 +129,44 @@ #### Scenario: 任一完成条件缺失 - **WHEN** 发布、回验、任务证据、严格验证或分支清理任一条件尚未完成 - **THEN** 系统不得宣称一次性治理已经完成 + +### Requirement: 发布工作流内部 artifact 传递必须保持唯一且完整 +发布工作流 MUST 将唯一普通正式 APK 以唯一名称上传,并在后续作业按该名称下载到确定路径; +下载结果 MUST 恰好包含预期文件名的一个 APK。artifact 服务报告的摘要或下载内容摘要不一致 +时 MUST 失败,不得创建 tag、草稿 Release 或其他发布对象。Action 升级不得改变 APK 名称、 +路径、保留期、包名、签名身份或 Release 唯一资产约束。 + +#### Scenario: 唯一 APK 完整传递 +- **WHEN** 构建作业产生唯一预期普通正式 APK 并完成 artifact 上传 +- **THEN** 后续作业按唯一 artifact 名称下载到预期目录 +- **AND** 下载后只存在文件名、大小和摘要均符合预期的一个 APK + +#### Scenario: artifact 摘要不匹配 +- **WHEN** 下载内容摘要与 artifact 服务提供的预期摘要不一致 +- **THEN** 发布工作流立即失败 +- **AND** 不创建 tag、草稿 Release 或替代资产 + +#### Scenario: artifact 数量或路径漂移 +- **WHEN** 下载目录没有 APK、包含多个 APK 或唯一 APK 不在预期路径 +- **THEN** 发布工作流立即失败 +- **AND** 不通过搜索其他目录或重命名未知文件继续发布 + +### Requirement: 发布 Action 升级验证不得产生发布副作用 +系统 MUST 在不读取正式签名 Secrets、不运行正式构建、不推送 tag、不创建或修改 Release 的 +前提下验证发布工作流的触发、权限、目标 Action 版本、唯一 APK 传递和失败即停止合同。只有 +替代变更合并后另行取得明确发布授权,才可手动触发正式发布工作流。 + +#### Scenario: Pull Request 验证发布工作流 +- **WHEN** Pull Request 修改发布工作流或其合同 +- **THEN** 系统通过静态语法检查和仓库合同验证发布边界 +- **AND** 不读取签名 Secrets、不构建正式 APK 且不创建任何发布对象 + +#### Scenario: 验证需要真实 artifact 闭环 +- **WHEN** 静态合同不足以证明升级后的上传下载兼容性 +- **THEN** 系统使用无签名、无发布权限的测试夹具执行上传下载闭环 +- **AND** 夹具不得被当作 APK、Release 资产或正式发布证据 + +#### Scenario: 工作流升级已经合并 +- **WHEN** 替代变更已合并且维护者尚未明确授权新一轮正式发布 +- **THEN** 系统保持发布工作流未触发 +- **AND** 不以 CI 绿色推断签名、设备或公开 Release 门禁已经通过 diff --git a/openspec/specs/repository-maintenance-governance/spec.md b/openspec/specs/repository-maintenance-governance/spec.md index d98271e6e..3603d6c55 100644 --- a/openspec/specs/repository-maintenance-governance/spec.md +++ b/openspec/specs/repository-maintenance-governance/spec.md @@ -118,3 +118,48 @@ - **WHEN** 自动同步工作流仍引用已撤空仓库或不存在的分支 - **THEN** 系统移除该工作流 - **AND** 不把失败目标机械改成未经批准的新上游 + +### Requirement: GitHub Actions 大版本升级必须逐项审查并替代失效队列 +系统 MUST 从最新 `master` 评估 GitHub Actions 大版本升级,并为目标版本的运行时、runner +下限、凭据、缓存、artifact、权限和事件筛选语义保存可验证的判断。目标组合 MUST 为 +`actions/checkout@v7`、`actions/setup-java@v6`、`gradle/actions/setup-gradle@v6`、 +`actions/upload-artifact@v7`、`actions/download-artifact@v8`、`actions/stale@v11`、 +`pnpm/action-setup@v6` 和 `actions/setup-node@v7`;CodeQL、JDK、Node.js、pnpm、Gradle、 +AGP 和应用依赖不得借此变更升级。落后当前 `master` 且无法通过现行仓库契约的机器人 Pull +Request MUST NOT 直接合并,只能在替代 Pull Request 完成评审与全部必需检查后按可追踪的 +替代关系关闭。 + +#### Scenario: 候选分支落后且契约失败 +- **WHEN** GitHub Actions 更新 Pull Request 已落后当前 `master`,并因仍要求旧版本的精确 + 仓库契约而失败 +- **THEN** 系统保留该失败作为真实兼容性 RED,并从最新 `master` 建立替代变更 +- **AND** 不通过删除、跳过或模糊契约断言使原 Pull Request 可合并 + +#### Scenario: 任一大版本缺少兼容证据 +- **WHEN** 目标 Action 的运行时、runner 下限、权限、缓存、凭据或 artifact 行为尚未完成审查 +- **THEN** 系统不得批准或合并该升级 +- **AND** 已完成审查的其他 Action 不得被用于推断该目标兼容 + +#### Scenario: 替代 Pull Request 通过完整门禁 +- **WHEN** 从最新 `master` 创建的替代 Pull Request 已通过人工评审和全部必需检查 +- **THEN** 系统可以关闭被替代的机器人 Pull Request,并在关闭记录中关联替代 Pull Request +- **AND** 该动作不得被解释为允许普通依赖自动合并 + +### Requirement: Action 升级必须保持 stale 的问题范围和最小权限 +升级后的 stale 工作流 MUST 继续只处理带 `needs-info` 标签且超过 30 天未补充资料的 issue, +MUST 禁止对 Pull Request 执行 stale 或 close,并 MUST 继续豁免崩溃、数据损坏和安全 issue。 +工作流权限 MUST 限定为读取仓库内容和写入 issue,不得新增 Pull Request、Release、Actions +或其他写权限。 + +#### Scenario: Pull Request 长期无活动 +- **WHEN** 任意 Pull Request 超过 stale 时间阈值且没有新活动 +- **THEN** stale 工作流不得标记或关闭该 Pull Request + +#### Scenario: 严重 issue 带有豁免标签 +- **WHEN** issue 带有 `crash`、`data-loss` 或 `security` 标签 +- **THEN** stale 工作流不得仅因时间流逝标记或关闭该 issue + +#### Scenario: needs-info issue 长期未补充 +- **WHEN** issue 带有 `needs-info` 标签、没有豁免标签且超过 30 天未补充资料 +- **THEN** stale 工作流可以按既有消息和恢复说明关闭该 issue +- **AND** 执行期间只使用 issue 写权限和内容读权限