Skip to content
This repository was archived by the owner on Jun 14, 2026. It is now read-only.

Commit 9da1ab4

Browse files
authored
Merge pull request #18 from PolicyEngine/codex/mp-300k-release-loop
Add mp-300k artifact gates
2 parents 2a26973 + 0207fc5 commit 9da1ab4

7 files changed

Lines changed: 1860 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
name: mp-300k Artifact Gates
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
workflow_dispatch:
9+
inputs:
10+
gate_inputs_artifact:
11+
description: Optional Actions artifact name containing artifact.tar.gz and evidence JSONs.
12+
required: false
13+
default: ""
14+
type: string
15+
artifact_archive_url:
16+
description: URL to a .zip, .tar, or .tar.gz artifact bundle containing manifest.json.
17+
required: false
18+
default: ""
19+
type: string
20+
ecps_comparison_url:
21+
description: Optional URL to precomputed PE-native eCPS comparison JSON.
22+
required: false
23+
type: string
24+
runtime_smoke_url:
25+
description: Optional URL to runtime smoke benchmark JSON.
26+
required: false
27+
type: string
28+
benchmark_manifest_url:
29+
description: Optional URL to the frozen microsimulation benchmark manifest.
30+
required: false
31+
type: string
32+
target_period:
33+
description: PolicyEngine period to validate.
34+
required: false
35+
default: "2024"
36+
type: string
37+
runtime_ratio_threshold:
38+
description: Maximum candidate/baseline runtime ratio.
39+
required: false
40+
default: "1.25"
41+
type: string
42+
artifact_size_ratio_threshold:
43+
description: Maximum candidate/baseline H5 size ratio.
44+
required: false
45+
default: "2.0"
46+
type: string
47+
require_ecps_comparison:
48+
description: Keep the eCPS comparison as a blocking gate.
49+
required: false
50+
default: true
51+
type: boolean
52+
workflow_call:
53+
inputs:
54+
gate_inputs_artifact:
55+
required: false
56+
default: ""
57+
type: string
58+
artifact_archive_url:
59+
required: false
60+
default: ""
61+
type: string
62+
ecps_comparison_url:
63+
required: false
64+
type: string
65+
runtime_smoke_url:
66+
required: false
67+
type: string
68+
benchmark_manifest_url:
69+
required: false
70+
type: string
71+
target_period:
72+
required: false
73+
default: "2024"
74+
type: string
75+
runtime_ratio_threshold:
76+
required: false
77+
default: "1.25"
78+
type: string
79+
artifact_size_ratio_threshold:
80+
required: false
81+
default: "2.0"
82+
type: string
83+
require_ecps_comparison:
84+
required: false
85+
default: true
86+
type: boolean
87+
88+
permissions:
89+
actions: read
90+
contents: read
91+
92+
jobs:
93+
implementation-tests:
94+
if: github.event_name == 'pull_request' || github.event_name == 'push'
95+
runs-on: ubuntu-latest
96+
defaults:
97+
run:
98+
working-directory: microplex-us
99+
steps:
100+
- name: Check out microplex-us
101+
uses: actions/checkout@v4
102+
with:
103+
path: microplex-us
104+
105+
- name: Check out core microplex
106+
uses: actions/checkout@v4
107+
with:
108+
repository: PolicyEngine/microplex
109+
ref: main
110+
path: microplex
111+
112+
- name: Set up Python
113+
uses: actions/setup-python@v5
114+
with:
115+
python-version: "3.13"
116+
117+
- name: Set up uv
118+
uses: astral-sh/setup-uv@v6
119+
120+
- name: Test artifact gate implementation
121+
run: |
122+
uv run --python 3.13 --extra dev --with pydantic --with-editable ../microplex pytest -q \
123+
tests/pipelines/test_mp300k_artifact_gates.py \
124+
tests/pipelines/test_mp300k_gate_inputs.py
125+
126+
artifact-gates:
127+
if: github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call'
128+
runs-on: ubuntu-latest
129+
defaults:
130+
run:
131+
working-directory: microplex-us
132+
steps:
133+
- name: Check out microplex-us
134+
uses: actions/checkout@v4
135+
with:
136+
path: microplex-us
137+
138+
- name: Check out core microplex
139+
uses: actions/checkout@v4
140+
with:
141+
repository: PolicyEngine/microplex
142+
ref: main
143+
path: microplex
144+
145+
- name: Set up Python
146+
uses: actions/setup-python@v5
147+
with:
148+
python-version: "3.13"
149+
150+
- name: Set up uv
151+
uses: astral-sh/setup-uv@v6
152+
153+
- name: Download packaged gate inputs
154+
if: inputs.gate_inputs_artifact != ''
155+
uses: actions/download-artifact@v4
156+
with:
157+
name: ${{ inputs.gate_inputs_artifact }}
158+
path: gate-inputs
159+
160+
- name: Download artifact and evidence from URLs
161+
if: inputs.gate_inputs_artifact == ''
162+
run: |
163+
mkdir -p ../gate-inputs/evidence
164+
if [ -z "${{ inputs.artifact_archive_url }}" ]; then
165+
echo "Either gate_inputs_artifact or artifact_archive_url is required."
166+
exit 1
167+
fi
168+
curl --fail --location "${{ inputs.artifact_archive_url }}" --output ../gate-inputs/artifact-archive
169+
170+
if [ -n "${{ inputs.ecps_comparison_url }}" ]; then
171+
curl --fail --location "${{ inputs.ecps_comparison_url }}" --output ../gate-inputs/evidence/ecps_comparison.json
172+
fi
173+
if [ -n "${{ inputs.runtime_smoke_url }}" ]; then
174+
curl --fail --location "${{ inputs.runtime_smoke_url }}" --output ../gate-inputs/evidence/runtime_smoke.json
175+
fi
176+
if [ -n "${{ inputs.benchmark_manifest_url }}" ]; then
177+
curl --fail --location "${{ inputs.benchmark_manifest_url }}" --output ../gate-inputs/evidence/benchmark_manifest.json
178+
fi
179+
180+
- name: Normalize gate inputs
181+
run: |
182+
uv run --python 3.13 python - <<'PY'
183+
import shutil
184+
from pathlib import Path
185+
186+
root = Path("../gate-inputs")
187+
evidence_dir = root / "evidence"
188+
evidence_dir.mkdir(parents=True, exist_ok=True)
189+
190+
archive_target = root / "artifact-archive"
191+
if not archive_target.exists():
192+
archive_candidates = []
193+
for pattern in ("artifact.tar.gz", "artifact.tgz", "artifact.tar", "artifact.zip", "*.tar.gz", "*.tgz", "*.tar", "*.zip"):
194+
archive_candidates.extend(root.glob(pattern))
195+
archive_candidates = [
196+
path for path in archive_candidates if path.is_file()
197+
]
198+
if not archive_candidates:
199+
raise SystemExit(
200+
"Packaged gate inputs did not contain artifact.tar.gz, "
201+
"artifact.tgz, artifact.tar, or artifact.zip"
202+
)
203+
shutil.copyfile(archive_candidates[0], archive_target)
204+
205+
for name in ("ecps_comparison", "runtime_smoke", "benchmark_manifest"):
206+
source = root / f"{name}.json"
207+
destination = evidence_dir / f"{name}.json"
208+
if source.exists() and not destination.exists():
209+
shutil.copyfile(source, destination)
210+
PY
211+
212+
- name: Resolve artifact directory
213+
run: |
214+
uv run --python 3.13 python - <<'PY'
215+
import tarfile
216+
import zipfile
217+
from pathlib import Path
218+
219+
archive = Path("../gate-inputs/artifact-archive")
220+
extract_root = Path("../gate-inputs/artifact-root")
221+
extract_root.mkdir(parents=True, exist_ok=True)
222+
223+
if tarfile.is_tarfile(archive):
224+
with tarfile.open(archive) as handle:
225+
handle.extractall(extract_root, filter="data")
226+
elif zipfile.is_zipfile(archive):
227+
with zipfile.ZipFile(archive) as handle:
228+
for member in handle.infolist():
229+
destination = (extract_root / member.filename).resolve()
230+
if not destination.is_relative_to(extract_root.resolve()):
231+
raise SystemExit(
232+
f"zip archive member escapes artifact root: {member.filename}"
233+
)
234+
handle.extract(member, extract_root)
235+
else:
236+
raise SystemExit("artifact_archive_url must point to a tar or zip archive")
237+
238+
manifests = sorted(
239+
extract_root.rglob("manifest.json"),
240+
key=lambda path: len(path.relative_to(extract_root).parts),
241+
)
242+
if not manifests:
243+
raise SystemExit("artifact archive does not contain manifest.json")
244+
Path("../gate-inputs/artifact_dir.txt").write_text(str(manifests[0].parent.resolve()))
245+
PY
246+
247+
- name: Run artifact gates
248+
run: |
249+
artifact_dir="$(cat ../gate-inputs/artifact_dir.txt)"
250+
args=(
251+
--artifact-dir "$artifact_dir"
252+
--target-period "${{ inputs.target_period }}"
253+
--artifact-size-ratio-threshold "${{ inputs.artifact_size_ratio_threshold }}"
254+
--runtime-ratio-threshold "${{ inputs.runtime_ratio_threshold }}"
255+
--output-json ../gate-inputs/mp300k_artifact_gates.json
256+
--no-update-manifest
257+
)
258+
259+
if [ -f ../gate-inputs/evidence/ecps_comparison.json ]; then
260+
args+=(--ecps-comparison-json ../gate-inputs/evidence/ecps_comparison.json)
261+
else
262+
args+=(--skip-ecps-computation)
263+
fi
264+
if [ -f ../gate-inputs/evidence/runtime_smoke.json ]; then
265+
args+=(--runtime-smoke-json ../gate-inputs/evidence/runtime_smoke.json)
266+
fi
267+
if [ -f ../gate-inputs/evidence/benchmark_manifest.json ]; then
268+
args+=(--benchmark-manifest ../gate-inputs/evidence/benchmark_manifest.json)
269+
fi
270+
if [ "${{ inputs.require_ecps_comparison }}" != "true" ]; then
271+
args+=(--no-require-ecps-comparison)
272+
fi
273+
274+
uv run --python 3.13 --extra dev --with pydantic --with-editable ../microplex \
275+
microplex-us-mp300k-artifact-gates "${args[@]}"
276+
277+
- name: Upload gate report
278+
if: always()
279+
uses: actions/upload-artifact@v4
280+
with:
281+
name: mp300k-artifact-gates
282+
path: gate-inputs/mp300k_artifact_gates.json

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ requires-python = ">=3.13"
1515
dependencies = [
1616
"microplex[calibrate] @ git+https://github.com/PolicyEngine/microplex.git@1e0627182f9df40aacd7043c96956c2895bf9d30",
1717
"duckdb>=1.2",
18+
"h5py>=3.10",
1819
"requests>=2.31",
1920
]
2021

@@ -45,6 +46,8 @@ microplex-us-build-aca-ptc-multipliers = "microplex_us.targets.aca_ptc:main"
4546
microplex-us-backfill-pe-native-audit = "microplex_us.pipelines.backfill_pe_native_audit:main"
4647
microplex-us-backfill-pe-native-scores = "microplex_us.pipelines.backfill_pe_native_scores:main"
4748
microplex-us-check-site-snapshot = "microplex_us.pipelines.check_site_snapshot:main"
49+
microplex-us-mp300k-artifact-gates = "microplex_us.pipelines.mp300k_artifact_gates:main"
50+
microplex-us-package-mp300k-gate-inputs = "microplex_us.pipelines.mp300k_gate_inputs:main"
4851
microplex-us-pe-dataset-readiness = "microplex_us.pipelines.pe_us_dataset_readiness:main"
4952
microplex-us-dashboard = "microplex_us.pipelines.dashboard:main"
5053
microplex-us-pe-native-calibration-benchmark = "microplex_us.pipelines.pe_native_calibration_benchmark:main"

0 commit comments

Comments
 (0)