Skip to content

Commit baddec1

Browse files
committed
Fix publication promotion retry finalization
1 parent f2b565f commit baddec1

3 files changed

Lines changed: 138 additions & 9 deletions

File tree

.github/scripts/promote_publication_pipeline.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,19 @@ def _promotion_context_from_status(context: RunContext, status: dict) -> RunCont
5959
raise RuntimeError("Run manifest is missing candidate_version.")
6060
if not release_bump:
6161
raise RuntimeError("Run manifest is missing release_bump.")
62+
release_version = _manifest_field(manifest, "release_version")
63+
if not release_version:
64+
release_version = release_version_from_bump(
65+
_current_package_version(),
66+
release_bump,
67+
)
6268
return RunContext.from_mapping(
6369
manifest.get("run_context"),
6470
run_id=context.run_id,
6571
modal_app_name=context.modal_app_name,
6672
modal_environment=context.modal_environment,
6773
candidate_version=candidate_version,
68-
release_version=release_version_from_bump(
69-
_current_package_version(),
70-
release_bump,
71-
),
74+
release_version=release_version,
7275
base_release_version=base_release_version,
7376
release_bump=release_bump,
7477
)

.github/scripts/restore_publication_changelog.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import filecmp
6+
import json
67
import os
78
import shutil
89
import sys
@@ -11,6 +12,7 @@
1112

1213
REPO_ROOT = Path(__file__).resolve().parents[2]
1314
ROOT_CHANGELOG_DIR = REPO_ROOT / "changelog.d"
15+
PUBLICATION_SCOPE_PATH = REPO_ROOT / ".github" / "publication_scope.json"
1416
PUBLICATION_CANDIDATES_DIR = REPO_ROOT / ".github" / "publication_candidates"
1517
CHANGELOG_KEEP_FILE = ".gitkeep"
1618

@@ -53,16 +55,61 @@ def _validate_root_fragments_match_snapshot(
5355
)
5456

5557

58+
def _publication_scope() -> dict[str, str]:
59+
if not PUBLICATION_SCOPE_PATH.exists():
60+
return {}
61+
return json.loads(PUBLICATION_SCOPE_PATH.read_text())
62+
63+
64+
def _scope_matches_environment(scope: dict[str, str]) -> bool:
65+
expected = {
66+
"candidate_scope": os.environ.get("US_DATA_CANDIDATE_VERSION")
67+
or os.environ.get("CANDIDATE_VERSION"),
68+
"base_release_version": os.environ.get("US_DATA_BASE_RELEASE_VERSION")
69+
or os.environ.get("BASE_RELEASE_VERSION"),
70+
"release_bump": os.environ.get("US_DATA_RELEASE_BUMP")
71+
or os.environ.get("RELEASE_BUMP"),
72+
}
73+
return all(
74+
not value or not scope.get(key) or scope[key] == value
75+
for key, value in expected.items()
76+
)
77+
78+
79+
def _snapshot_dir(run_id: str) -> Path:
80+
return PUBLICATION_CANDIDATES_DIR / run_id / "changelog.d"
81+
82+
83+
def _resolve_snapshot_dir(run_id: str) -> Path:
84+
requested_dir = _snapshot_dir(run_id)
85+
if _fragments(requested_dir):
86+
return requested_dir
87+
88+
scope = _publication_scope()
89+
scope_run_id = scope.get("run_id", "")
90+
if scope_run_id and scope_run_id != run_id and _scope_matches_environment(scope):
91+
scope_dir = _snapshot_dir(scope_run_id)
92+
if _fragments(scope_dir):
93+
print(
94+
"No changelog snapshot found for promotion run "
95+
f"{run_id}; using publication scope snapshot {scope_run_id}.",
96+
)
97+
return scope_dir
98+
99+
details = (
100+
f"No candidate changelog fragments found for run {run_id}: {requested_dir}"
101+
)
102+
if scope_run_id and scope_run_id != run_id:
103+
details += f"; publication scope points to {scope_run_id}: {_snapshot_dir(scope_run_id)}"
104+
raise RuntimeError(details)
105+
106+
56107
def restore_candidate_changelog(run_id: str) -> Path:
57108
if not run_id:
58109
raise RuntimeError("US_DATA_RUN_ID is required to restore changelog fragments.")
59110

60-
snapshot_dir = PUBLICATION_CANDIDATES_DIR / run_id / "changelog.d"
111+
snapshot_dir = _resolve_snapshot_dir(run_id)
61112
snapshot_fragments = _fragments(snapshot_dir)
62-
if not snapshot_fragments:
63-
raise RuntimeError(
64-
f"No candidate changelog fragments found for run {run_id}: {snapshot_dir}"
65-
)
66113

67114
ROOT_CHANGELOG_DIR.mkdir(parents=True, exist_ok=True)
68115
root_fragments = _fragments(ROOT_CHANGELOG_DIR)

tests/unit/test_publication_scripts.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,42 @@ def test_restore_publication_changelog_restores_candidate_snapshot(
406406
assert (root_changelog / "123.changed.md").read_text() == "Changed a thing.\n"
407407

408408

409+
def test_restore_publication_changelog_falls_back_to_scope_snapshot(
410+
tmp_path,
411+
monkeypatch,
412+
):
413+
module = _load_script(
414+
".github/scripts/restore_publication_changelog.py",
415+
"restore_publication_changelog_scope_fallback_test",
416+
)
417+
root_changelog = tmp_path / "changelog.d"
418+
publication_dir = tmp_path / ".github" / "publication_candidates"
419+
snapshot = publication_dir / "versioning-run" / "changelog.d"
420+
snapshot.mkdir(parents=True)
421+
(snapshot / "123.changed.md").write_text("Changed a thing.\n")
422+
scope_path = tmp_path / ".github" / "publication_scope.json"
423+
scope_path.write_text(
424+
json.dumps(
425+
{
426+
"base_release_version": "1.115.3",
427+
"candidate_scope": "1.115.3-patch",
428+
"release_bump": "patch",
429+
"run_id": "versioning-run",
430+
}
431+
)
432+
)
433+
monkeypatch.setenv("US_DATA_CANDIDATE_VERSION", "1.115.3-patch")
434+
monkeypatch.setenv("US_DATA_BASE_RELEASE_VERSION", "1.115.3")
435+
monkeypatch.setenv("US_DATA_RELEASE_BUMP", "patch")
436+
monkeypatch.setattr(module, "ROOT_CHANGELOG_DIR", root_changelog)
437+
monkeypatch.setattr(module, "PUBLICATION_SCOPE_PATH", scope_path)
438+
monkeypatch.setattr(module, "PUBLICATION_CANDIDATES_DIR", publication_dir)
439+
440+
module.restore_candidate_changelog("pipeline-run")
441+
442+
assert (root_changelog / "123.changed.md").read_text() == "Changed a thing.\n"
443+
444+
409445
def test_restore_publication_changelog_rejects_unrelated_root_fragments(
410446
tmp_path,
411447
monkeypatch,
@@ -650,6 +686,49 @@ def from_name(app_name, function_name, **kwargs):
650686
assert "VERSION_OVERRIDE" not in json.dumps(captured["calls"])
651687

652688

689+
def test_promote_publication_script_prefers_manifest_release_version(
690+
tmp_path,
691+
monkeypatch,
692+
):
693+
monkeypatch.setitem(
694+
sys.modules,
695+
"modal",
696+
types.SimpleNamespace(Function=types.SimpleNamespace()),
697+
)
698+
module = _load_script(
699+
".github/scripts/promote_publication_pipeline.py",
700+
"promote_publication_pipeline_release_version_test",
701+
)
702+
_write_pyproject(tmp_path, "1.74.0")
703+
monkeypatch.setattr(module, "_REPO_ROOT", tmp_path)
704+
context = module.RunContext.from_mapping(
705+
{"run_id": "run-123"},
706+
modal_app_name="app",
707+
modal_environment="main",
708+
)
709+
710+
promoted_context = module._promotion_context_from_status(
711+
context,
712+
{
713+
"run_manifest": {
714+
"run_id": "run-123",
715+
"candidate_version": "1.73.0-minor",
716+
"base_release_version": "1.73.0",
717+
"release_bump": "minor",
718+
"release_version": "1.74.0",
719+
"run_context": {
720+
"run_id": "run-123",
721+
"candidate_version": "1.73.0-minor",
722+
"base_release_version": "1.73.0",
723+
"release_bump": "minor",
724+
},
725+
}
726+
},
727+
)
728+
729+
assert promoted_context.release_version == "1.74.0"
730+
731+
653732
def test_promote_publication_script_requires_release_bump(
654733
tmp_path,
655734
monkeypatch,

0 commit comments

Comments
 (0)