| # Licensed to the Apache Software Foundation (ASF) under one | |
| # or more contributor license agreements. See the NOTICE file | |
| # distributed with this work for additional information | |
| # regarding copyright ownership. The ASF licenses this file | |
| # to you under the Apache License, Version 2.0 (the | |
| # "License"); you may not use this file except in compliance | |
| # with the License. You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, | |
| # software distributed under the License is distributed on an | |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| # KIND, either express or implied. See the License for the | |
| # specific language governing permissions and limitations | |
| # under the License. | |
| import hashlib | |
| import importlib.util | |
| import subprocess | |
| import sys | |
| import tarfile | |
| from argparse import Namespace | |
| from pathlib import Path | |
| from types import SimpleNamespace | |
| import pytest | |
| def _load_release_module(): | |
| module_path = Path(__file__).resolve().parent.parent / "scripts" / "apache_release.py" | |
| spec = importlib.util.spec_from_file_location("apache_release", module_path) | |
| module = importlib.util.module_from_spec(spec) | |
| assert spec.loader is not None | |
| sys.modules[spec.name] = module | |
| spec.loader.exec_module(module) | |
| return module | |
| release = _load_release_module() | |
| def test_build_sdist_uses_artifact_specific_license(monkeypatch, tmp_path): | |
| project_files = { | |
| "LICENSE": "full source license\nwebsite/src/components/ui/\n", | |
| "LICENSE-sdist": "sdist license\nexamples/deep-researcher\n", | |
| "pyproject.toml": '[project]\nname = "apache-burr"\n', | |
| } | |
| output_dir = tmp_path / "artifacts" | |
| def fake_check_git_working_tree(): | |
| return None | |
| def fake_run_command(command, **kwargs): | |
| if command[:3] == ["git", "archive", "HEAD"]: | |
| archive_path = Path(command[command.index("--output") + 1]) | |
| with tarfile.open(archive_path, "w") as archive: | |
| for name, contents in project_files.items(): | |
| source = tmp_path / name | |
| source.write_text(contents, encoding="utf-8") | |
| archive.add(source, arcname=name) | |
| elif command == ["flit", "build", "--format", "sdist"]: | |
| source_tree = Path(kwargs["cwd"]) | |
| assert (source_tree / "LICENSE").read_text(encoding="utf-8") == project_files[ | |
| "LICENSE-sdist" | |
| ] | |
| assert not (source_tree / "LICENSE-sdist").exists() | |
| built_sdist = source_tree / "dist" / "apache_burr-0.42.0.tar.gz" | |
| built_sdist.parent.mkdir() | |
| with tarfile.open(built_sdist, "w:gz") as archive: | |
| archive.add(source_tree / "LICENSE", arcname="apache_burr-0.42.0/LICENSE") | |
| else: | |
| pytest.fail(f"Unexpected command: {command}") | |
| return subprocess.CompletedProcess(command, 0) | |
| monkeypatch.setattr(release, "_check_git_working_tree", fake_check_git_working_tree) | |
| monkeypatch.setattr(release, "_git_source_date_epoch", lambda: 987654321) | |
| monkeypatch.setattr(release, "_run_command", fake_run_command) | |
| artifact = Path(release._build_sdist_from_git("0.42.0", str(output_dir))) | |
| assert artifact == output_dir / "apache-burr-0.42.0-incubating-sdist.tar.gz" | |
| with tarfile.open(artifact, "r:gz") as archive: | |
| license_text = archive.extractfile("apache_burr-0.42.0/LICENSE").read().decode() | |
| assert license_text == project_files["LICENSE-sdist"] | |
| assert all(not name.endswith("/LICENSE-sdist") for name in archive.getnames()) | |
| def test_git_source_date_epoch_ignores_ambient_epoch_and_anchors_repo(monkeypatch, capsys): | |
| monkeypatch.setenv("SOURCE_DATE_EPOCH", "123456789") | |
| calls = [] | |
| def fake_run_command(command, **kwargs): | |
| calls.append((command, kwargs)) | |
| return SimpleNamespace(stdout="987654321\n") | |
| monkeypatch.setattr(release, "_run_command", fake_run_command) | |
| assert release._git_source_date_epoch() == 987654321 | |
| assert calls[0][0] == [ | |
| "git", | |
| "-C", | |
| str(release.PROJECT_ROOT), | |
| "-c", | |
| "log.showSignature=false", | |
| "show", | |
| "-s", | |
| "--format=%ct", | |
| "HEAD", | |
| ] | |
| assert "Ignoring ambient SOURCE_DATE_EPOCH=123456789" in capsys.readouterr().out | |
| def test_wheel_source_date_epoch_uses_explicit_rebuild_epoch(monkeypatch): | |
| monkeypatch.setenv("SOURCE_DATE_EPOCH", "123456789") | |
| assert release._wheel_source_date_epoch() == 123456789 | |
| def test_wheel_source_date_epoch_treats_blank_as_unset(monkeypatch): | |
| monkeypatch.setenv("SOURCE_DATE_EPOCH", " ") | |
| monkeypatch.setattr(release, "_git_source_date_epoch", lambda: 987654321) | |
| assert release._wheel_source_date_epoch() == 987654321 | |
| def test_environment_source_date_epoch_rejects_non_integer(monkeypatch): | |
| monkeypatch.setenv("SOURCE_DATE_EPOCH", "not-an-epoch") | |
| with pytest.raises(SystemExit): | |
| release._environment_source_date_epoch() | |
| def _write_artifact_set(directory: Path, version: str, wheel_name: str = None) -> None: | |
| wheel_name = wheel_name or f"apache_burr-{version}-py3-none-any.whl" | |
| artifact_names = [ | |
| f"apache-burr-{version}-incubating-src.tar.gz", | |
| f"apache-burr-{version}-incubating-sdist.tar.gz", | |
| wheel_name, | |
| ] | |
| for artifact_name in artifact_names: | |
| artifact_path = directory / artifact_name | |
| artifact_path.write_bytes(b"artifact") | |
| artifact_path.with_name(f"{artifact_name}.asc").write_text("sig", encoding="utf-8") | |
| artifact_path.with_name(f"{artifact_name}.sha512").write_text("sha", encoding="utf-8") | |
| def test_parse_rc_label_accepts_supported_formats(): | |
| assert release._parse_rc_label("0.42.0-RC1") == ("0.42.0", "1") | |
| assert release._parse_rc_label("0.42.0-incubating-RC1") == ("0.42.0", "1") | |
| def test_parse_rc_label_rejects_invalid_format(): | |
| with pytest.raises(SystemExit): | |
| release._parse_rc_label("0.42.0") | |
| def test_validate_promotion_artifacts_requires_expected_set(tmp_path): | |
| _write_artifact_set(tmp_path, "0.42.0") | |
| artifacts = release._validate_promotion_artifacts(str(tmp_path), "0.42.0") | |
| assert len(artifacts) == 9 | |
| assert any(path.endswith("apache-burr-0.42.0-incubating-src.tar.gz") for path in artifacts) | |
| assert any(path.endswith("apache-burr-0.42.0-incubating-sdist.tar.gz") for path in artifacts) | |
| assert any(path.endswith("apache_burr-0.42.0-py3-none-any.whl") for path in artifacts) | |
| def test_validate_promotion_artifacts_fails_when_companion_missing(tmp_path): | |
| _write_artifact_set(tmp_path, "0.42.0") | |
| (tmp_path / "apache-burr-0.42.0-incubating-src.tar.gz.asc").unlink() | |
| with pytest.raises(SystemExit): | |
| release._validate_promotion_artifacts(str(tmp_path), "0.42.0") | |
| def test_promotion_target_url_appends_version_subdir(): | |
| assert ( | |
| release._promotion_target_url( | |
| "0.42.0", "https://dist.apache.org/repos/dist/release/incubator/burr" | |
| ) | |
| == "https://dist.apache.org/repos/dist/release/incubator/burr/0.42.0" | |
| ) | |
| def test_twine_upload_command_includes_only_sdist_and_wheel(): | |
| command = release._twine_upload_command( | |
| [ | |
| "apache-burr-0.42.0-incubating-src.tar.gz", | |
| "apache-burr-0.42.0-incubating-src.tar.gz.asc", | |
| "apache-burr-0.42.0-incubating-sdist.tar.gz", | |
| "apache_burr-0.42.0-py3-none-any.whl", | |
| ] | |
| ) | |
| assert command == ( | |
| "twine upload apache-burr-0.42.0-incubating-sdist.tar.gz " | |
| "apache_burr-0.42.0-py3-none-any.whl" | |
| ) | |
| def test_cmd_promote_rejects_already_promoted_release(monkeypatch): | |
| monkeypatch.setattr(release, "_verify_project_root", lambda: None) | |
| monkeypatch.setattr(release, "_svn_target_exists", lambda url: True) | |
| args = Namespace( | |
| rc_label="0.42.0-RC1", | |
| apache_id="hari", | |
| dry_run=False, | |
| dev_svn_root="https://dist.apache.org/repos/dist/dev/incubator/burr", | |
| release_svn_root="https://dist.apache.org/repos/dist/release/incubator/burr", | |
| ) | |
| with pytest.raises(SystemExit): | |
| release.cmd_promote(args) | |
| def test_cmd_promote_dry_run_uses_server_copy_without_committing(monkeypatch, tmp_path): | |
| calls = {"checkout": [], "promote": None} | |
| class _TempDir: | |
| def __enter__(self): | |
| return str(tmp_path) | |
| def __exit__(self, exc_type, exc, tb): | |
| return False | |
| def fake_checkout(url: str, checkout_dir: str): | |
| calls["checkout"].append((url, checkout_dir)) | |
| Path(checkout_dir).mkdir(parents=True, exist_ok=True) | |
| def fake_validate(rc_checkout_dir: str, version: str): | |
| assert version == "0.42.0" | |
| return [ | |
| f"{rc_checkout_dir}/apache-burr-0.42.0-incubating-sdist.tar.gz", | |
| f"{rc_checkout_dir}/apache_burr-0.42.0-py3-none-any.whl", | |
| ] | |
| def fake_promote(source_url, target_url, message, apache_id, dry_run=False): | |
| calls["promote"] = (source_url, target_url, message, apache_id, dry_run) | |
| return True | |
| monkeypatch.setattr(release, "_verify_project_root", lambda: None) | |
| monkeypatch.setattr(release.tempfile, "TemporaryDirectory", lambda prefix=None: _TempDir()) | |
| monkeypatch.setattr(release, "_svn_target_exists", lambda url: False) | |
| monkeypatch.setattr(release, "_svn_checkout", fake_checkout) | |
| monkeypatch.setattr(release, "_validate_promotion_artifacts", fake_validate) | |
| monkeypatch.setattr(release, "_promote_with_server_copy", fake_promote) | |
| args = Namespace( | |
| rc_label="0.42.0-RC1", | |
| apache_id="hari", | |
| dry_run=True, | |
| dev_svn_root="https://dist.apache.org/repos/dist/dev/incubator/burr", | |
| release_svn_root="https://dist.apache.org/repos/dist/release/incubator/burr", | |
| ) | |
| assert release.cmd_promote(args) is True | |
| # only the RC is checked out; the release tree is never downloaded | |
| assert len(calls["checkout"]) == 1 | |
| assert calls["checkout"][0][0].endswith("/0.42.0-incubating-RC1") | |
| source_url, target_url, message, apache_id, dry_run = calls["promote"] | |
| assert source_url.endswith("/0.42.0-incubating-RC1") | |
| assert target_url == "https://dist.apache.org/repos/dist/release/incubator/burr/0.42.0" | |
| assert apache_id == "hari" | |
| assert dry_run is True | |
| def test_verify_parser_accepts_skip_signing(): | |
| parser = release._build_parser() | |
| args = parser.parse_args(["verify", "0.42.0", "0", "--skip-signing"]) | |
| assert args.skip_signing is True | |
| def test_cmd_verify_skip_signing_succeeds_without_asc_files(tmp_path): | |
| version = "0.42.0" | |
| content = b"fake artifact content" | |
| sha = hashlib.sha512(content).hexdigest() | |
| artifact_name = f"apache-burr-{version}-incubating-src.tar.gz" | |
| (tmp_path / artifact_name).write_bytes(content) | |
| (tmp_path / f"{artifact_name}.sha512").write_text(f"{sha} {artifact_name}\n") | |
| # No .asc file — simulates a --skip-signing build | |
| args = Namespace(version=version, rc_num="0", artifacts_dir=str(tmp_path), skip_signing=True) | |
| assert release.cmd_verify(args) is True |