From 1626e864068b1f63bd9c18891650ef48d47388f5 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Thu, 9 Apr 2026 23:02:31 +0200 Subject: [PATCH 1/4] Propagate simulation telemetry metadata --- policyengine_api/libs/simulation_api_modal.py | 6 ++ policyengine_api/services/economy_service.py | 86 ++++++++++++++++++- tests/fixtures/libs/simulation_api_modal.py | 11 +++ tests/fixtures/services/economy_service.py | 2 + tests/unit/libs/test_simulation_api_modal.py | 21 +++++ tests/unit/services/test_economy_service.py | 31 +++++++ 6 files changed, 153 insertions(+), 4 deletions(-) diff --git a/policyengine_api/libs/simulation_api_modal.py b/policyengine_api/libs/simulation_api_modal.py index e171888e7..4cf0b1423 100644 --- a/policyengine_api/libs/simulation_api_modal.py +++ b/policyengine_api/libs/simulation_api_modal.py @@ -22,6 +22,7 @@ class ModalSimulationExecution: job_id: str status: str + run_id: Optional[str] = None result: Optional[dict] = None error: Optional[str] = None policyengine_bundle: Optional[dict] = None @@ -88,6 +89,7 @@ def run(self, payload: dict) -> ModalSimulationExecution: { "message": "Modal simulation job submitted", "job_id": data.get("job_id"), + "run_id": data.get("run_id"), "status": data.get("status"), }, severity="INFO", @@ -98,12 +100,14 @@ def run(self, payload: dict) -> ModalSimulationExecution: status=data["status"], policyengine_bundle=data.get("policyengine_bundle"), resolved_app_name=data.get("resolved_app_name"), + run_id=data.get("run_id"), ) except httpx.HTTPStatusError as e: logger.log_struct( { "message": f"Modal API HTTP error: {e.response.status_code}", + "run_id": (payload.get("_telemetry") or {}).get("run_id"), "response_text": e.response.text[:500], }, severity="ERROR", @@ -114,6 +118,7 @@ def run(self, payload: dict) -> ModalSimulationExecution: logger.log_struct( { "message": f"Modal API request error: {str(e)}", + "run_id": (payload.get("_telemetry") or {}).get("run_id"), }, severity="ERROR", ) @@ -174,6 +179,7 @@ def get_execution_by_id(self, job_id: str) -> ModalSimulationExecution: return ModalSimulationExecution( job_id=job_id, status=data["status"], + run_id=data.get("run_id"), result=data.get("result"), error=data.get("error"), policyengine_bundle=data.get("policyengine_bundle"), diff --git a/policyengine_api/services/economy_service.py b/policyengine_api/services/economy_service.py index 42612eefa..08108152c 100644 --- a/policyengine_api/services/economy_service.py +++ b/policyengine_api/services/economy_service.py @@ -24,6 +24,8 @@ from policyengine.utils.data.datasets import get_default_dataset import json import datetime +import hashlib +import uuid from typing import Literal, Any, Optional, Annotated from dotenv import load_dotenv from pydantic import BaseModel @@ -484,17 +486,21 @@ def _handle_create_impact( data_version=setup_options.data_version, ) + sim_params = sim_config.model_dump(mode="json") + telemetry = self._build_simulation_telemetry( + setup_options=setup_options, + sim_config=sim_params, + ) + logger.log_struct( { "message": "Setting up sim API job", + "run_id": telemetry["run_id"], **setup_options.model_dump(), } ) - # Build params with metadata for Logfire tracing in the simulation API. - # The _metadata field will be captured by the Logfire span before - # SimulationOptions validation (which silently ignores extra fields). - sim_params = sim_config.model_dump() + # Preserve both legacy metadata and the new telemetry envelope. sim_params["_metadata"] = { "reform_policy_id": setup_options.reform_policy_id, "baseline_policy_id": setup_options.baseline_policy_id, @@ -505,14 +511,17 @@ def _handle_create_impact( "dataset": setup_options.dataset, "resolved_app_name": setup_options.runtime_app_name, } + sim_params["_telemetry"] = telemetry sim_api_execution = simulation_api.run(sim_params) execution_id = simulation_api.get_execution_id(sim_api_execution) + run_id = getattr(sim_api_execution, "run_id", None) or telemetry["run_id"] progress_log = { **setup_options.model_dump(), "message": "Sim API job started", "execution_id": execution_id, + "run_id": run_id, } logger.log_struct(progress_log, severity="INFO") @@ -759,6 +768,75 @@ def _setup_data( ) raise + def _build_simulation_telemetry( + self, + setup_options: EconomicImpactSetupOptions, + sim_config: dict[str, Any], + ) -> dict[str, Any]: + simulation_kind, geography_type, geography_code = ( + self._classify_simulation_geography( + country_id=setup_options.country_id, + region=setup_options.region, + ) + ) + + return { + "run_id": str(uuid.uuid4()), + "process_id": setup_options.process_id, + "traceparent": self._get_current_traceparent(), + "requested_at": datetime.datetime.now(datetime.UTC).isoformat(), + "simulation_kind": simulation_kind, + "geography_code": geography_code, + "geography_type": geography_type, + "config_hash": self._stable_config_hash(sim_config), + "capture_mode": "disabled", + } + + def _classify_simulation_geography( + self, + country_id: str, + region: str, + ) -> tuple[str, str, str]: + if region == country_id: + return "national", "national", country_id + + if "/" not in region: + return "other", "other", region + + geography_type, geography_code = region.split("/", maxsplit=1) + simulation_kind = ( + "district" + if geography_type == "congressional_district" + else geography_type + ) + return simulation_kind, geography_type, geography_code + + def _stable_config_hash(self, payload: dict[str, Any]) -> str: + encoded = json.dumps( + payload, + sort_keys=True, + separators=(",", ":"), + default=str, + ).encode("utf-8") + return f"sha256:{hashlib.sha256(encoded).hexdigest()}" + + def _get_current_traceparent(self) -> str | None: + try: + from opentelemetry import trace + except Exception: + return None + + span = trace.get_current_span() + span_context = span.get_span_context() + if not getattr(span_context, "is_valid", False): + return None + + trace_flags = int(getattr(span_context, "trace_flags", 0)) + return ( + f"00-{span_context.trace_id:032x}-" + f"{span_context.span_id:016x}-{trace_flags:02x}" + ) + # Note: The following methods that interface with the ReformImpactsService # are written separately because the service relies upon mutating an original # 'computing' record to 'ok' or 'error' status, rather than creating a new record. diff --git a/tests/fixtures/libs/simulation_api_modal.py b/tests/fixtures/libs/simulation_api_modal.py index fa47f8b2a..6d514a7e5 100644 --- a/tests/fixtures/libs/simulation_api_modal.py +++ b/tests/fixtures/libs/simulation_api_modal.py @@ -18,6 +18,7 @@ # Mock data constants MOCK_MODAL_JOB_ID = "fc-abc123xyz" +MOCK_RUN_ID = "run-abc123xyz" MOCK_MODAL_BASE_URL = "https://test-modal-api.modal.run" MOCK_SIMULATION_PAYLOAD = { @@ -31,6 +32,15 @@ "include_cliffs": False, } +MOCK_SIMULATION_PAYLOAD_WITH_TELEMETRY = { + **MOCK_SIMULATION_PAYLOAD, + "_telemetry": { + "run_id": MOCK_RUN_ID, + "process_id": "job_20250626120000_1234", + "capture_mode": "disabled", + }, +} + MOCK_SIMULATION_RESULT = { "poverty_impact": {"baseline": 0.12, "reform": 0.10}, "budget_impact": {"baseline": 1000, "reform": 1200}, @@ -46,6 +56,7 @@ MOCK_SUBMIT_RESPONSE_SUCCESS = { "job_id": MOCK_MODAL_JOB_ID, + "run_id": MOCK_RUN_ID, "status": MODAL_EXECUTION_STATUS_SUBMITTED, "poll_url": f"/jobs/{MOCK_MODAL_JOB_ID}", "country": "us", diff --git a/tests/fixtures/services/economy_service.py b/tests/fixtures/services/economy_service.py index 88f2d08b0..cf41873ed 100644 --- a/tests/fixtures/services/economy_service.py +++ b/tests/fixtures/services/economy_service.py @@ -30,6 +30,7 @@ ) MOCK_MODAL_JOB_ID = "fc-test123xyz" MOCK_EXECUTION_ID = MOCK_MODAL_JOB_ID # Alias for test compatibility +MOCK_RUN_ID = "run-test123xyz" MOCK_PROCESS_ID = "job_20250626120000_1234" MOCK_MODEL_VERSION = "1.2.3" MOCK_POLICYENGINE_VERSION = "3.4.0" @@ -248,6 +249,7 @@ def create_mock_modal_execution( """ mock_execution = MagicMock() mock_execution.job_id = job_id + mock_execution.run_id = MOCK_RUN_ID mock_execution.name = job_id # Alias for compatibility mock_execution.status = status mock_execution.result = result diff --git a/tests/unit/libs/test_simulation_api_modal.py b/tests/unit/libs/test_simulation_api_modal.py index 740bd37a2..afa1b8d10 100644 --- a/tests/unit/libs/test_simulation_api_modal.py +++ b/tests/unit/libs/test_simulation_api_modal.py @@ -23,6 +23,8 @@ MOCK_MODAL_JOB_ID, MOCK_MODAL_BASE_URL, MOCK_SIMULATION_PAYLOAD, + MOCK_SIMULATION_PAYLOAD_WITH_TELEMETRY, + MOCK_RUN_ID, MOCK_SIMULATION_RESULT, MOCK_POLICYENGINE_BUNDLE, MOCK_RESOLVED_APP_NAME, @@ -136,6 +138,7 @@ def test__given_valid_payload__then_returns_execution_with_job_id( # Then assert execution.job_id == MOCK_MODAL_JOB_ID + assert execution.run_id == MOCK_RUN_ID assert execution.status == MODAL_EXECUTION_STATUS_SUBMITTED assert execution.policyengine_bundle == MOCK_POLICYENGINE_BUNDLE assert execution.resolved_app_name == MOCK_RESOLVED_APP_NAME @@ -161,6 +164,24 @@ def test__given_valid_payload__then_posts_to_correct_endpoint( assert "/simulate/economy/comparison" in call_args[0][0] assert call_args[1]["json"] == MOCK_SIMULATION_PAYLOAD + def test__given_telemetry_payload__then_preserves_it_in_post_body( + self, + mock_httpx_client, + mock_modal_logger, + ): + mock_httpx_client.post.return_value = create_mock_httpx_response( + status_code=202, + json_data=MOCK_SUBMIT_RESPONSE_SUCCESS, + ) + api = SimulationAPIModal() + + api.run(MOCK_SIMULATION_PAYLOAD_WITH_TELEMETRY) + + call_args = mock_httpx_client.post.call_args + assert ( + call_args[1]["json"]["_telemetry"]["run_id"] == MOCK_RUN_ID + ) + def test__given_http_error__then_raises_exception( self, mock_httpx_client, diff --git a/tests/unit/services/test_economy_service.py b/tests/unit/services/test_economy_service.py index 9ad1aa1e7..afd627f80 100644 --- a/tests/unit/services/test_economy_service.py +++ b/tests/unit/services/test_economy_service.py @@ -26,6 +26,7 @@ MOCK_OPTIONS_HASH, MOCK_EXECUTION_ID, MOCK_PROCESS_ID, + MOCK_RUN_ID, MOCK_REFORM_IMPACT_DATA, MOCK_RESOLVED_DATASET, MOCK_RESOLVED_APP_NAME, @@ -276,6 +277,36 @@ def test__given_no_previous_impact__includes_metadata_in_simulation_params( sim_params["_metadata"]["resolved_app_name"] == MOCK_RESOLVED_APP_NAME ) + def test__given_no_previous_impact__includes_telemetry_in_simulation_params( + self, + economy_service, + base_params, + mock_country_package_versions, + mock_get_dataset_version, + mock_policy_service, + mock_reform_impacts_service, + mock_simulation_api, + mock_logger, + mock_datetime, + mock_numpy_random, + ): + mock_reform_impacts_service.get_all_reform_impacts.return_value = [] + + economy_service.get_economic_impact(**base_params) + + sim_params = mock_simulation_api.run.call_args[0][0] + + assert sim_params["_telemetry"]["run_id"] + assert sim_params["_telemetry"]["process_id"] == MOCK_PROCESS_ID + assert sim_params["_telemetry"]["simulation_kind"] == "national" + assert sim_params["_telemetry"]["geography_type"] == "national" + assert sim_params["_telemetry"]["geography_code"] == MOCK_COUNTRY_ID + assert sim_params["_telemetry"]["capture_mode"] == "disabled" + assert sim_params["_telemetry"]["config_hash"].startswith("sha256:") + progress_log = mock_logger.log_struct.call_args_list[-1].args[0] + assert progress_log["run_id"] == MOCK_RUN_ID + assert mock_logger.log_struct.call_args_list[-1].kwargs["severity"] == "INFO" + def test__given_runtime_cache_version__uses_versioned_economy_cache_key( self, economy_service, From f8f638ae036ea6a0f4132ee1827cd3941313b84f Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 13 Apr 2026 19:20:25 +0200 Subject: [PATCH 2/4] Format simulation telemetry envelope changes --- policyengine_api/services/economy_service.py | 6 +----- tests/unit/libs/test_simulation_api_modal.py | 4 +--- tests/unit/services/test_economy_service.py | 4 +++- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/policyengine_api/services/economy_service.py b/policyengine_api/services/economy_service.py index 08108152c..771543489 100644 --- a/policyengine_api/services/economy_service.py +++ b/policyengine_api/services/economy_service.py @@ -359,7 +359,6 @@ def _determine_impact_action( self, most_recent_impact: dict | None, ) -> ImpactAction: - if not most_recent_impact: return ImpactAction.CREATE @@ -450,7 +449,6 @@ def _handle_computing_impact( setup_options: EconomicImpactSetupOptions, most_recent_impact: dict, ) -> EconomicImpactResult: - execution = simulation_api.get_execution_by_id( most_recent_impact["execution_id"] ) @@ -805,9 +803,7 @@ def _classify_simulation_geography( geography_type, geography_code = region.split("/", maxsplit=1) simulation_kind = ( - "district" - if geography_type == "congressional_district" - else geography_type + "district" if geography_type == "congressional_district" else geography_type ) return simulation_kind, geography_type, geography_code diff --git a/tests/unit/libs/test_simulation_api_modal.py b/tests/unit/libs/test_simulation_api_modal.py index afa1b8d10..10b278c82 100644 --- a/tests/unit/libs/test_simulation_api_modal.py +++ b/tests/unit/libs/test_simulation_api_modal.py @@ -178,9 +178,7 @@ def test__given_telemetry_payload__then_preserves_it_in_post_body( api.run(MOCK_SIMULATION_PAYLOAD_WITH_TELEMETRY) call_args = mock_httpx_client.post.call_args - assert ( - call_args[1]["json"]["_telemetry"]["run_id"] == MOCK_RUN_ID - ) + assert call_args[1]["json"]["_telemetry"]["run_id"] == MOCK_RUN_ID def test__given_http_error__then_raises_exception( self, diff --git a/tests/unit/services/test_economy_service.py b/tests/unit/services/test_economy_service.py index afd627f80..d036ab296 100644 --- a/tests/unit/services/test_economy_service.py +++ b/tests/unit/services/test_economy_service.py @@ -305,7 +305,9 @@ def test__given_no_previous_impact__includes_telemetry_in_simulation_params( assert sim_params["_telemetry"]["config_hash"].startswith("sha256:") progress_log = mock_logger.log_struct.call_args_list[-1].args[0] assert progress_log["run_id"] == MOCK_RUN_ID - assert mock_logger.log_struct.call_args_list[-1].kwargs["severity"] == "INFO" + assert ( + mock_logger.log_struct.call_args_list[-1].kwargs["severity"] == "INFO" + ) def test__given_runtime_cache_version__uses_versioned_economy_cache_key( self, From 8f469e6a9a37d7a1cff572f61173e14bdf3490f5 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 13 Apr 2026 19:29:18 +0200 Subject: [PATCH 3/4] Fix changelog fragment discovery across CI --- .github/bump_version.py | 7 +-- .github/changelog_fragments.py | 82 +++++++++++++++++++++++++++++++ .github/scripts/update-package.sh | 2 +- .github/workflows/pr.yml | 15 ++++-- 4 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 .github/changelog_fragments.py diff --git a/.github/bump_version.py b/.github/bump_version.py index 779a82e38..a301d9d27 100644 --- a/.github/bump_version.py +++ b/.github/bump_version.py @@ -4,6 +4,8 @@ import sys from pathlib import Path +from changelog_fragments import iter_valid_fragments, load_towncrier_config + def get_current_version(pyproject_path: Path) -> str: text = pyproject_path.read_text() @@ -18,9 +20,8 @@ def get_current_version(pyproject_path: Path) -> str: def infer_bump(changelog_dir: Path) -> str: - fragments = [ - f for f in changelog_dir.iterdir() if f.is_file() and f.name != ".gitkeep" - ] + config = load_towncrier_config(changelog_dir.parent) + fragments = iter_valid_fragments(config) if not fragments: print("No changelog fragments found", file=sys.stderr) sys.exit(1) diff --git a/.github/changelog_fragments.py b/.github/changelog_fragments.py new file mode 100644 index 000000000..be0665b40 --- /dev/null +++ b/.github/changelog_fragments.py @@ -0,0 +1,82 @@ +"""Helpers for locating Towncrier changelog fragments.""" + +from __future__ import annotations + +import sys +import tomllib +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True) +class TowncrierConfig: + root: Path + directory: Path + types: tuple[str, ...] + + +def load_towncrier_config(root: Path) -> TowncrierConfig: + with (root / "pyproject.toml").open("rb") as file: + pyproject = tomllib.load(file) + + towncrier = pyproject["tool"]["towncrier"] + fragment_types = tuple( + entry["directory"] + for entry in towncrier.get("type", []) + if entry.get("directory") + ) + return TowncrierConfig( + root=root, + directory=root / towncrier["directory"], + types=fragment_types, + ) + + +def iter_valid_fragments(config: TowncrierConfig) -> list[Path]: + fragments: list[Path] = [] + for fragment_type in config.types: + fragment_dir = config.directory / fragment_type + if not fragment_dir.exists(): + continue + for path in sorted(fragment_dir.rglob("*")): + if path.is_file() and path.name != ".gitkeep": + fragments.append(path) + return fragments + + +def iter_invalid_fragments(config: TowncrierConfig) -> list[Path]: + fragments: list[Path] = [] + allowed_roots = { + (config.directory / fragment_type).resolve() for fragment_type in config.types + } + for path in sorted(config.directory.rglob("*")): + if not path.is_file() or path.name == ".gitkeep": + continue + if path.parent.resolve() not in allowed_roots: + fragments.append(path) + return fragments + + +def main() -> int: + root = Path(__file__).resolve().parent.parent + config = load_towncrier_config(root) + valid = iter_valid_fragments(config) + invalid = iter_invalid_fragments(config) + + if invalid: + print("Invalid changelog fragments:", file=sys.stderr) + for path in invalid: + print(path.relative_to(root), file=sys.stderr) + return 1 + + if not valid: + print("No valid changelog fragments found", file=sys.stderr) + return 1 + + for path in valid: + print(path.relative_to(root)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/update-package.sh b/.github/scripts/update-package.sh index 5800669e4..2f8ddced2 100755 --- a/.github/scripts/update-package.sh +++ b/.github/scripts/update-package.sh @@ -75,7 +75,7 @@ fi # --------------------------------------------------------------------------- # 6. Create changelog fragment (required by PR CI) # --------------------------------------------------------------------------- -FRAGMENT="changelog.d/update-${PACKAGE}-${LATEST}.changed.md" +FRAGMENT="changelog.d/changed/update-${PACKAGE}-${LATEST}.md" echo "Update ${DISPLAY_NAME} to ${LATEST}." > "$FRAGMENT" # --------------------------------------------------------------------------- diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 488fe83cf..907a19094 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,15 +23,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Check for changelog fragment run: | - FRAGMENTS=$(find changelog.d -type f ! -name '.gitkeep' | wc -l) - if [ "$FRAGMENTS" -eq 0 ]; then - echo "::error::No changelog fragment found in changelog.d/" - echo "Add one with: echo 'Description.' > changelog.d/\$(git branch --show-current)..md" - echo "Types: added, changed, fixed, removed, breaking" + if ! python .github/changelog_fragments.py >/tmp/changelog-fragments.txt 2>/tmp/changelog-fragments.err; then + cat /tmp/changelog-fragments.err + echo "::error::Expected at least one valid changelog fragment under changelog.d//" + echo "Add one with: echo 'Description.' > changelog.d//\$(git branch --show-current).md" + echo "Types: breaking, added, changed, fixed, removed" exit 1 fi + cat /tmp/changelog-fragments.txt test_container_builds: name: Docker runs-on: ubuntu-latest From 62a6096eceb9203099aa2e4dc6553e8e8ee17ee0 Mon Sep 17 00:00:00 2001 From: Anthony Volk Date: Mon, 13 Apr 2026 20:00:45 +0200 Subject: [PATCH 4/4] Use Towncrier's built-in changelog check --- .github/bump_version.py | 7 ++- .github/changelog_fragments.py | 82 ------------------------------- .github/scripts/update-package.sh | 2 +- .github/workflows/pr.yml | 14 ++---- changelog.d/3394.fixed.md | 1 + changelog.d/fixed/3394.md | 1 - 6 files changed, 10 insertions(+), 97 deletions(-) delete mode 100644 .github/changelog_fragments.py create mode 100644 changelog.d/3394.fixed.md delete mode 100644 changelog.d/fixed/3394.md diff --git a/.github/bump_version.py b/.github/bump_version.py index a301d9d27..779a82e38 100644 --- a/.github/bump_version.py +++ b/.github/bump_version.py @@ -4,8 +4,6 @@ import sys from pathlib import Path -from changelog_fragments import iter_valid_fragments, load_towncrier_config - def get_current_version(pyproject_path: Path) -> str: text = pyproject_path.read_text() @@ -20,8 +18,9 @@ def get_current_version(pyproject_path: Path) -> str: def infer_bump(changelog_dir: Path) -> str: - config = load_towncrier_config(changelog_dir.parent) - fragments = iter_valid_fragments(config) + fragments = [ + f for f in changelog_dir.iterdir() if f.is_file() and f.name != ".gitkeep" + ] if not fragments: print("No changelog fragments found", file=sys.stderr) sys.exit(1) diff --git a/.github/changelog_fragments.py b/.github/changelog_fragments.py deleted file mode 100644 index be0665b40..000000000 --- a/.github/changelog_fragments.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Helpers for locating Towncrier changelog fragments.""" - -from __future__ import annotations - -import sys -import tomllib -from dataclasses import dataclass -from pathlib import Path - - -@dataclass(frozen=True) -class TowncrierConfig: - root: Path - directory: Path - types: tuple[str, ...] - - -def load_towncrier_config(root: Path) -> TowncrierConfig: - with (root / "pyproject.toml").open("rb") as file: - pyproject = tomllib.load(file) - - towncrier = pyproject["tool"]["towncrier"] - fragment_types = tuple( - entry["directory"] - for entry in towncrier.get("type", []) - if entry.get("directory") - ) - return TowncrierConfig( - root=root, - directory=root / towncrier["directory"], - types=fragment_types, - ) - - -def iter_valid_fragments(config: TowncrierConfig) -> list[Path]: - fragments: list[Path] = [] - for fragment_type in config.types: - fragment_dir = config.directory / fragment_type - if not fragment_dir.exists(): - continue - for path in sorted(fragment_dir.rglob("*")): - if path.is_file() and path.name != ".gitkeep": - fragments.append(path) - return fragments - - -def iter_invalid_fragments(config: TowncrierConfig) -> list[Path]: - fragments: list[Path] = [] - allowed_roots = { - (config.directory / fragment_type).resolve() for fragment_type in config.types - } - for path in sorted(config.directory.rglob("*")): - if not path.is_file() or path.name == ".gitkeep": - continue - if path.parent.resolve() not in allowed_roots: - fragments.append(path) - return fragments - - -def main() -> int: - root = Path(__file__).resolve().parent.parent - config = load_towncrier_config(root) - valid = iter_valid_fragments(config) - invalid = iter_invalid_fragments(config) - - if invalid: - print("Invalid changelog fragments:", file=sys.stderr) - for path in invalid: - print(path.relative_to(root), file=sys.stderr) - return 1 - - if not valid: - print("No valid changelog fragments found", file=sys.stderr) - return 1 - - for path in valid: - print(path.relative_to(root)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.github/scripts/update-package.sh b/.github/scripts/update-package.sh index 2f8ddced2..5800669e4 100755 --- a/.github/scripts/update-package.sh +++ b/.github/scripts/update-package.sh @@ -75,7 +75,7 @@ fi # --------------------------------------------------------------------------- # 6. Create changelog fragment (required by PR CI) # --------------------------------------------------------------------------- -FRAGMENT="changelog.d/changed/update-${PACKAGE}-${LATEST}.md" +FRAGMENT="changelog.d/update-${PACKAGE}-${LATEST}.changed.md" echo "Update ${DISPLAY_NAME} to ${LATEST}." > "$FRAGMENT" # --------------------------------------------------------------------------- diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 907a19094..472f429f7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,20 +23,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" + - name: Setup uv + uses: astral-sh/setup-uv@v5 - name: Check for changelog fragment - run: | - if ! python .github/changelog_fragments.py >/tmp/changelog-fragments.txt 2>/tmp/changelog-fragments.err; then - cat /tmp/changelog-fragments.err - echo "::error::Expected at least one valid changelog fragment under changelog.d//" - echo "Add one with: echo 'Description.' > changelog.d//\$(git branch --show-current).md" - echo "Types: breaking, added, changed, fixed, removed" - exit 1 - fi - cat /tmp/changelog-fragments.txt + run: uv run --with "towncrier>=24.8.0" towncrier check --compare-with origin/master test_container_builds: name: Docker runs-on: ubuntu-latest diff --git a/changelog.d/3394.fixed.md b/changelog.d/3394.fixed.md new file mode 100644 index 000000000..d554c49d1 --- /dev/null +++ b/changelog.d/3394.fixed.md @@ -0,0 +1 @@ +Attach PolicyEngine bundle metadata to economy results. diff --git a/changelog.d/fixed/3394.md b/changelog.d/fixed/3394.md deleted file mode 100644 index 126469ec9..000000000 --- a/changelog.d/fixed/3394.md +++ /dev/null @@ -1 +0,0 @@ -Record resolved PolicyEngine bundle metadata from the runtime that actually executed society-wide simulations, and key reproduce/cache behavior off the resolved dataset bundle rather than caller-side defaults.