Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / notebooks-smoke (push) Has been cancelled
Deploy / changes (push) Has been cancelled
Deploy / notebooks (push) Has been cancelled
Deploy / zotero (push) Has been cancelled
Deploy / docs (push) Has been cancelled
Deploy / api (push) Has been cancelled
Deploy / bcda (push) Has been cancelled
Deploy / llm (push) Has been cancelled
Deploy / mc (push) Has been cancelled
Deploy / report (push) Has been cancelled
Infra CI / api (push) Has been cancelled
Infra CI / notebooks (push) Has been cancelled
Infra CI / zotero (push) Has been cancelled
Infra CI / docs (push) Has been cancelled
Infra CI / bcda (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
The mc container's rootfs is read-only, so docker cp failed after the bag was already served; the tarball now streams in through 'rc pipe'. Re-publishing the identical bag (same tag manifest) runs the remaining steps; a different bag under a published version is still refused. v0.1 is out: reach /releases, rustfs lead-releases, Gitea release lead-review-v0.1, bib GI94D3XZ.
219 lines
6.8 KiB
Python
219 lines
6.8 KiB
Python
"""stack lead release publish — serve, archive, announce a verified bag."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from lead.release.bagit import write_bag
|
|
from lead.release.publish import publish, read_bag_info, write_index
|
|
|
|
|
|
def _bag(root: Path, version: str = "0.1") -> Path:
|
|
bag = root / f"lead-review-v{version}"
|
|
(bag / "data").mkdir(parents=True)
|
|
(bag / "data" / "protocol.md").write_text("# protocol\n")
|
|
(bag / "data" / "ro-crate-metadata.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"@context": "https://w3id.org/ro/crate/1.2/context",
|
|
"@graph": [
|
|
{
|
|
"@id": "ro-crate-metadata.json",
|
|
"@type": "CreativeWork",
|
|
"about": {"@id": "./"},
|
|
},
|
|
{
|
|
"@id": "./",
|
|
"@type": "Dataset",
|
|
"name": "LEAD living review",
|
|
"description": "test release",
|
|
"datePublished": "2026-11-11",
|
|
"license": {
|
|
"@id": "https://creativecommons.org/licenses/by/4.0/"
|
|
},
|
|
"hasPart": [{"@id": "protocol.md"}],
|
|
},
|
|
{
|
|
"@id": "protocol.md",
|
|
"@type": "File",
|
|
"sha256": __import__("hashlib")
|
|
.sha256(b"# protocol\n")
|
|
.hexdigest(),
|
|
"contentSize": "11",
|
|
},
|
|
],
|
|
}
|
|
)
|
|
)
|
|
write_bag(
|
|
bag,
|
|
[
|
|
("Version", version),
|
|
("Git-Commit", "a" * 40),
|
|
("Bagging-Date", "2026-11-11"),
|
|
("Internal-Sender-Description", "first release"),
|
|
],
|
|
)
|
|
return bag
|
|
|
|
|
|
class Calls:
|
|
def __init__(self) -> None:
|
|
self.archived: list[tuple[Path, str]] = []
|
|
self.releases: list[dict] = []
|
|
self.bib: list[dict] = []
|
|
|
|
def archive(self, bag: Path, name: str) -> None:
|
|
self.archived.append((bag, name))
|
|
|
|
def gitea_release(self, **kw) -> str:
|
|
self.releases.append(kw)
|
|
return "https://git.test/rel/1"
|
|
|
|
def bib_item(self, **kw) -> str:
|
|
self.bib.append(kw)
|
|
return "KEY12345"
|
|
|
|
|
|
def test_read_bag_info_joins_continuation_lines(tmp_path):
|
|
bag = _bag(tmp_path)
|
|
info = read_bag_info(bag)
|
|
assert info["Version"] == "0.1" and info["Git-Commit"] == "a" * 40
|
|
assert info["Internal-Sender-Description"] == "first release"
|
|
|
|
|
|
def test_publish_serves_archives_and_announces(tmp_path):
|
|
bag = _bag(tmp_path / "build")
|
|
serve = tmp_path / "serve"
|
|
calls = Calls()
|
|
out = publish(
|
|
bag,
|
|
serve_root=serve,
|
|
base_url="https://reach.test/releases",
|
|
archive=calls.archive,
|
|
gitea_release=calls.gitea_release,
|
|
bib_item=calls.bib_item,
|
|
)
|
|
served = serve / bag.name
|
|
assert (served / "bagit.txt").exists()
|
|
assert calls.archived == [(served, bag.name)]
|
|
assert calls.releases[0]["tag"] == "lead-review-v0.1"
|
|
assert calls.releases[0]["commit"] == "a" * 40
|
|
assert calls.bib[0]["url"] == "https://reach.test/releases/lead-review-v0.1/"
|
|
index = json.loads((serve / "index.json").read_text())
|
|
assert index["releases"][0]["version"] == "0.1"
|
|
assert out["url"] == "https://reach.test/releases/lead-review-v0.1/"
|
|
|
|
|
|
def test_publish_refuses_a_bag_that_does_not_verify(tmp_path):
|
|
bag = _bag(tmp_path / "build")
|
|
(bag / "data" / "protocol.md").write_text("tampered\n")
|
|
calls = Calls()
|
|
with pytest.raises(ValueError, match="does not verify"):
|
|
publish(
|
|
bag,
|
|
serve_root=tmp_path / "serve",
|
|
base_url="https://reach.test/releases",
|
|
archive=calls.archive,
|
|
gitea_release=calls.gitea_release,
|
|
bib_item=calls.bib_item,
|
|
)
|
|
assert not calls.archived and not calls.releases
|
|
|
|
|
|
def test_publish_never_overwrites_a_published_version(tmp_path):
|
|
from lead.release.bagit import write_bag
|
|
|
|
calls = Calls()
|
|
kw = dict(
|
|
serve_root=tmp_path / "serve",
|
|
base_url="https://reach.test/releases",
|
|
archive=calls.archive,
|
|
gitea_release=calls.gitea_release,
|
|
bib_item=calls.bib_item,
|
|
)
|
|
publish(_bag(tmp_path / "a"), **kw)
|
|
other = _bag(tmp_path / "b")
|
|
write_bag(other, [("Version", "0.1"), ("Git-Commit", "b" * 40)])
|
|
with pytest.raises(FileExistsError):
|
|
publish(other, **kw)
|
|
|
|
|
|
def test_republishing_the_identical_bag_resumes(tmp_path):
|
|
# A publish that died after serving (the archive step failed) must be
|
|
# finishable: the same bag again runs the remaining steps.
|
|
calls = Calls()
|
|
kw = dict(
|
|
serve_root=tmp_path / "serve",
|
|
base_url="https://reach.test/releases",
|
|
gitea_release=calls.gitea_release,
|
|
bib_item=calls.bib_item,
|
|
)
|
|
bag = _bag(tmp_path / "a")
|
|
|
|
def boom(bag, name):
|
|
raise RuntimeError("rustfs down")
|
|
|
|
with pytest.raises(RuntimeError):
|
|
publish(bag, archive=boom, **kw)
|
|
publish(bag, archive=calls.archive, **kw)
|
|
assert len(calls.archived) == 1 and len(calls.releases) == 1
|
|
|
|
|
|
def test_index_lists_every_served_release_newest_first(tmp_path):
|
|
serve = tmp_path / "serve"
|
|
for v in ("0.1", "0.2"):
|
|
bag = _bag(tmp_path / f"b{v}", v)
|
|
(serve).mkdir(exist_ok=True)
|
|
bag.rename(serve / bag.name)
|
|
write_index(serve, base_url="https://reach.test/releases")
|
|
index = json.loads((serve / "index.json").read_text())
|
|
assert [r["version"] for r in index["releases"]] == ["0.2", "0.1"]
|
|
assert (serve / "index.html").read_text().count("lead-review-v") >= 2
|
|
|
|
|
|
def test_cli_publish_offline(tmp_path):
|
|
from typer.testing import CliRunner
|
|
|
|
from cli.lead import app
|
|
|
|
bag = _bag(tmp_path / "build")
|
|
res = CliRunner().invoke(
|
|
app,
|
|
[
|
|
"release",
|
|
"publish",
|
|
str(bag),
|
|
"--serve-root",
|
|
str(tmp_path / "serve"),
|
|
"--base-url",
|
|
"https://reach.test/releases",
|
|
"--no-archive",
|
|
"--no-gitea",
|
|
"--no-bib",
|
|
],
|
|
)
|
|
assert res.exit_code == 0, res.output
|
|
assert "url\thttps://reach.test/releases/lead-review-v0.1/" in res.output
|
|
from lead.release.bagit import write_bag
|
|
|
|
other = _bag(tmp_path / "other")
|
|
write_bag(other, [("Version", "0.1"), ("Git-Commit", "c" * 40)])
|
|
again = CliRunner().invoke(
|
|
app,
|
|
[
|
|
"release",
|
|
"publish",
|
|
str(other),
|
|
"--serve-root",
|
|
str(tmp_path / "serve"),
|
|
"--no-archive",
|
|
"--no-gitea",
|
|
"--no-bib",
|
|
],
|
|
)
|
|
assert again.exit_code == 1 and "already published" in again.output
|