Skip to content

Commit 2250b00

Browse files
Apply deterministic postprocessing CLI exit fix
1 parent d594717 commit 2250b00

1 file changed

Lines changed: 281 additions & 0 deletions

File tree

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
name: Apply deterministic postprocessing CLI exit fix
2+
3+
on:
4+
push:
5+
branches:
6+
- agent/release-readiness-audit
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
apply-fix:
13+
if: github.actor != 'github-actions[bot]'
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
ref: agent/release-readiness-audit
19+
fetch-depth: 0
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.11'
23+
- name: Install package and test tooling
24+
run: |
25+
python -m pip install --upgrade pip
26+
python -m pip install --no-cache-dir -e . pytest
27+
- name: Apply deterministic CLI and wrapper fix
28+
shell: bash
29+
run: |
30+
set -euo pipefail
31+
python - <<'PY'
32+
from pathlib import Path
33+
34+
cli = Path('src/wwgpt/cli.py')
35+
text = cli.read_text()
36+
seed_marker = '''def _seeds(s: str | None) -> list[int] | None:
37+
return None if not s else [int(x) for x in s.split(',') if x]
38+
39+
40+
'''
41+
seed_replacement = '''def _seeds(s: str | None) -> list[int] | None:
42+
return None if not s else [int(x) for x in s.split(',') if x]
43+
44+
45+
def _exit_after_flush(status: int = 0) -> None:
46+
"""Exit a completed CLI command without waiting on library worker threads."""
47+
sys.stderr.flush()
48+
sys.stdout.flush()
49+
os._exit(status)
50+
51+
52+
'''
53+
if 'def _exit_after_flush(status: int = 0)' not in text:
54+
if seed_marker not in text:
55+
raise SystemExit('CLI seed helper marker not found')
56+
text = text.replace(seed_marker, seed_replacement, 1)
57+
old_analyze = ' elif args.cmd=="analyze-results": print(analyze_results(args.results_root, args.analysis_plan))\n'
58+
new_analyze = ''' elif args.cmd=="analyze-results":
59+
print(analyze_results(args.results_root, args.analysis_plan), flush=True)
60+
_exit_after_flush(0)
61+
'''
62+
if old_analyze in text:
63+
text = text.replace(old_analyze, new_analyze, 1)
64+
elif new_analyze.strip() not in text:
65+
raise SystemExit('analyze-results CLI branch not found')
66+
old_prepare = ''' sys.stderr.flush()
67+
sys.stdout.flush()
68+
# Some streaming dataset backends can leave non-daemon workers alive after all artifacts
69+
# have been written. Exit the CLI process deterministically so shell wrappers can finish.
70+
os._exit(0)
71+
'''
72+
new_prepare = ''' # Some data and analysis backends can leave non-daemon workers alive after all
73+
# artifacts have been written. Exit deterministically so shell wrappers finish.
74+
_exit_after_flush(0)
75+
'''
76+
if old_prepare in text:
77+
text = text.replace(old_prepare, new_prepare, 1)
78+
old_report = ''' elif args.cmd=="generate-reproducibility-report":
79+
print(
80+
write_reproducibility_report(
81+
args.experiment_root,
82+
strict=args.strict,
83+
analysis_plan=args.analysis_plan,
84+
)
85+
)
86+
'''
87+
new_report = ''' elif args.cmd=="generate-reproducibility-report":
88+
print(
89+
write_reproducibility_report(
90+
args.experiment_root,
91+
strict=args.strict,
92+
analysis_plan=args.analysis_plan,
93+
),
94+
flush=True,
95+
)
96+
_exit_after_flush(0)
97+
'''
98+
if old_report in text:
99+
text = text.replace(old_report, new_report, 1)
100+
elif new_report.strip() not in text:
101+
raise SystemExit('reproducibility CLI branch not found')
102+
cli.write_text(text)
103+
104+
release_test = Path('tests/test_release_readiness_audit.py')
105+
tests = release_test.read_text()
106+
test_block = '''
107+
108+
def test_postprocessing_cli_exits_after_flushing_completed_artifacts() -> None:
109+
source = Path("src/wwgpt/cli.py").read_text()
110+
assert "def _exit_after_flush(status: int = 0)" in source
111+
assert 'elif args.cmd=="analyze-results":\\n print(' in source
112+
assert 'elif args.cmd=="generate-reproducibility-report":' in source
113+
assert source.count("_exit_after_flush(0)") >= 3
114+
'''
115+
if 'test_postprocessing_cli_exits_after_flushing_completed_artifacts' not in tests:
116+
release_test.write_text(tests + test_block)
117+
118+
local_test = Path('tests/test_local_level_scripts.py')
119+
tests = local_test.read_text()
120+
local_block = '''
121+
122+
def test_bounded_level012_runner_normalizes_report_output_and_emits_diagnostics() -> None:
123+
source = Path("scripts/run_bounded_level012_acceptance.sh").read_text()
124+
assert "set -Eeuo pipefail" in source
125+
assert "trap on_error ERR" in source
126+
assert "extract_report_path" in source
127+
assert "release_reproducibility_first.log" in source
128+
assert "release_reproducibility_second.log" in source
129+
assert 'FIRST_REPORT="$(extract_report_path "$FIRST_LOG")"' in source
130+
assert 'SECOND_REPORT="$(extract_report_path "$SECOND_LOG")"' in source
131+
assert 'FIRST_REPORT="$(wwgpt generate-reproducibility-report' not in source
132+
'''
133+
if 'test_bounded_level012_runner_normalizes_report_output_and_emits_diagnostics' not in tests:
134+
local_test.write_text(tests + local_block)
135+
PY
136+
cat > scripts/run_bounded_level012_acceptance.sh <<'EOF'
137+
#!/usr/bin/env bash
138+
set -Eeuo pipefail
139+
140+
if [ "$#" -ne 1 ]; then
141+
echo "usage: $0 RESULTS_ROOT" >&2
142+
exit 2
143+
fi
144+
145+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
146+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
147+
cd "$REPO_ROOT"
148+
149+
RESULTS_ROOT="$(python - "$1" <<'PY'
150+
from pathlib import Path
151+
import sys
152+
print(Path(sys.argv[1]).expanduser().resolve())
153+
PY
154+
)"
155+
PLAN="$RESULTS_ROOT/release_acceptance_plan.yaml"
156+
157+
on_error() {
158+
local status=$?
159+
local line=${BASH_LINENO[0]:-unknown}
160+
local command=${BASH_COMMAND:-unknown}
161+
echo "[release-acceptance] failure status=$status line=$line command=$command" >&2
162+
for log in \
163+
"$RESULTS_ROOT/release_health.log" \
164+
"$RESULTS_ROOT/release_analysis.log" \
165+
"$RESULTS_ROOT/release_audit.log" \
166+
"$RESULTS_ROOT/release_reproducibility_first.log" \
167+
"$RESULTS_ROOT/release_reproducibility_second.log"; do
168+
if [ -f "$log" ]; then
169+
echo "[release-acceptance] tail $log" >&2
170+
tail -n 40 "$log" >&2 || true
171+
fi
172+
done
173+
exit "$status"
174+
}
175+
trap on_error ERR
176+
177+
extract_report_path() {
178+
python - "$1" <<'PY'
179+
from pathlib import Path
180+
import sys
181+
182+
log = Path(sys.argv[1])
183+
lines = [line.strip() for line in log.read_text().splitlines() if line.strip()]
184+
candidates = [line for line in lines if line.endswith(".pdf")]
185+
if not candidates:
186+
raise SystemExit(f"no PDF report path found in {log}")
187+
report = Path(candidates[-1]).expanduser()
188+
if not report.is_absolute():
189+
report = (Path.cwd() / report).resolve()
190+
else:
191+
report = report.resolve()
192+
if not report.is_file():
193+
raise SystemExit(f"reported PDF does not exist: {report}")
194+
print(report)
195+
PY
196+
}
197+
198+
python scripts/run_bounded_level012_acceptance.py --results-root "$RESULTS_ROOT"
199+
wwgpt check-health --experiment-root "$RESULTS_ROOT" >"$RESULTS_ROOT/release_health.log"
200+
wwgpt analyze-results "$RESULTS_ROOT" --analysis-plan "$PLAN" >"$RESULTS_ROOT/release_analysis.log"
201+
wwgpt audit-experiment --experiment-root "$RESULTS_ROOT" >"$RESULTS_ROOT/release_audit.log"
202+
203+
FIRST_LOG="$RESULTS_ROOT/release_reproducibility_first.log"
204+
SECOND_LOG="$RESULTS_ROOT/release_reproducibility_second.log"
205+
wwgpt generate-reproducibility-report \
206+
--experiment-root "$RESULTS_ROOT" \
207+
--analysis-plan "$PLAN" \
208+
--strict >"$FIRST_LOG"
209+
wwgpt generate-reproducibility-report \
210+
--experiment-root "$RESULTS_ROOT" \
211+
--analysis-plan "$PLAN" \
212+
--strict >"$SECOND_LOG"
213+
FIRST_REPORT="$(extract_report_path "$FIRST_LOG")"
214+
SECOND_REPORT="$(extract_report_path "$SECOND_LOG")"
215+
if [ "$FIRST_REPORT" != "$SECOND_REPORT" ]; then
216+
echo "reproducibility report path changed across identical reruns" >&2
217+
echo "first: $FIRST_REPORT" >&2
218+
echo "second: $SECOND_REPORT" >&2
219+
exit 1
220+
fi
221+
222+
python - "$RESULTS_ROOT" <<'PY'
223+
from pathlib import Path
224+
import json
225+
import sys
226+
import pandas as pd
227+
228+
root = Path(sys.argv[1]).resolve()
229+
analysis = root / "analysis"
230+
complete = sorted(root.rglob("run_complete.json"))
231+
if len(complete) != 6:
232+
raise SystemExit(f"expected six complete runs, found {len(complete)}")
233+
inventory = pd.read_csv(analysis / "runs_manifest.csv")
234+
if len(inventory) != 6:
235+
raise SystemExit(f"expected six analyzed arms, found {len(inventory)}")
236+
if set(pd.to_numeric(inventory["level"], errors="raise").astype(int)) != {0, 1, 2}:
237+
raise SystemExit("analysis did not preserve Levels 0, 1, and 2")
238+
required = [
239+
analysis / "analysis_eligibility.json",
240+
analysis / "acceleration_by_seed.csv",
241+
analysis / "integrity_summary.json",
242+
analysis / "reproducibility_report.json",
243+
analysis / "reproducibility_report.pdf",
244+
analysis / "cross_level_run_inventory.csv",
245+
]
246+
missing = [str(path) for path in required if not path.is_file()]
247+
if missing:
248+
raise SystemExit(f"missing release artifacts: {missing}")
249+
eligibility = json.loads((analysis / "analysis_eligibility.json").read_text())
250+
if not eligibility.get("eligible"):
251+
raise SystemExit(f"analysis ineligible: {eligibility}")
252+
print("LEVEL_0_1_2_RELEASE_ACCEPTANCE_PASS")
253+
PY
254+
EOF
255+
chmod +x scripts/run_bounded_level012_acceptance.sh
256+
rm -f .github/workflows/apply-linux-cli-exit-fix.yml
257+
- name: Validate focused and clean-process paths
258+
env:
259+
MPLBACKEND: Agg
260+
run: |
261+
set -euo pipefail
262+
python -m compileall -q src tests scripts
263+
bash -n scripts/*.sh
264+
pytest -q \
265+
tests/test_release_readiness_audit.py \
266+
tests/test_local_level_scripts.py \
267+
tests/test_acceleration_analysis.py \
268+
tests/test_level012_full_pipeline.py
269+
timeout 300 ./scripts/run_bounded_level012_acceptance.sh /tmp/linux-cli-exit-acceptance
270+
- name: Commit validated fix
271+
shell: bash
272+
run: |
273+
set -euo pipefail
274+
git config user.name github-actions[bot]
275+
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
276+
git add src/wwgpt/cli.py scripts/run_bounded_level012_acceptance.sh \
277+
tests/test_release_readiness_audit.py tests/test_local_level_scripts.py \
278+
.github/workflows/apply-linux-cli-exit-fix.yml
279+
git diff --cached --check
280+
git commit -m 'Exit completed postprocessing commands deterministically'
281+
git push origin HEAD:agent/release-readiness-audit

0 commit comments

Comments
 (0)