From 03117b7eeacb3c3307fe970bcd4088931c321d56 Mon Sep 17 00:00:00 2001 From: miiyakii Date: Thu, 13 Aug 2026 21:34:19 +0800 Subject: [PATCH] release: prepare 0.3.0rc3 repository candidate --- CHANGELOG.md | 9 +++++++ README.md | 2 +- README.zh-CN.md | 2 +- docs/en/releases/0.3.0rc3.md | 22 +++++++++++++++++ install.sh | 17 +++++++++++--- plugins/benchwork/.codex-plugin/plugin.json | 2 +- plugins/benchwork/benchwork-plugin-api.json | 2 +- pyproject.toml | 2 +- src/benchwork/__init__.py | 2 +- src/benchwork/install/compatibility.json | 4 ++-- tests/installer/test_installation.py | 7 +++--- tests/installer/test_shell.py | 26 ++++++++++++++------- tests/plugin/test_plugin_package.py | 2 +- tests/test_distribution.py | 2 +- 14 files changed, 76 insertions(+), 25 deletions(-) create mode 100644 docs/en/releases/0.3.0rc3.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc558b..aa0ee38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes are recorded here. Benchwork remains a prerelease and uses versioned schemas plus explicit migration notes for compatibility. +## 0.3.0rc3 — Repository release verification + +- Fixed the release installer to retain the canonical wheel filename required + by `uv` and `pipx` during install, repair, and rollback. +- Split repository release verification from PyPI publication: GitHub Pages and + immutable-installer smoke tests now run without a package-index publish. + +See [release notes](docs/en/releases/0.3.0rc3.md). + ## 0.3.0rc2 — The Invitation - Fixed `benchwork_open_task` minting every Task Capsule for the `codex` Host. diff --git a/README.md b/README.md index 424b77a..08b9231 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ tools may propose work, but only the deterministic Athanor kernel can accept a canonical transition. Accepted events are written to a local, append-only Chronicle with chained SHA-256 Sigils and receipts. -> Project status: `0.3.0rc2` release candidate. Phase 2 freezes the Codex +> Project status: `0.3.0rc3` release candidate. Phase 2 freezes the Codex > Plugin-first Instrumentarium while Phase 3 adds a bounded contract-only, > fail-closed local slice; canonical authority and RFC acceptance remain > external prerequisites. diff --git a/README.zh-CN.md b/README.zh-CN.md index 005f23c..1bcbdba 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ Benchwork 将科研活动转化为明确、可审查的状态。智能体和工 提案,但只有确定性的 Athanor 内核能够接受规范状态迁移。获准事件写入本地 追加式 Chronicle,并通过链式 SHA-256 Sigil 与 Receipt 留下记录。 -> 项目状态:`0.3.0rc2` 候选版本。Phase 2 已冻结 Codex Plugin-first +> 项目状态:`0.3.0rc3` 候选版本。Phase 2 已冻结 Codex Plugin-first > Instrumentarium;Phase 3 增加了范围受限、默认 fail-closed 的本地合同纵切, > 规范权威与 RFC 接受仍是外部前置条件。 diff --git a/docs/en/releases/0.3.0rc3.md b/docs/en/releases/0.3.0rc3.md new file mode 100644 index 0000000..9bf8506 --- /dev/null +++ b/docs/en/releases/0.3.0rc3.md @@ -0,0 +1,22 @@ +--- +language: en +canonical: true +--- + +# Benchwork 0.3.0rc3 — Repository release verification + +`0.3.0rc3` is a GitHub-repository-only release candidate. PyPI publication is +deliberately disabled for this release path. + +## Fixed + +- The installer now keeps the canonical distribution wheel filename when using + `uv` or `pipx`, including repair and rollback downloads. +- The release workflow can build, publish the GitHub Pages installer, and run + its immutable-install smoke test without publishing a Python package index. + +## Scope + +This candidate retains the Phase 3 contract-only boundary described in the +[0.3.0rc2 release notes](0.3.0rc2.md). It does not add external acceptance, +Chronicle, Storage, Athanor, or package-index authority. diff --git a/install.sh b/install.sh index a2e02d8..64651b8 100755 --- a/install.sh +++ b/install.sh @@ -485,6 +485,16 @@ download() { fail 4 "download exceeded ${limit} bytes: ${url}" } +wheel_filename() { + wheel_url_value=$1 + wheel_filename_value=${wheel_url_value%%\?*} + wheel_filename_value=${wheel_filename_value##*/} + case "${wheel_filename_value}" in + benchwork_arcana-*.whl) printf '%s\n' "${wheel_filename_value}" ;; + *) fail 4 "release manifest wheel URL has an unsupported filename" ;; + esac +} + sha256() { if [ "${sha_command}" = sha256sum ]; then sha256sum "$1" | awk '{print $1}' @@ -596,7 +606,7 @@ if [ "${uninstall}" -eq 1 ] || { [ "${repair}" -eq 1 ] && [ "${version_set}" -eq recovery_wheel_url=$(json_get "${recovery_manifest}" package.wheel.url) recovery_wheel_sha=$(json_get "${recovery_manifest}" package.wheel.sha256) recovery_wheel_size=$(json_get "${recovery_manifest}" package.wheel.size) - recovery_wheel=${tmp_root}/recovery.whl + recovery_wheel=${tmp_root}/$(wheel_filename "${recovery_wheel_url}") download "${recovery_wheel_url}" "${recovery_wheel}" "${recovery_wheel_size}" verify_sha "${recovery_wheel}" "${recovery_wheel_sha}" PATH="${recovery_bin_dir}:${PATH}" @@ -725,6 +735,7 @@ expected_plugin_version=$(printf '%s' "${version}" | fail 4 "plugin version does not match the package version" [ "${python_requirement}" = ">=3.11" ] || fail 4 "unsupported Python requirement" [ "${wheel_size}" -le "${MAX_WHEEL_SIZE}" ] || fail 4 "wheel exceeds size limit" +wheel_filename "${wheel_url}" >/dev/null case "${manifest_channel}" in rc) case "${version}" in *rc*) ;; *) fail 4 "RC manifest has a non-RC version" ;; esac ;; nightly) case "${version}" in *dev*) ;; *) fail 4 "nightly manifest has a non-nightly version" ;; esac ;; @@ -986,7 +997,7 @@ fi printf '%s\n' "$$" >"${lock_dir}/pid" backend_bootstrapped=0 -wheel_path=${tmp_root}/benchwork.whl +wheel_path=${tmp_root}/$(wheel_filename "${wheel_url}") if [ "${reuse_cli}" -eq 0 ]; then download "${wheel_url}" "${wheel_path}" "${wheel_size}" verify_sha "${wheel_path}" "${wheel_sha}" @@ -1021,7 +1032,7 @@ rollback_previous_installation() { old_wheel_url=$(json_get "${old_manifest}" package.wheel.url) old_wheel_sha=$(json_get "${old_manifest}" package.wheel.sha256) old_wheel_size=$(json_get "${old_manifest}" package.wheel.size) - old_wheel=${tmp_root}/rollback.whl + old_wheel=${tmp_root}/$(wheel_filename "${old_wheel_url}") download "${old_wheel_url}" "${old_wheel}" "${old_wheel_size}" verify_sha "${old_wheel}" "${old_wheel_sha}" if [ "${previous_backend}" = uv ] && command -v uv >/dev/null 2>&1; then diff --git a/plugins/benchwork/.codex-plugin/plugin.json b/plugins/benchwork/.codex-plugin/plugin.json index 6ea4de0..3a76043 100644 --- a/plugins/benchwork/.codex-plugin/plugin.json +++ b/plugins/benchwork/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "benchwork", - "version": "0.3.0-rc.2", + "version": "0.3.0-rc.3", "description": "Auditable research workflows and scientific control-plane tools for Codex", "author": { "name": "Endofthestars", diff --git a/plugins/benchwork/benchwork-plugin-api.json b/plugins/benchwork/benchwork-plugin-api.json index 30c5926..bd17433 100644 --- a/plugins/benchwork/benchwork-plugin-api.json +++ b/plugins/benchwork/benchwork-plugin-api.json @@ -1,7 +1,7 @@ { "schema_version": "benchwork-plugin-api/1.0", "plugin": "benchwork", - "plugin_version": "0.3.0-rc.2", + "plugin_version": "0.3.0-rc.3", "benchwork_api": "0.3", "minimum_runtime": "0.3.0rc2", "skills_api": "1", diff --git a/pyproject.toml b/pyproject.toml index 42cfc4a..b083410 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "benchwork-arcana" -version = "0.3.0rc2" +version = "0.3.0rc3" description = "An auditable workbench for agent-assisted computational research." readme = "README.md" requires-python = ">=3.11" diff --git a/src/benchwork/__init__.py b/src/benchwork/__init__.py index f3aadfa..a7fce6f 100644 --- a/src/benchwork/__init__.py +++ b/src/benchwork/__init__.py @@ -1,3 +1,3 @@ """Benchwork's deterministic Athanor foundation.""" -__version__ = "0.3.0rc2" +__version__ = "0.3.0rc3" diff --git a/src/benchwork/install/compatibility.json b/src/benchwork/install/compatibility.json index 5e61739..27dead3 100644 --- a/src/benchwork/install/compatibility.json +++ b/src/benchwork/install/compatibility.json @@ -1,8 +1,8 @@ { "schema_version": "benchwork-install-compatibility/1.0", "package": "benchwork-arcana", - "version": "0.3.0rc2", - "plugin_version": "0.3.0-rc.2", + "version": "0.3.0rc3", + "plugin_version": "0.3.0-rc.3", "plugin_api": "benchwork-plugin-api/1.0", "mcp_tool_registry": "mcp-tool-registry/1.0" } diff --git a/tests/installer/test_installation.py b/tests/installer/test_installation.py index 89b134c..a61d138 100644 --- a/tests/installer/test_installation.py +++ b/tests/installer/test_installation.py @@ -29,8 +29,9 @@ ROOT = Path(__file__).parents[2] -VERSION = "0.3.0rc2" -PLUGIN_VERSION = "0.3.0-rc.2" +VERSION = "0.3.0rc3" +WHEEL_NAME = f"benchwork_arcana-{VERSION}-py3-none-any.whl" +PLUGIN_VERSION = "0.3.0-rc.3" def artifact(name: str, *, size: int = 1) -> dict: @@ -51,7 +52,7 @@ def manifest() -> dict: "package": { "name": "benchwork-arcana", "requirement": f"benchwork-arcana=={VERSION}", - "wheel": artifact("benchwork.whl"), + "wheel": artifact(WHEEL_NAME), "sdist": artifact("benchwork.tar.gz"), }, "plugin": { diff --git a/tests/installer/test_shell.py b/tests/installer/test_shell.py index becb3e2..64efc15 100644 --- a/tests/installer/test_shell.py +++ b/tests/installer/test_shell.py @@ -11,6 +11,7 @@ ROOT = Path(__file__).parents[2] +VERSION = "0.3.0rc3" def run_exact_dry(document: str) -> subprocess.CompletedProcess[str]: @@ -49,7 +50,7 @@ def run_exact_dry(document: str) -> subprocess.CompletedProcess[str]: "--dry-run", "--json", "--version", - "0.3.0rc2", + VERSION, ], cwd=work, env=environment, @@ -119,7 +120,7 @@ def test_exact_dry_run_is_non_mutating(self) -> None: "--dry-run", "--json", "--version", - "0.3.0rc2", + VERSION, ], cwd=work, env=environment, @@ -145,6 +146,12 @@ def test_manifest_mismatches_and_duplicate_keys_fail_before_mutation(self) -> No self.assertEqual(result.returncode, 4) self.assertIn("plugin version does not match", result.stderr) + invalid = manifest() + invalid["package"]["wheel"]["url"] = "https://example.test/benchwork.whl" + result = run_exact_dry(json.dumps(invalid)) + self.assertEqual(result.returncode, 4) + self.assertIn("wheel URL has an unsupported filename", result.stderr) + result = run_exact_dry( json.dumps(manifest()).replace( '"schema_version": "benchwork-release-manifest/1.0"', @@ -169,8 +176,8 @@ def test_rc_channel_verifies_its_manifest_descriptor(self) -> None: channel = { "schema_version": "benchwork-release-channel/1.0", "channel": "rc", - "version": "0.3.0rc2", - "manifest_url": "https://example.test/releases/0.3.0rc2/release-manifest.json", + "version": VERSION, + "manifest_url": f"https://example.test/releases/{VERSION}/release-manifest.json", "manifest_sha256": hashlib.sha256(manifest_blob).hexdigest(), "manifest_size": len(manifest_blob), } @@ -210,7 +217,7 @@ def test_rc_channel_verifies_its_manifest_descriptor(self) -> None: ) self.assertEqual(result.returncode, 0, result.stderr) plan = json.loads(result.stdout) - self.assertEqual(plan["version"], "0.3.0rc2") + self.assertEqual(plan["version"], VERSION) self.assertEqual(plan["channel"], "rc") def test_full_uv_lifecycle_is_idempotent_and_preserves_research_state(self) -> None: @@ -240,7 +247,8 @@ def test_full_uv_lifecycle_is_idempotent_and_preserves_research_state(self) -> N sentinel = research / "existing.txt" sentinel.write_text("preserve me\n", encoding="utf-8") - wheel = artifacts / "benchwork.whl" + wheel_name = "benchwork_arcana-0.3.0rc3-py3-none-any.whl" + wheel = artifacts / wheel_name wheel.write_bytes(b"fake exact wheel") plugin = artifacts / "plugin.tar.gz" subprocess.run( @@ -263,13 +271,13 @@ def test_full_uv_lifecycle_is_idempotent_and_preserves_research_state(self) -> N fake_curl = fake_bin / "curl" fake_curl.write_text( - """#!/usr/bin/python3 + f"""#!/usr/bin/python3 import os, pathlib, shutil, sys args = sys.argv[1:] output = pathlib.Path(args[args.index("--output") + 1]) url = args[-1] name = "release-manifest.json" if url.endswith("release-manifest.json") else pathlib.Path(url).name -mapping = {"benchwork.whl": "benchwork.whl", "plugin.tar.gz": "plugin.tar.gz"} +mapping = {{{wheel_name!r}: {wheel_name!r}, "plugin.tar.gz": "plugin.tar.gz"}} source = pathlib.Path(os.environ["FAKE_ARTIFACTS"]) / mapping.get(name, name) shutil.copyfile(source, output) """, @@ -322,7 +330,7 @@ def test_full_uv_lifecycle_is_idempotent_and_preserves_research_state(self) -> N "sh", str(ROOT / "install.sh"), "--version", - "0.3.0rc2", + VERSION, "--backend", "uv", "--install-dir", diff --git a/tests/plugin/test_plugin_package.py b/tests/plugin/test_plugin_package.py index a9cf09d..498e566 100644 --- a/tests/plugin/test_plugin_package.py +++ b/tests/plugin/test_plugin_package.py @@ -23,7 +23,7 @@ def test_manifest_marketplace_and_components_agree(self) -> None: entry = next(item for item in marketplace["plugins"] if item["name"] == "benchwork") self.assertEqual(marketplace["name"], "benchwork-local") self.assertEqual(manifest["name"], "benchwork") - self.assertEqual(manifest["version"], "0.3.0-rc.2") + self.assertEqual(manifest["version"], "0.3.0-rc.3") self.assertEqual(entry["source"]["path"], "./plugins/benchwork") self.assertEqual(entry["policy"]["installation"], "AVAILABLE") self.assertEqual(entry["policy"]["authentication"], "ON_INSTALL") diff --git a/tests/test_distribution.py b/tests/test_distribution.py index 5cce338..83128a0 100644 --- a/tests/test_distribution.py +++ b/tests/test_distribution.py @@ -11,7 +11,7 @@ class DistributionTest(unittest.TestCase): def test_package_and_project_versions_match(self) -> None: project = loads((Path(__file__).parents[1] / "pyproject.toml").read_text(encoding="utf-8")) self.assertEqual(benchwork.__version__, project["project"]["version"]) - self.assertEqual(benchwork.__version__, "0.3.0rc2") + self.assertEqual(benchwork.__version__, "0.3.0rc3") def test_init_creates_both_versioned_registries(self) -> None: with tempfile.TemporaryDirectory() as directory: