Skip to content

Commit a768d22

Browse files
committed
add local CI script to catch failures before pushing
Made-with: Cursor
1 parent b71748c commit a768d22

2 files changed

Lines changed: 142 additions & 15 deletions

File tree

ci-local.sh

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
RED='\033[0;31m'
5+
GREEN='\033[0;32m'
6+
YELLOW='\033[1;33m'
7+
CYAN='\033[0;36m'
8+
NC='\033[0m'
9+
10+
FAILED=()
11+
PASSED=()
12+
SKIPPED=()
13+
14+
run_step() {
15+
local name="$1"
16+
shift
17+
echo -e "\n${CYAN}── $name ──${NC}"
18+
echo -e "${YELLOW}$ $*${NC}"
19+
if "$@"; then
20+
PASSED+=("$name")
21+
echo -e "${GREEN}$name${NC}"
22+
else
23+
FAILED+=("$name")
24+
echo -e "${RED}$name${NC}"
25+
fi
26+
}
27+
28+
skip_step() {
29+
local name="$1"
30+
local reason="$2"
31+
SKIPPED+=("$name ($reason)")
32+
echo -e "\n${YELLOW}── $name [SKIPPED: $reason] ──${NC}"
33+
}
34+
35+
usage() {
36+
echo "Usage: $0 [rust|lint|python|all|fix]"
37+
echo ""
38+
echo "Mirrors the GitHub Actions CI workflows locally."
39+
echo ""
40+
echo " rust Rust CI: cargo check, test, fmt --check, clippy"
41+
echo " lint Lint PR: ruff check, isort --check-only"
42+
echo " python Python CI: nox test suite (current Python version)"
43+
echo " all Everything (default)"
44+
echo " fix Auto-fix: cargo fmt, ruff --fix, isort"
45+
exit 0
46+
}
47+
48+
# ── rust-CI.yml ───────────────────────────────────────────────────────────────
49+
run_rust() {
50+
echo -e "\n${CYAN}═══ Rust CI (.github/workflows/rust-CI.yml) ═══${NC}"
51+
run_step "cargo check" cargo check
52+
run_step "cargo test" cargo test
53+
run_step "cargo fmt" cargo fmt --check
54+
run_step "cargo clippy" cargo clippy
55+
}
56+
57+
# ── lint-pr.yml ───────────────────────────────────────────────────────────────
58+
run_lint() {
59+
echo -e "\n${CYAN}═══ Lint PR (.github/workflows/lint-pr.yml) ═══${NC}"
60+
61+
if command -v ruff &>/dev/null; then
62+
run_step "ruff check" ruff check .
63+
else
64+
skip_step "ruff check" "ruff not installed (pip install ruff)"
65+
fi
66+
67+
if command -v isort &>/dev/null; then
68+
run_step "isort check" isort --check-only --diff .
69+
else
70+
skip_step "isort check" "isort not installed (pip install isort)"
71+
fi
72+
}
73+
74+
# ── python-CI.yml ─────────────────────────────────────────────────────────────
75+
run_python() {
76+
echo -e "\n${CYAN}═══ Python CI (.github/workflows/python-CI.yml) ═══${NC}"
77+
local pyver
78+
pyver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
79+
80+
if command -v nox &>/dev/null; then
81+
run_step "nox (python $pyver)" nox --non-interactive --error-on-missing-interpreter -p "$pyver"
82+
else
83+
skip_step "nox tests" "nox not installed (pip install nox)"
84+
fi
85+
}
86+
87+
# ── fix mode ──────────────────────────────────────────────────────────────────
88+
run_fix() {
89+
echo -e "\n${CYAN}═══ Auto-fix ═══${NC}"
90+
run_step "cargo fmt" cargo fmt
91+
command -v ruff &>/dev/null && run_step "ruff fix" ruff check --fix . || skip_step "ruff fix" "not installed"
92+
command -v isort &>/dev/null && run_step "isort fix" isort . || skip_step "isort fix" "not installed"
93+
}
94+
95+
# ── main ──────────────────────────────────────────────────────────────────────
96+
MODE="${1:-all}"
97+
98+
case "$MODE" in
99+
rust) run_rust ;;
100+
lint) run_lint ;;
101+
python) run_python ;;
102+
fix) run_fix ;;
103+
all) run_rust; run_lint; run_python ;;
104+
-h|--help|help) usage ;;
105+
*) echo "Unknown mode: $MODE"; usage ;;
106+
esac
107+
108+
# ── summary ───────────────────────────────────────────────────────────────────
109+
echo -e "\n${CYAN}═══ Summary ═══${NC}"
110+
for s in "${PASSED[@]+"${PASSED[@]}"}"; do echo -e " ${GREEN}${NC} $s"; done
111+
for s in "${SKIPPED[@]+"${SKIPPED[@]}"}"; do echo -e " ${YELLOW}${NC} $s"; done
112+
for s in "${FAILED[@]+"${FAILED[@]}"}"; do echo -e " ${RED}${NC} $s"; done
113+
114+
if [ ${#FAILED[@]} -gt 0 ]; then
115+
echo -e "\n${RED}CI would fail: ${#FAILED[@]} check(s) failed.${NC}"
116+
exit 1
117+
else
118+
echo -e "\n${GREEN}All checks passed. Safe to push.${NC}"
119+
exit 0
120+
fi

src/runtime.rs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -228,23 +228,30 @@ where
228228
let pyres = result.into_pyobject(py).map(Bound::unbind);
229229
let resolved: PyResult<()> = match pyres {
230230
Ok(val) => {
231-
let cb = fut_ref.getattr(py, pyo3::intern!(py, "set_result"))?;
232-
let _ = event_loop_ref.call_method1(
233-
py,
234-
pyo3::intern!(py, "call_soon_threadsafe"),
235-
(PyFutureResultSetter, cb, val),
236-
);
237-
Ok(())
231+
let cb = fut_ref.getattr(py, pyo3::intern!(py, "set_result"));
232+
match cb {
233+
Ok(cb) => {
234+
let _ = event_loop_ref.call_method1(
235+
py,
236+
pyo3::intern!(py, "call_soon_threadsafe"),
237+
(PyFutureResultSetter, cb, val),
238+
);
239+
Ok(())
240+
}
241+
Err(e) => Err(e),
242+
}
238243
}
239244
Err(err) => {
240-
let cb = fut_ref.getattr(py, pyo3::intern!(py, "set_exception"))?;
241-
let val = err.into_py_any(py)?;
242-
let _ = event_loop_ref.call_method1(
243-
py,
244-
pyo3::intern!(py, "call_soon_threadsafe"),
245-
(PyFutureResultSetter, cb, val),
246-
);
247-
Ok(())
245+
(|| -> PyResult<()> {
246+
let cb = fut_ref.getattr(py, pyo3::intern!(py, "set_exception"))?;
247+
let val = err.into_py_any(py)?;
248+
let _ = event_loop_ref.call_method1(
249+
py,
250+
pyo3::intern!(py, "call_soon_threadsafe"),
251+
(PyFutureResultSetter, cb, val),
252+
);
253+
Ok(())
254+
})()
248255
}
249256
};
250257
if let Err(err) = resolved {

0 commit comments

Comments
 (0)