Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,16 @@ jobs:
ruby Scripts/verify-source-release.rb \
--version 0.2.0 \
--ref HEAD \
--allow-source-blocked
--allow-source-blocked \
--report "$RUNNER_TEMP/pocketroot-v0.2.0-source-candidate.json"

- name: Upload source candidate audit report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pocketroot-source-candidate-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/pocketroot-v0.2.0-source-candidate.json
if-no-files-found: error
retention-days: 14

- name: Report Apple toolchain
run: |
Expand Down
18 changes: 17 additions & 1 deletion .github/workflows/source-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ jobs:
--tooling-root trusted \
--version "$version" \
--ref "$RELEASE_TAG" \
--require-tag "$RELEASE_TAG"
--require-tag "$RELEASE_TAG" \
--report "$RUNNER_TEMP/pocketroot-$version-source-release.json"

- name: Resolve exact Swift Package version externally
env:
Expand All @@ -74,3 +75,18 @@ jobs:
commit="$(git -C candidate rev-parse "$RELEASE_TAG^{commit}")"
./trusted/Scripts/verify-immutable-upstream-releases.sh
./trusted/Scripts/verify-swiftpm-release-tag.sh "$version" "$commit"

- name: Require source release verification report
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
version="${RELEASE_TAG#v}"
test -f "$RUNNER_TEMP/pocketroot-$version-source-release.json"

- name: Publish source release verification report artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pocketroot-source-release-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/pocketroot-0.2.0-source-release.json
if-no-files-found: error
retention-days: 30
6 changes: 6 additions & 0 deletions CHANGELOG.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit

### Changed

- Tightened the untagged `v0.2.0` source-candidate audit:
`--allow-source-blocked` accepts only final source-release authorization as
unsatisfied, and fails on any pinned-gate-set, NOTICE, license, public API
status, or source-boundary drift. CI and the trusted tag workflow upload only
a JSON verification report with the commit, archive SHA-256, file counts,
and exact blockers—not the temporary source tar, RootFS, App, or binaries.
- Split the minimum-Xcode 16 gate into an independent native-runtime job and a
five-way `fail-fast: false` UI matrix for the public-SHA external consumer,
Quick Start iPhone/iPad, and Host App iPhone/iPad. Every isolated runner
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循

### Changed

- 收紧 `v0.2.0` 未打 tag 源码候选审计:`--allow-source-blocked` 只接受最终源码发布
授权这一项尚未满足,固定门禁集合、NOTICE、许可证、公开 API 状态或源码边界漂移
都会失败;CI 和受信 tag 工作流只上传带 commit、archive SHA-256、文件统计和精确
阻塞项的 JSON 验证报告,不上传临时源码 tar、RootFS、App 或二进制制品。
- 将最低 Xcode 16 门禁拆为独立 native runtime job 和五路 `fail-fast: false` UI
matrix,分别并行验证公开 SHA 外部消费者、Quick Start iPhone/iPad 与 Host App
iPhone/iPad。全部 job 复用仓库内 composite action,但各自在隔离 runner 上重新
Expand Down
4 changes: 4 additions & 0 deletions Docs/Releases/0.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ archive、IPA、XCFramework 镜像或二进制 SDK,也不创建 tag 或 GitHub
RootFS 继续由调用方合法取得、审核并作为本地输入提供;Runtime/App/二进制分发和
RootFS 再分发保持 fail-closed。

未打 tag 的候选审计只容许 `source-release-authorized` 这一项尚未满足;源码边界、
公开 API 状态、LICENSE、贡献者政策和 NOTICE 必须全部通过。CI 生成带 commit、
archive SHA-256、文件统计和精确阻塞项的 JSON 报告,但不保留或上传源码 tar。

## 尚未关闭

- v0.2.0 源码发布的项目所有者授权、发布日期、annotated tag 和外部精确版本解析;
Expand Down
13 changes: 9 additions & 4 deletions Docs/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ PocketRoot 把验证分成宿主逻辑、真实 RootFS、iOS 构建、完整原
| Host App UI smoke | `./Scripts/run-host-app-ui-smoke.sh` | Apple Silicon + iOS 18 Simulator + archive | iPhone/iPad Simulator 上的公开宿主 Boot、SwiftTerm PTY、生命周期、Workspace 会话持续性、Files 增删改/预览、系统 document picker 导入、share sheet 保存与再次导入 round-trip,以及有序 shutdown | 真机系统文件交互、真机键盘、iPad 真机或发行可用 |
| Host App 真机 UI smoke | `./Scripts/run-host-app-device-ui-smoke.sh` | Xcode 可解析的 development-signed iPhone/iPad + archive | 同一 Host App 生命周期 UI 测试的真机执行、签名与 development entitlement | iPad、真实压力或发行可用 |
| 物理设备原生 smoke | `./Scripts/run-runtime-device-smoke.sh` | 签名 iOS 18+ iPhone/iPad + archive | 同一 17 项检查、可选进程暂停/恢复、UIKit 前后台、强制重启持久化、受限存储故障、有界内存警告恢复或持久 PTY 稳定性,development entitlement 与 shutdown 返回 | 真实 storage/memory pressure、断电、jetsam、iPad 或发行可用 |
| 源码发布审计 | `ruby Scripts/verify-source-release.rb --version 0.2.0 --allow-source-blocked` | Git commit | 候选版本文档齐全,`git archive` 不含 RootFS、App、IPA、XCFramework 镜像、压缩载荷或原生二进制 | 已授予源码发布授权或 Runtime/App/RootFS 分发授权 |
| 源码发布审计 | `ruby Scripts/verify-source-release.rb --version 0.2.0 --allow-source-blocked` | Git commit | 除最终源码发布授权外的门禁全部满足,候选版本文档齐全,`git archive` 不含 RootFS、App、IPA、XCFramework 镜像、压缩载荷或原生二进制 | 已授予源码发布授权或 Runtime/App/RootFS 分发授权 |
| 文档检查 | `./Scripts/check-docs.sh` | macOS/Linux shell | 中英文成对、中文覆盖和相对链接 | 技术实现正确 |

## 2. 宿主 Swift Package 测试
Expand Down Expand Up @@ -588,14 +588,19 @@ Workspace、系统 document picker 导入、share sheet 保存、guest 删除后
失败,也保留首次 `xcodebuild` 的诊断结果。
这些 Simulator 结果不证明签名真机或发行可用。

当前候选 PR 只允许以 `--allow-source-blocked` 审计未打 tag 的 `v0.2.0` commit;
当前候选 PR 只允许以 `--allow-source-blocked` 审计未打 tag 的 `v0.2.0` commit。
这个模式不是绕过门禁:它要求固定的源码门禁集合和顺序完全一致,并且唯一未满足项
必须是 `source-release-authorized`;NOTICE、许可证、公开 API 状态或源码边界出现任何
回退都会失败。审计报告记录 schema、commit、archive SHA-256、文件统计、精确阻塞项
和授权状态;CI 只保留该 JSON 报告 14 天,不保留或上传临时源码 tar。
`--require-source-ready` 与 tag 工作流保持失败关闭。只有明确授予源码发布授权并再次
评审后,才可推送 `v0.2.0` annotated tag,并从受保护的 `main` 手动调度
`.github/workflows/source-release.yml`。工作流使用 `main` checkout
里的可信校验工具审计独立的 tag checkout,要求 annotated tag 的 commit 位于该
`main` 历史上,重新生成并扫描 `git archive`,随后从仓库外以 `exact: "0.2.0"`
解析公开 Swift Package,并核对解析版本与 peeled commit。该工作流不会创建或上传
RootFS、App、IPA、XCFramework 或二进制 SDK。
解析公开 Swift Package,并核对解析版本与 peeled commit。成功后只保留 JSON 验证
报告 30 天;该工作流不会创建或上传源码 tar、RootFS、App、IPA、XCFramework 或
二进制 SDK。

## 9. 改动与最小验证矩阵

Expand Down
6 changes: 6 additions & 0 deletions Docs/en/Releases/0.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ or binary SDK, and it creates no tag or GitHub Release. Callers must continue to
lawfully obtain and review a local RootFS input. Runtime/App/binary distribution
and RootFS redistribution remain fail-closed.

The untagged candidate audit permits only `source-release-authorized` to remain
unsatisfied. Source boundaries, public API status, LICENSE, contributor policy,
and NOTICE must all pass. CI writes a JSON report containing the commit,
archive SHA-256, file counts, and exact blockers, but neither retains nor
uploads the source tar.

## Still open

- Owner authorization, release date, annotated tag, and external exact-version
Expand Down
17 changes: 12 additions & 5 deletions Docs/en/Testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ PocketRoot separates host logic, real RootFS, iOS build, native final-link, and
| Host App UI smoke | `./Scripts/run-host-app-ui-smoke.sh` | Public-host boot, SwiftTerm PTY, lifecycle, Workspace persistence, Files mutations/previews, system document-picker import, share-sheet save and re-import round trip, and ordered shutdown on iPhone/iPad iOS 18 Simulators | Physical-device system file interaction, physical keyboards, physical iPad, distribution |
| Physical Host App UI smoke | `./Scripts/run-host-app-device-ui-smoke.sh` | The same lifecycle UI test on an Xcode-resolved, development-signed iPhone/iPad, including signature and development entitlements | iPad, real pressure, distribution |
| Physical native smoke | `./Scripts/run-runtime-device-smoke.sh` | Same 17 checks with optional process suspend/resume, UIKit lifecycle, forced-relaunch persistence, bounded storage-failure recovery, bounded memory-warning recovery, or persistent-PTY stability; development entitlements and returning soft shutdown | Real storage/memory pressure, power cut, jetsam, iPad, distribution |
| Source-release audit | `ruby Scripts/verify-source-release.rb --version 0.2.0 --allow-source-blocked` | Complete candidate documents and a `git archive` without RootFS, App, IPA, XCFramework mirror, compressed payload, or native binary content | Granted source-release authorization or Runtime/App/RootFS distribution authorization |
| Source-release audit | `ruby Scripts/verify-source-release.rb --version 0.2.0 --allow-source-blocked` | Every gate except final source-release authorization is satisfied, candidate documents are complete, and `git archive` contains no RootFS, App, IPA, XCFramework mirror, compressed payload, or native binary content | Granted source-release authorization or Runtime/App/RootFS distribution authorization |
| Documentation | `./Scripts/check-docs.sh` | Pairs, Chinese coverage, relative links | Implementation correctness |

## Package tests
Expand Down Expand Up @@ -539,15 +539,22 @@ final logs plus both available `.xcresult` bundles. This Simulator evidence
does not prove signed-device or distribution readiness.

This candidate PR audits only an untagged `v0.2.0` commit with
`--allow-source-blocked`; `--require-source-ready` and the tag workflow remain
fail closed. Only after explicit source-release authorization and another
review may an annotated `v0.2.0` tag be pushed and
`--allow-source-blocked`. That mode does not bypass gates: it requires the
exact pinned source-gate set and order, with `source-release-authorized` as the
only unsatisfied gate. Any NOTICE, license, public-API-status, or source-boundary
regression fails the audit. The JSON report records its schema, commit, archive
SHA-256, file counts, exact blockers, and authorization status. CI retains only
that JSON report for 14 days and neither retains nor uploads the temporary
source tar. `--require-source-ready` and the tag workflow remain fail closed.
Only after explicit source-release authorization and another review may an
annotated `v0.2.0` tag be pushed and
`.github/workflows/source-release.yml` dispatched from protected `main`. The workflow
uses trusted verifier tooling from the `main` checkout against a separate tag
checkout, requires the annotated tag commit to belong to that trusted `main`
history, regenerates and audits the `git archive`, then resolves the public
package externally with `exact: "0.2.0"` and verifies both the resolved version
and peeled commit. It does not create or upload a RootFS, App, IPA,
and peeled commit. On success it retains only the JSON verification report for
30 days. It does not create or upload a source tar, RootFS, App, IPA,
XCFramework, or binary SDK.

## Minimum checks by change
Expand Down
104 changes: 90 additions & 14 deletions Scripts/verify-source-release.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ module PocketRootSourceRelease
class VerificationError < StandardError; end

RELEASE_VERSION = "0.2.0"
SOURCE_AUTHORIZATION_GATE_ID = "source-release-authorized"
EXPECTED_SOURCE_GATE_IDS = %w[
source-boundary-excludes-rootfs
public-api-status-declared
top-level-license-finalized
contributor-policy-approved
release-notice-approved
source-release-authorized
].freeze
MAX_ENTRIES = 20_000
MAX_FILE_BYTES = 64 * 1024 * 1024
MAX_ARCHIVE_CONTENT_BYTES = 256 * 1024 * 1024
Expand Down Expand Up @@ -175,25 +184,75 @@ def verify_source_readiness(
decisions_path = root.join("Compliance/Release/RELEASE-DECISIONS.json")
readiness = load_json(readiness_path, "release readiness")
decisions = load_json(decisions_path, "release decisions")
source_status = readiness.dig("tracks", "sourcePackageRelease", "status")
source_track = readiness.dig("tracks", "sourcePackageRelease")
source_status = source_track&.fetch("status", nil)
unless readiness["releaseVersion"] == version &&
decisions["releaseVersion"] == version &&
%w[blocked ready].include?(source_status)
raise VerificationError,
"source readiness is not bound to release #{version}"
end
if require_ready &&
(
source_status != "ready" ||
!%w[
source-release-authorized
source-and-runtime-distribution-authorized
].include?(decisions["status"])
)
gates = source_track.fetch("gates")
unless gates.is_a?(Array) &&
gates.all? { |gate| gate.is_a?(Hash) } &&
gates.map { |gate| gate["id"] } == EXPECTED_SOURCE_GATE_IDS
raise VerificationError,
"source readiness gate set is not bound to release #{version}"
end
blocked_gate_ids = gates.each_with_object([]) do |gate, blocked|
blocked << gate.fetch("id") unless gate.fetch("satisfied") == true
end
authorized = %w[
source-release-authorized
source-and-runtime-distribution-authorized
].include?(decisions["status"])
if require_ready && (source_status != "ready" || !authorized)
raise VerificationError,
"source authorization is not bound to release #{version}"
end
source_status
if source_status == "ready" && (!blocked_gate_ids.empty? || !authorized)
raise VerificationError,
"ready source track has unresolved gates for release #{version}"
end
if !require_ready && source_status == "blocked" &&
blocked_gate_ids != [SOURCE_AUTHORIZATION_GATE_ID]
raise VerificationError,
"source candidate has unresolved gates beyond explicit release " \
"authorization: #{blocked_gate_ids.join(', ')}"
end

{
"status" => source_status,
"candidateReady" => blocked_gate_ids.empty? ||
blocked_gate_ids == [SOURCE_AUTHORIZATION_GATE_ID],
"blockedGateIds" => blocked_gate_ids,
"authorizationStatus" => decisions.fetch("status")
}
end

def write_report(path, result)
report = Pathname(path).expand_path
report.dirname.mkpath
if report.symlink? || (report.exist? && !report.file?)
raise VerificationError,
"source verification report path is not a regular file: #{report}"
end

temporary = Tempfile.new(
[".#{report.basename}", ".tmp"],
report.dirname.to_s
)
temporary.binmode
temporary.write(JSON.pretty_generate(result))
temporary.write("\n")
temporary.flush
temporary.fsync
temporary.close
File.rename(temporary.path, report)
report
ensure
temporary&.close unless temporary&.closed?
temporary&.unlink
end

def verify_annotated_tag(root, ref, tag)
Expand Down Expand Up @@ -461,7 +520,7 @@ def verify(
prefix,
required_paths: REQUIRED_PATHS + release_paths
)
source_status = nil
source_readiness = nil
Dir.mktmpdir("pocketroot-source-release-snapshot-") do |directory|
release_root = materialize_archive(
archive,
Expand All @@ -473,18 +532,24 @@ def verify(
version,
require_released: require_source_ready
)
source_status = verify_source_readiness(
source_readiness = verify_source_readiness(
release_root,
version,
tooling_root: Pathname(tooling_root).realpath,
require_ready: require_source_ready
)
end
result = result.merge(
result = {
"schemaVersion" => 1
}.merge(result).merge(
"releaseVersion" => version,
"ref" => ref,
"commit" => run_git(root, "rev-parse", "#{ref}^{commit}"),
"sourceTrack" => source_status
"sourceTrack" => source_readiness.fetch("status"),
"sourceCandidateReady" => source_readiness.fetch("candidateReady"),
"sourceBlockedGateIds" => source_readiness.fetch("blockedGateIds"),
"authorizationStatus" =>
source_readiness.fetch("authorizationStatus")
)
result["archivePath"] = archive.to_s if output
result
Expand Down Expand Up @@ -526,6 +591,9 @@ def verify(
cli.on("--output PATH", "Keep the verified tar at PATH") do |value|
options[:output] = value
end
cli.on("--report PATH", "Write the JSON verification report at PATH") do |value|
options[:report] = value
end
end
parser.parse!
raise OptionParser::MissingArgument, "--version" unless options[:version]
Expand All @@ -539,6 +607,14 @@ def verify(
tooling_root: options.fetch(:tooling_root, options[:root]),
require_source_ready: options[:require_source_ready]
)
if options[:report]
report = Pathname(options[:report]).expand_path
if options[:output] && report == Pathname(options[:output]).expand_path
raise PocketRootSourceRelease::VerificationError,
"source archive and JSON report paths must be different"
end
PocketRootSourceRelease.write_report(report, result)
end
puts JSON.pretty_generate(result)
rescue OptionParser::ParseError,
PocketRootSourceRelease::VerificationError => error
Expand Down
Loading