diff --git a/.github/workflows/zhenxun-preview.yml b/.github/workflows/zhenxun-preview.yml index 3648ae4..ce0e378 100644 --- a/.github/workflows/zhenxun-preview.yml +++ b/.github/workflows/zhenxun-preview.yml @@ -6,13 +6,8 @@ on: pull_request: workflow_dispatch: inputs: - version: - description: Preview version - default: 0.1.0-preview.1 - required: true - type: string publish: - description: Sync fixed farm-preview branch in zhenxun repository + description: Sync fixed main branch in zhenxun repository default: false type: boolean @@ -54,8 +49,6 @@ jobs: build: needs: test runs-on: ubuntu-latest - env: - PREVIEW_VERSION: ${{ inputs.version || '0.1.0-preview.1' }} steps: - uses: actions/checkout@v7 with: @@ -66,7 +59,7 @@ jobs: - name: Build selected target only run: >- python scripts/build_zhenxun.py - --output dist/zhenxun-preview --version "$PREVIEW_VERSION" + --output dist/zhenxun-preview - name: Copy standalone helper scripts run: | cp scripts/install_zhenxun.py dist/zhenxun-preview/ diff --git a/pyproject.toml b/pyproject.toml index bcf0aaa..8db871f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "farm-core" -version = "0.1.0" +dynamic = ["version"] description = "Framework-independent farm foundation" readme = "README.md" requires-python = ">=3.10" @@ -17,6 +17,9 @@ render = ["Pillow>=10.4,<13"] [tool.setuptools.packages.find] where = ["src"] +[tool.setuptools.dynamic] +version = {file = "version.txt"} + [tool.setuptools.package-data] farm_core = ["data/*.json"] farm_render = ["data/*.json", "data/*.zip", "data/*.md"] diff --git a/release/zhenxun/verify.yml b/release/zhenxun/verify.yml index c6ce026..fc902f2 100644 --- a/release/zhenxun/verify.yml +++ b/release/zhenxun/verify.yml @@ -1,7 +1,7 @@ name: Verify generated farm preview on: push: - branches: [farm-preview] + branches: [main] workflow_dispatch: permissions: contents: read diff --git a/scripts/build_zhenxun.py b/scripts/build_zhenxun.py index a057e77..7c60c9b 100644 --- a/scripts/build_zhenxun.py +++ b/scripts/build_zhenxun.py @@ -24,10 +24,16 @@ def source_identity(root): } -def build(root, output, version): +def read_version(root): + version = (Path(root) / "version.txt").read_text(encoding="utf-8").strip() if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z0-9.-]+)?", version): - raise ValueError("Invalid preview version") + raise ValueError("Invalid version in root version.txt file") + return version + + +def build(root, output): root, output = Path(root).resolve(), Path(output).resolve() + version = read_version(root) if output.exists(): raise FileExistsError("Use a new output directory") if not output.is_relative_to(root / "dist") and output.is_relative_to(root): @@ -63,6 +69,7 @@ def build(root, output, version): for name, source in { "LICENSE": root / "LICENSE", "README.md": root / "docs/11-zhenxun-preview.md", + "version.txt": root / "version.txt", "images/farm-preview.png": root / "images/farm-preview.png", "verify_package.py": root / "scripts/verify_zhenxun.py", ".github/workflows/verify.yml": root / "release/zhenxun/verify.yml", @@ -107,10 +114,9 @@ def build(root, output, version): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True) - parser.add_argument("--version", default="0.1.0-preview.1") args = parser.parse_args() root = Path(__file__).resolve().parents[1] - print(build(root, args.output, args.version)) + print(build(root, args.output)) if __name__ == "__main__": diff --git a/scripts/check_workflows.py b/scripts/check_workflows.py index bbb3fe2..6fa3a15 100644 --- a/scripts/check_workflows.py +++ b/scripts/check_workflows.py @@ -25,6 +25,8 @@ def main(): if set(trigger) != {"push", "pull_request", "workflow_dispatch"}: raise ValueError("Unsafe or missing workflow trigger") inputs = trigger["workflow_dispatch"]["inputs"] + if set(inputs) != {"publish"}: + raise ValueError("Version must come from the root version.txt file") if inputs["publish"]["default"] != "false": raise ValueError("Publish must be opt-in") sync = source["jobs"]["sync-preview"] @@ -35,7 +37,7 @@ def main(): raise ValueError("Publish boundary differs") if generated["permissions"] != {"contents":"read"}: raise ValueError("Generated workflow permissions differ") - if generated["on"]["push"]["branches"] != ["farm-preview"]: + if generated["on"]["push"]["branches"] != ["main"]: raise ValueError("Generated branch differs") print("Workflow YAML and publication guards checked") diff --git a/scripts/publish_zhenxun.py b/scripts/publish_zhenxun.py index 6e6b9db..0a51b2a 100644 --- a/scripts/publish_zhenxun.py +++ b/scripts/publish_zhenxun.py @@ -55,8 +55,6 @@ def publish(package, confirmed=False, request=None): repository = call("GET", "", None) if not repository: raise ValueError(f"Target repository is not accessible: {REPOSITORY}") - if repository.get("default_branch") == BRANCH: - raise ValueError("Preview must not be the repository default branch") ref = call("GET", f"git/ref/heads/{BRANCH}", None) parent = ref["object"]["sha"] if ref else None if parent: diff --git a/tests/test_zhenxun_package.py b/tests/test_zhenxun_package.py index 63a6b3e..ce4060a 100644 --- a/tests/test_zhenxun_package.py +++ b/tests/test_zhenxun_package.py @@ -60,14 +60,15 @@ class ZhenxunPackageTests(unittest.TestCase): self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) self.output = self.root / "bundle" - self.bundle = build(ROOT, self.output, "0.1.0-preview.1") + self.bundle = build(ROOT, self.output) def test_reproducible_private_complete_package(self): - other = build(ROOT, self.root / "second", "0.1.0-preview.1") + other = build(ROOT, self.root / "second") self.assertEqual(self.bundle.read_bytes(), other.read_bytes()) manifest = verify_package(self.bundle) self.assertEqual(manifest["target"], "zhenxun") - self.assertEqual(manifest["version"], "0.1.0-preview.1") + self.assertEqual(manifest["version"], + (ROOT / "version.txt").read_text().strip()) self.assertIn("dirty", manifest["source"]) with ZipFile(self.bundle) as archive: paths = archive.namelist() @@ -80,7 +81,7 @@ class ZhenxunPackageTests(unittest.TestCase): self.assertIn("from ..farm_core.domain", source) self.assertNotIn("from farm_core.domain", source) with self.assertRaises(FileExistsError): - build(ROOT, self.output, "0.1.0-preview.1") + build(ROOT, self.output) def test_install_and_existing_target_are_safe(self): plugins = self.root / "plugins" @@ -131,11 +132,29 @@ class ZhenxunPackageTests(unittest.TestCase): from publish_zhenxun import publish report = publish(self.bundle) self.assertFalse(report["published"]) - self.assertEqual(report["branch"], "farm-preview") + self.assertEqual(report["branch"], "main") self.assertEqual(report["repository"], "Shu-Ying/zhenxun_plugin_farm") self.assertNotIn("token", json.dumps(report)) - with self.assertRaises(ValueError): - build(ROOT, self.root / "bad", "bad; echo secret") + from build_zhenxun import read_version + for value in ("bad; echo secret", "", "1.0.0\n2.0.0"): + with self.subTest(value=value): + (self.root / "version.txt").write_text(value) + with self.assertRaises(ValueError): + read_version(self.root) + + def test_version_file_controls_package_version(self): + from build_zhenxun import read_version + (self.root / "version.txt").write_text("1.2.3-preview.4\n") + self.assertEqual(read_version(self.root), "1.2.3-preview.4") + with patch("build_zhenxun.read_version", + return_value="1.2.3-preview.4"): + bundle = build(ROOT, self.root / "versioned") + self.assertTrue(bundle.name.endswith("-1.2.3-preview.4.zip")) + self.assertEqual(verify_package(bundle)["version"], "1.2.3-preview.4") + with ZipFile(bundle) as archive: + self.assertEqual(archive.read( + "zhenxun_plugin_farm_preview/_version.py", + ), b'__version__ = "1.2.3-preview.4"\n') def clean_package(self): from verify_zhenxun import read_package, MANIFEST, ROOT_NAME @@ -180,7 +199,7 @@ class ZhenxunPackageTests(unittest.TestCase): self.assertTrue(publish( package, True, request, )["published"]) - self.assertIn(("PATCH", "git/refs/heads/farm-preview", { + self.assertIn(("PATCH", "git/refs/heads/main", { "sha":"created", "force":False, }), calls) @@ -200,7 +219,7 @@ class ZhenxunPackageTests(unittest.TestCase): with patch.dict("os.environ", {"FARM_RELEASE_TOKEN": "unit-test"}): self.assertTrue(publish(package, True, request)["published"]) self.assertIn(("POST", "git/refs", { - "ref": "refs/heads/farm-preview", "sha": "created", + "ref": "refs/heads/main", "sha": "created", }), calls) self.assertFalse(any(c[0] == "PATCH" for c in calls)) @@ -211,15 +230,20 @@ class ZhenxunPackageTests(unittest.TestCase): with self.assertRaisesRegex(ValueError, "not accessible"): publish(package, True, lambda *args: None) - def test_symlink_duplicate_and_default_branch_rejected(self): + def test_symlink_duplicate_and_unmanaged_branch_rejected(self): from publish_zhenxun import publish from zipfile import ZipInfo package, _ = self.clean_package() with patch.dict("os.environ", {"FARM_RELEASE_TOKEN":"unit-test"}): - with self.assertRaises(ValueError): - publish(package, True, lambda *args: { - "default_branch":"farm-preview", - }) + def unmanaged(method, path, data): + if path == "": + return {"default_branch": "main"} + if path.startswith("git/ref/"): + return {"object": {"sha": "existing"}} + return None + + with self.assertRaisesRegex(ValueError, "not generated by Farm"): + publish(package, True, unmanaged) for kind in ("duplicate", "link"): bad = self.root / (kind + ".zip") bad.write_bytes(self.bundle.read_bytes()) diff --git a/tests/test_zhenxun_runtime.py b/tests/test_zhenxun_runtime.py index a3add2a..bb5da73 100644 --- a/tests/test_zhenxun_runtime.py +++ b/tests/test_zhenxun_runtime.py @@ -20,7 +20,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): self.temporary = tempfile.TemporaryDirectory() self.root = Path(self.temporary.name) - build(ROOT, self.root / "built", "0.1.0-preview.1") + build(ROOT, self.root / "built") self.package = "farm_fixture" module = ModuleType(self.package) module.__path__ = [str(self.root / "built/repository")] diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..1d5e9e0 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +1.5.3 \ No newline at end of file