Skip to content

Commit 92f2394

Browse files
authored
Merge pull request #1091 from PolicyEngine/codex/self-healing-publication-version-sync
Self-heal publication version sync
2 parents 44bb2d5 + b7521d6 commit 92f2394

7 files changed

Lines changed: 494 additions & 44 deletions

File tree

.github/scripts/check_data_release_version.py

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

55
import argparse
6+
from dataclasses import dataclass
67
import json
78
import os
89
from pathlib import Path
@@ -19,6 +20,17 @@
1920
)
2021
VERSION_RE = re.compile(r'^version\s*=\s*"([^"]+)"', re.MULTILINE)
2122
SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:rc\d+)?$")
23+
CURRENT = "current"
24+
BEHIND = "behind"
25+
AHEAD = "ahead"
26+
UNKNOWN = "unknown"
27+
28+
29+
@dataclass(frozen=True)
30+
class ReleaseVersionState:
31+
package_version: str
32+
finalized_release_version: str
33+
release_version_relation: str
2234

2335

2436
def stable_version_tuple(version: str) -> tuple[int, int, int]:
@@ -28,7 +40,22 @@ def stable_version_tuple(version: str) -> tuple[int, int, int]:
2840
return tuple(int(part) for part in match.groups())
2941

3042

31-
def pyproject_version(root: Path = REPO_ROOT) -> str:
43+
def release_version_relation(
44+
*,
45+
package_version: str,
46+
finalized_release_version: str,
47+
) -> str:
48+
package_tuple = stable_version_tuple(package_version)
49+
finalized_tuple = stable_version_tuple(finalized_release_version)
50+
if package_tuple < finalized_tuple:
51+
return BEHIND
52+
if package_tuple > finalized_tuple:
53+
return AHEAD
54+
return CURRENT
55+
56+
57+
def pyproject_version(root: Path | None = None) -> str:
58+
root = root or REPO_ROOT
3259
text = (root / "pyproject.toml").read_text()
3360
match = VERSION_RE.search(text)
3461
if not match:
@@ -58,8 +85,12 @@ def version_violations(
5885
package_version: str,
5986
finalized_release_version: str,
6087
) -> list[str]:
61-
if stable_version_tuple(package_version) >= stable_version_tuple(
62-
finalized_release_version
88+
if (
89+
release_version_relation(
90+
package_version=package_version,
91+
finalized_release_version=finalized_release_version,
92+
)
93+
!= BEHIND
6394
):
6495
return []
6596
return [
@@ -70,22 +101,55 @@ def version_violations(
70101
]
71102

72103

73-
def check_repository(
74-
root: Path = REPO_ROOT,
104+
def check_repository_state(
105+
root: Path | None = None,
75106
*,
76107
finalized_release_version: str | None = None,
77108
version_manifest_url: str = DEFAULT_VERSION_MANIFEST_URL,
78-
) -> list[str]:
109+
) -> ReleaseVersionState:
110+
root = root or REPO_ROOT
79111
package_version = pyproject_version(root)
80112
finalized_release_version = finalized_release_version or latest_hf_release_version(
81113
version_manifest_url
82114
)
83-
return version_violations(
115+
relation = release_version_relation(
116+
package_version=package_version,
117+
finalized_release_version=finalized_release_version,
118+
)
119+
return ReleaseVersionState(
84120
package_version=package_version,
85121
finalized_release_version=finalized_release_version,
122+
release_version_relation=relation,
86123
)
87124

88125

126+
def check_repository(
127+
root: Path | None = None,
128+
*,
129+
finalized_release_version: str | None = None,
130+
version_manifest_url: str = DEFAULT_VERSION_MANIFEST_URL,
131+
) -> list[str]:
132+
state = check_repository_state(
133+
root,
134+
finalized_release_version=finalized_release_version,
135+
version_manifest_url=version_manifest_url,
136+
)
137+
return version_violations(
138+
package_version=state.package_version,
139+
finalized_release_version=state.finalized_release_version,
140+
)
141+
142+
143+
def write_github_outputs(state: ReleaseVersionState) -> None:
144+
output_path = os.environ.get("GITHUB_OUTPUT")
145+
if not output_path:
146+
return
147+
with Path(output_path).open("a") as output:
148+
output.write(f"package_version={state.package_version}\n")
149+
output.write(f"finalized_release_version={state.finalized_release_version}\n")
150+
output.write(f"release_version_relation={state.release_version_relation}\n")
151+
152+
89153
def main(argv: list[str] | None = None) -> int:
90154
parser = argparse.ArgumentParser(description=__doc__)
91155
parser.add_argument(
@@ -103,17 +167,58 @@ def main(argv: list[str] | None = None) -> int:
103167
args = parser.parse_args(argv)
104168

105169
try:
106-
violations = check_repository(
107-
version_manifest_url=args.version_manifest_url,
170+
package_version = pyproject_version()
171+
stable_version_tuple(package_version)
172+
except (OSError, ValueError) as exc:
173+
write_github_outputs(
174+
ReleaseVersionState(
175+
package_version="",
176+
finalized_release_version="",
177+
release_version_relation=UNKNOWN,
178+
)
179+
)
180+
print(f"Could not read data package version: {exc}", file=sys.stderr)
181+
return 1
182+
183+
try:
184+
finalized_release_version = latest_hf_release_version(args.version_manifest_url)
185+
state = ReleaseVersionState(
186+
package_version=package_version,
187+
finalized_release_version=finalized_release_version,
188+
release_version_relation=release_version_relation(
189+
package_version=package_version,
190+
finalized_release_version=finalized_release_version,
191+
),
108192
)
109193
except (URLError, OSError, ValueError) as exc:
194+
write_github_outputs(
195+
ReleaseVersionState(
196+
package_version=package_version,
197+
finalized_release_version="",
198+
release_version_relation=UNKNOWN,
199+
)
200+
)
110201
print(
111202
f"Could not check finalized HF data release version: {exc}", file=sys.stderr
112203
)
113204
return 1 if args.mode == "fail" else 0
114205

206+
write_github_outputs(state)
207+
violations = version_violations(
208+
package_version=state.package_version,
209+
finalized_release_version=state.finalized_release_version,
210+
)
115211
if not violations:
116-
print("Data package version is current with the latest finalized HF release.")
212+
if state.release_version_relation == AHEAD:
213+
print(
214+
"Data package version "
215+
f"{state.package_version} is ahead of finalized HF release "
216+
f"{state.finalized_release_version}."
217+
)
218+
else:
219+
print(
220+
"Data package version is current with the latest finalized HF release."
221+
)
117222
return 0
118223

119224
for violation in violations:

.github/scripts/promote_publication_pipeline.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import json
66
import os
77
import sys
8-
import tomllib
98
from pathlib import Path
109

1110
import modal
@@ -17,15 +16,9 @@
1716
from policyengine_us_data.utils.run_context import ( # noqa: E402
1817
RunContext,
1918
release_version_from_bump,
20-
stable_release_version,
2119
)
2220

2321

24-
def _current_package_version() -> str:
25-
with (_REPO_ROOT / "pyproject.toml").open("rb") as file:
26-
return stable_release_version(tomllib.load(file)["project"]["version"])
27-
28-
2922
def _modal_function(app_name: str, function_name: str, environment_name: str):
3023
if environment_name:
3124
return modal.Function.from_name(
@@ -61,8 +54,13 @@ def _promotion_context_from_status(context: RunContext, status: dict) -> RunCont
6154
raise RuntimeError("Run manifest is missing release_bump.")
6255
release_version = _manifest_field(manifest, "release_version")
6356
if not release_version:
57+
if not base_release_version:
58+
raise RuntimeError(
59+
"Run manifest is missing base_release_version, so promotion "
60+
"cannot reconstruct release_version from release_bump."
61+
)
6462
release_version = release_version_from_bump(
65-
_current_package_version(),
63+
base_release_version,
6664
release_bump,
6765
)
6866
return RunContext.from_mapping(
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Synchronize pyproject.toml with the latest finalized HF data release."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import os
7+
from pathlib import Path
8+
import re
9+
import sys
10+
from urllib.error import URLError
11+
12+
_SCRIPT_DIR = Path(__file__).resolve().parent
13+
if str(_SCRIPT_DIR) not in sys.path:
14+
sys.path.insert(0, str(_SCRIPT_DIR))
15+
16+
from check_data_release_version import (
17+
BEHIND,
18+
DEFAULT_VERSION_MANIFEST_URL,
19+
REPO_ROOT,
20+
check_repository_state,
21+
)
22+
23+
24+
VERSION_RE = re.compile(r'^(version\s*=\s*)"([^"]+)"', re.MULTILINE)
25+
26+
27+
def update_pyproject_version(pyproject: Path, release_version: str) -> str:
28+
text = pyproject.read_text()
29+
match = VERSION_RE.search(text)
30+
if not match:
31+
raise ValueError("Could not find project version in pyproject.toml")
32+
33+
current_version = match.group(2)
34+
if current_version == release_version:
35+
return current_version
36+
37+
updated = VERSION_RE.sub(rf'\1"{release_version}"', text, count=1)
38+
pyproject.write_text(updated)
39+
return current_version
40+
41+
42+
def sync_finalized_data_release_version(
43+
root: Path | None = None,
44+
*,
45+
finalized_release_version: str | None = None,
46+
version_manifest_url: str = DEFAULT_VERSION_MANIFEST_URL,
47+
) -> bool:
48+
root = root or REPO_ROOT
49+
state = check_repository_state(
50+
root,
51+
finalized_release_version=finalized_release_version,
52+
version_manifest_url=version_manifest_url,
53+
)
54+
if state.release_version_relation != BEHIND:
55+
print(
56+
"No finalized data release version sync needed: "
57+
f"package={state.package_version}, "
58+
f"finalized={state.finalized_release_version}, "
59+
f"relation={state.release_version_relation}."
60+
)
61+
return False
62+
63+
previous_version = update_pyproject_version(
64+
root / "pyproject.toml",
65+
state.finalized_release_version,
66+
)
67+
print(
68+
"Synchronized pyproject.toml with finalized HF data release: "
69+
f"{previous_version} -> {state.finalized_release_version}."
70+
)
71+
return True
72+
73+
74+
def main(argv: list[str] | None = None) -> int:
75+
parser = argparse.ArgumentParser(description=__doc__)
76+
parser.add_argument(
77+
"--version-manifest-url",
78+
default=os.environ.get(
79+
"US_DATA_VERSION_MANIFEST_URL", DEFAULT_VERSION_MANIFEST_URL
80+
),
81+
)
82+
parser.add_argument(
83+
"--finalized-release-version",
84+
default=os.environ.get("US_DATA_FINALIZED_RELEASE_VERSION"),
85+
help="Already-resolved finalized HF release version to sync to.",
86+
)
87+
args = parser.parse_args(argv)
88+
89+
try:
90+
sync_finalized_data_release_version(
91+
finalized_release_version=args.finalized_release_version,
92+
version_manifest_url=args.version_manifest_url,
93+
)
94+
except (URLError, OSError, ValueError) as exc:
95+
print(
96+
f"Could not synchronize finalized HF data release version: {exc}",
97+
file=sys.stderr,
98+
)
99+
return 1
100+
return 0
101+
102+
103+
if __name__ == "__main__":
104+
sys.exit(main())

.github/workflows/pipeline.yaml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,6 @@ on:
6767
description: "Number of Modal workers for parallel matrix build"
6868
default: "50"
6969
type: string
70-
allow_stale_policyengine_us:
71-
description: "Allow production build when policyengine-us lags the latest PyPI release"
72-
default: false
73-
type: boolean
74-
7570
concurrency:
7671
group: pipeline-${{ github.run_id }}-${{ github.run_attempt }}
7772
cancel-in-progress: false
@@ -103,11 +98,6 @@ jobs:
10398
RELEASE_BUMP: ${{ inputs.release_bump || '' }}
10499
run: python .github/scripts/resolve_run_context.py
105100

106-
- name: Require current PolicyEngine US dependency
107-
env:
108-
POLICYENGINE_US_ALLOW_STALE: ${{ inputs.allow_stale_policyengine_us }}
109-
run: python .github/scripts/check_policyengine_us_dependency.py --mode fail
110-
111101
- name: Require pyproject.toml to match finalized HF release base
112102
run: python .github/scripts/check_data_release_version.py --mode fail
113103

0 commit comments

Comments
 (0)