Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
00699d9
ci(release): adopt self-hosted SIF release pipeline (Spartan CPU) (#339)
ywatanabe1989 Jun 22, 2026
b66fa1b
chore(deps): complete umbrella pin reconcile to latest + add scitex-p…
ywatanabe1989 Jun 27, 2026
18d9c58
ci(run-in-sif): restore peer-standalones install + test quarantine (#…
ywatanabe1989 Jun 28, 2026
b86056c
chore(deps): cut 2.30.4 — sweep umbrella pins to PyPI latest (#342)
ywatanabe1989 Jun 29, 2026
7cc8c78
test(io): import scitex.io.bundle so attribute assertions are xdist-d…
ywatanabe1989 Jun 29, 2026
2f2cfa4
chore(deps): cut 2.30.5 — sweep umbrella pins to PyPI latest (#344)
ywatanabe1989 Jun 30, 2026
ac3d6ae
chore(umbrella): scitex 2.30.6 — re-pin members to current-published …
ywatanabe1989 Jul 6, 2026
f4343f1
fix(mcp): bounded per-peer mount so one hung peer can't darken the ag…
ywatanabe1989 Jul 6, 2026
b03d561
chore(umbrella): scitex 2.30.7 — clew 0.16.0→0.17.0 alignment + harde…
ywatanabe1989 Jul 6, 2026
ac66b1d
fix(mcp): aggregator mount policy (skip orochi+types) + cold-start CI…
ywatanabe1989 Jul 7, 2026
177102e
chore(release): scitex 2.30.8 — pin scitex-dev 0.28.0 + scitex-sessio…
ywatanabe1989 Jul 7, 2026
32b3903
fix(mcp): sync _LOCAL_UNMOUNTABLE fallback to skip scitex-str (#351)
ywatanabe1989 Jul 7, 2026
80c9883
fix(cli): umbrella CLI standardization slice 5 — mounts, dedupe, comp…
ywatanabe1989 Jul 7, 2026
1e45ab4
chore(ci): migrate to scitex-ai/.github reusable workflows
ywatanabe1989 Jul 9, 2026
fef9420
Merge pull request #355 from scitex-ai/chore/reusable-workflow-migration
ywatanabe1989 Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

export SCITEX_TODO_AGENT_ID="scitex-python"
export SCITEX_TODO_TASKS_YAML_SHARED="$HOME/.scitex/todo/tasks.yaml"
69 changes: 69 additions & 0 deletions .github/ci/build-in-sif.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Runs INSIDE the reused scitex-ci SIF (apptainer exec — invoked via
# exec-in-sif.sh). Builds scitex's wheel + sdist into ./dist/.
#
# WHY build in the SIF: the self-hosted Spartan runner has no Python on the
# bare node (the whole reason the old `actions/setup-python@v5` step failed:
# "version 3.x not found for this OS"). The SIF bakes python 3.11/3.12/3.13 +
# pip + uv at /opt/venv-<ver>, exactly like the working pytest-matrix CI.
#
# `python -m build` needs the `build` frontend, which is NOT baked in the SIF
# (only scitex-dev[all,dev] deps are). Mirror run-in-sif.sh: install `build`
# into a writable --target on node-local /tmp and put it on PYTHONPATH. The
# SIF's /opt/venv-* are root-owned + RO and the compute-node HOME is RO inside
# the container, so a normal install fails Permission denied — a --target on
# writable scratch sidesteps both.
#
# Fail-loud (operator directive): a missing interpreter or a failed build is a
# HARD error, never a silent fallback.
set -euo pipefail

V="${1:-3.12}"
VENV="/opt/venv-$V"
PY="$VENV/bin/python"
test -x "$PY" || {
echo "::error::baked python missing in $VENV — rebuild the SIF: scitex-container apptainer build ci-cpu"
exit 1
}

export LC_ALL=C.UTF-8 LANG=C.UTF-8

# Writable scratch (the runner's TMPDIR=~/.cache/tmp is a host path that does
# NOT resolve inside the container). Node-local /tmp is writable + ephemeral.
TMPDIR="/tmp/build-scitex-${GITHUB_RUN_ID:-0}-${GITHUB_RUN_ATTEMPT:-0}-$V"
export TMPDIR
rm -rf "$TMPDIR"
mkdir -p "$TMPDIR/site" "$TMPDIR/uv-cache"

# The compute-node $HOME is RO inside the container — point every cache the
# installer might touch at the writable scratch (else uv/pip die creating
# ~/.cache).
export UV_CACHE_DIR="$TMPDIR/uv-cache"
export XDG_CACHE_HOME="$TMPDIR"
export PIP_CACHE_DIR="$TMPDIR/pip-cache"

# A VIRTUAL_ENV leaked from the runner profile (~/.env-3.11) is a broken
# symlink in here; unset it so no tool follows it.
unset VIRTUAL_ENV || true

export PATH="$VENV/bin:$PATH"
echo "build: py=$("$PY" -V) target=$TMPDIR/site"

# Install the PEP 517 build frontend into the writable target (uv fast path,
# pip safety net), then build with it. Clean dist/ first so only the freshly
# built artifacts are uploaded.
uv pip install --python "$PY" --target="$TMPDIR/site" build ||
"$PY" -m pip install --target="$TMPDIR/site" build

export PYTHONPATH="$TMPDIR/site${PYTHONPATH:+:$PYTHONPATH}"

rm -rf dist
"$PY" -m build --outdir dist

echo "=== built artifacts ==="
ls -l dist
# fail-loud: refuse to continue the pipeline with an empty dist/.
test -n "$(ls -A dist 2>/dev/null)" || {
echo "::error::python -m build produced no artifacts in dist/"
exit 1
}
51 changes: 51 additions & 0 deletions .github/ci/exec-in-sif.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Outer apptainer-exec wrapper for scitex-tex's self-hosted (Spartan) CI.
#
# Runs ON THE RUNNER (outside the SIF). Resolves the apptainer shim + SIF image
# from the repo Actions Variables, then `apptainer exec`s the SIF and hands off
# to an INNER script (run inside the container). Keeps every workflow job's YAML
# down to one line — `bash .github/ci/exec-in-sif.sh <inner-script> [args...]` —
# and concentrates all the HPC/SIF plumbing (shim PATH, ~-expansion, scratch,
# binds) in one version-controlled place.
#
# Required env (set by the workflow from repo Actions Variables):
# SCITEX_CI_APPTAINER path to the apptainer shim (e.g. ~/.env-3.11/bin/apptainer)
# SCITEX_CI_SIF path to the CI SIF image (e.g. ~/.scitex/dev/containers/ci-cpu.sif)
#
# Usage:
# bash .github/ci/exec-in-sif.sh run-in-sif.sh 3.12
#
# Fail-loud (operator directive): a missing shim or SIF is a HARD error — never
# a silent fallback to a bare-runner install.
set -euo pipefail

INNER="${1:?inner script name required (relative to .github/ci/)}"
shift || true

# The runner's job shell is --noprofile --norc (no Lmod), so the apptainer shim
# must be put on PATH explicitly; it execs the real Apptainer binary directly.
# ~-expand the Actions-Variable paths: a quoted "~/…" is NOT tilde-expanded by
# the shell, so substitute a leading ~ with $HOME ourselves.
APPTAINER="${SCITEX_CI_APPTAINER:?SCITEX_CI_APPTAINER not set (repo Actions Variable)}"
SIF="${SCITEX_CI_SIF:?SCITEX_CI_SIF not set (repo Actions Variable)}"
APPTAINER="${APPTAINER/#\~/$HOME}"
SIF="${SIF/#\~/$HOME}"
export PATH="$HOME/.env-3.11/bin:$PATH"

[ -x "$APPTAINER" ] || {
echo "::error::apptainer shim not executable at $APPTAINER"
exit 1
}
[ -f "$SIF" ] || {
echo "::error::CI SIF missing at $SIF — rebuild it: scitex-container apptainer build ci-cpu"
exit 1
}

# apptainer scratch on the shared FS — keeps HOME clean.
export APPTAINER_TMPDIR="/data/gpfs/projects/punim0264/ywatanabe/ci/apptainer-tmp"
mkdir -p "$APPTAINER_TMPDIR"

# --bind punim0264: $HOME/.scitex is a symlink into punim0264; bind it so the
# symlink resolves inside the container. --pwd "$PWD" keeps the checkout as cwd.
exec "$APPTAINER" exec --pwd "$PWD" --bind /data/gpfs/projects/punim0264 \
"$SIF" bash ".github/ci/$INNER" "$@"
109 changes: 109 additions & 0 deletions .github/ci/publish-in-sif.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Runs INSIDE the reused scitex-ci SIF (apptainer exec — invoked via
# exec-in-sif.sh). Publishes ./dist/* to PyPI via MANUAL OIDC Trusted
# Publishing, then twine upload.
#
# WHY manual OIDC (not pypa/gh-action-pypi-publish): that action is a Docker
# container action. Self-hosted Spartan compute nodes have NO Docker, so the
# action cannot run there. PyPI Trusted Publishing is just an OIDC token
# exchange over plain HTTPS, so we do it by hand:
#
# 1. Ask the GitHub Actions OIDC provider for a JWT with audience=pypi,
# using the per-job ACTIONS_ID_TOKEN_REQUEST_{TOKEN,URL} env vars (present
# because the publish job declares `permissions: id-token: write`).
# apptainer exec (no --cleanenv) passes those host env vars into the SIF.
# 2. Exchange that JWT at PyPI's mint-token endpoint for a short-lived,
# scope-limited PyPI API token.
# 3. twine upload dist/* with TWINE_USERNAME=__token__ and that minted token.
#
# This requires a Trusted Publisher to be configured on PyPI for
# (project=scitex, owner=ywatanabe1989, repo=scitex-python,
# workflow=pypi-publish-and-github-release-on-tag.yml). It already is — the
# previous releases published via the Docker action under the same trusted
# publisher; only the *client* changes here, not PyPI's trust config.
#
# curl, python and (after a --target install) twine all live in the SIF.
#
# Fail-loud (operator directive): every step asserts non-empty output and
# `set -euo pipefail`; any failure is a HARD error with the exact cause, never
# a silent skip.
set -euo pipefail

V="${1:-3.12}"
VENV="/opt/venv-$V"
PY="$VENV/bin/python"
test -x "$PY" || {
echo "::error::baked python missing in $VENV — rebuild the SIF: scitex-container apptainer build ci-cpu"
exit 1
}

export LC_ALL=C.UTF-8 LANG=C.UTF-8

# dist/ must already hold the artifacts (downloaded by the publish job before
# this script runs). Fail loud if empty.
if [ ! -d dist ] || [ -z "$(ls -A dist 2>/dev/null)" ]; then
echo "::error::dist/ is empty — nothing to publish (download the build artifact first)"
exit 1
fi
echo "=== dist to publish ==="
ls -l dist

# --- writable scratch (compute-node HOME is RO inside the container) ---
TMPDIR="/tmp/publish-scitex-${GITHUB_RUN_ID:-0}-${GITHUB_RUN_ATTEMPT:-0}-$V"
export TMPDIR
rm -rf "$TMPDIR"
mkdir -p "$TMPDIR/site" "$TMPDIR/uv-cache"
export UV_CACHE_DIR="$TMPDIR/uv-cache"
export XDG_CACHE_HOME="$TMPDIR"
export PIP_CACHE_DIR="$TMPDIR/pip-cache"
unset VIRTUAL_ENV || true
export PATH="$VENV/bin:$PATH"

# --- step 1: request the OIDC JWT (audience=pypi) from GitHub ---
: "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:?ACTIONS_ID_TOKEN_REQUEST_TOKEN not set — the publish job needs 'permissions: id-token: write'}"
: "${ACTIONS_ID_TOKEN_REQUEST_URL:?ACTIONS_ID_TOKEN_REQUEST_URL not set — the publish job needs 'permissions: id-token: write'}"

echo "=== minting OIDC JWT (audience=pypi) ==="
JWT="$(curl -fsS \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=pypi" |
"$PY" -c 'import sys,json; print(json.load(sys.stdin)["value"])')"
test -n "$JWT" || {
echo "::error::OIDC JWT request returned an empty token"
exit 1
}
echo "OIDC JWT obtained (length=${#JWT})"

# --- step 2: exchange the JWT for a short-lived PyPI API token ---
echo "=== exchanging JWT at PyPI mint-token endpoint ==="
MINT_RESP="$(curl -sS -X POST https://pypi.org/_/oidc/mint-token \
-d "{\"token\":\"${JWT}\"}")"
MINTED="$(printf '%s' "$MINT_RESP" |
"$PY" -c 'import sys,json; d=json.load(sys.stdin); print(d.get("token",""))')"
if [ -z "$MINTED" ]; then
# Surface PyPI's error body VERBATIM (the JWT is NOT echoed) so a trust
# misconfiguration is diagnosable — this is the decisive failure mode for
# the fleet. Print the raw response unconditionally (most reliable on an
# error path); pretty-print is best-effort on top.
echo "::error::PyPI mint-token returned no token."
echo "--- PyPI mint-token response body (raw) ---"
printf '%s\n' "$MINT_RESP"
echo "--- (pretty, best-effort) ---"
printf '%s' "$MINT_RESP" |
"$PY" -c 'import sys,json; print(json.dumps(json.load(sys.stdin), indent=2))' \
2>/dev/null || true
exit 1
fi
echo "PyPI token minted (length=${#MINTED})"

# --- step 3: install twine into the writable target, then upload ---
echo "=== installing twine (--target) ==="
uv pip install --python "$PY" --target="$TMPDIR/site" twine ||
"$PY" -m pip install --target="$TMPDIR/site" twine
export PYTHONPATH="$TMPDIR/site${PYTHONPATH:+:$PYTHONPATH}"

echo "=== twine upload dist/* ==="
TWINE_USERNAME="__token__" TWINE_PASSWORD="$MINTED" \
"$PY" -m twine upload --non-interactive --disable-progress-bar dist/*

echo "PUBLISH-OK: scitex dist/* uploaded to PyPI via manual OIDC trusted publishing"
153 changes: 153 additions & 0 deletions .github/ci/run-in-sif.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# Runs INSIDE the reused scitex-ci SIF (apptainer exec). $1 = python version.
#
# WHY a layered install (not the bare PYTHONPATH=src trick scitex-dev uses):
# the shared ci-cpu.sif bakes scitex-dev[all,dev] DEPS, NOT scitex's —
# matplotlib / graphviz / seaborn / django / Pillow / networkx / playwright /
# pytesseract / scitex-app / scitex-ui are absent from the SIF. So we install
# THIS checkout + its [all,dev] extras (WITH dependency resolution) into a
# writable --target dir and prepend that on PYTHONPATH. The SIF still supplies
# the heavy shared base (pip/uv, the python interpreters, scitex-dev's deps),
# so only scitex's own thin dep set is fetched per run.
#
# --target (not a plain `-e .`): the SIF's /opt/venv-* are root-owned + RO and
# the HPC compute-node HOME is RO inside the container, so a normal site install
# fails Permission denied. A writable target on node-local /tmp sidesteps both.
#
# Fail-loud: a missing interpreter or a failed install is a hard error.
set -euo pipefail

V="${1:?python version arg required (3.11/3.12/3.13)}"
VENV="/opt/venv-$V"
test -x "$VENV/bin/python" || {
echo "::error::baked python missing in $VENV — rebuild the SIF: scitex-container apptainer build ci-cpu"
exit 1
}

export LC_ALL=C.UTF-8 LANG=C.UTF-8

# Real writable scratch. The runner profile exports TMPDIR=~/.cache/tmp, a host
# path that does NOT resolve inside the container; tests (tmp_path) and the
# install target both need a working, writable tmp. Node-local /tmp is writable
# + ephemeral and per-version-isolated so concurrent matrix legs don't collide.
export TMPDIR="/tmp/ci-scitex-${GITHUB_RUN_ID:-0}-${GITHUB_RUN_ATTEMPT:-0}-$V"
rm -rf "$TMPDIR"
mkdir -p "$TMPDIR/site" "$TMPDIR/uv-cache"

# The HPC compute-node $HOME is READ-ONLY inside the container, so uv/pip cannot
# create their default caches under ~/.cache — point them at the writable
# scratch instead (else `uv pip install` dies: "failed to create directory
# ~/.cache/uv: File exists / read-only").
export UV_CACHE_DIR="$TMPDIR/uv-cache"
export XDG_CACHE_HOME="$TMPDIR"
export PIP_CACHE_DIR="$TMPDIR/pip-cache"

# Headless matplotlib — no DISPLAY on the compute node; force the Agg backend so
# pyplot imports + figure rendering in the test suite never try to open a GUI.
export MPLBACKEND=Agg

# Dedicated, stable matplotlib config/cache dir for this matrix leg. Without
# pinning it, MPLCONFIGDIR defaults to $XDG_CACHE_HOME/matplotlib which is COLD
# every CI run; the xdist workers (one per core, see below) then each cold-start
# matplotlib and RACE to build fontList.json in that shared dir.
# A partial/contended cache makes some renders fall back to a different font, so
# scitex's reproducibility tests (validate_recipe renders the SAME recipe
# twice and compares) see render1 != render2 → spurious MSE-over-threshold
# failures (e.g. TestValidateRecipe, max channel diff 255). One stable dir +
# a single warm-up below (build the cache ONCE, pre-fork) removes the race.
export MPLCONFIGDIR="$TMPDIR/mpl"
mkdir -p "$MPLCONFIGDIR"

# A VIRTUAL_ENV leaked from the runner profile (~/.env-3.11) is a broken symlink
# in here; unset it so no tool (uv, pip) tries to follow it.
unset VIRTUAL_ENV || true

# venv bin on PATH (this matrix leg's python3 + pip); PYTHONPATH points at the
# writable target so imports + coverage use the freshly-installed checkout.
export PATH="$VENV/bin:$PATH"

echo "py=$("$VENV/bin/python" -V) target=$TMPDIR/site"

# Install scitex + its [all,dev] extras WITH deps into the writable target.
# Fallback chain mirrors scitex's historical bare-uv/pip workflow so a
# packaging hiccup in an optional extra doesn't strand CI: [all,dev] → [dev] →
# bare. uv first (fast resolver), pip as a final safety net.
uv pip install --python "$VENV/bin/python" --target="$TMPDIR/site" -e ".[all,dev]" ||
uv pip install --python "$VENV/bin/python" --target="$TMPDIR/site" -e ".[dev]" ||
uv pip install --python "$VENV/bin/python" --target="$TMPDIR/site" -e "." ||
pip install --target="$TMPDIR/site" -e ".[dev]"

# Peer standalones aliased by the umbrella (scitex_gen, scitex_dsp, ...) are NOT
# in the [all] extra, so the install above never refreshes them. Install them
# FRESH into the SAME --target so they shadow the SIF's baked (possibly stale)
# copies via the PYTHONPATH below — without this, scitex.dsp.utils imports the
# SIF's old scitex_gen (missing `to_even`) and the cross-package import test
# fails. Best-effort: an unavailable peer is importorskip'd by the identity test.
uv pip install --python "$VENV/bin/python" --target="$TMPDIR/site" \
scitex-dsp scitex-gen scitex-pd scitex-nn scitex-resource \
scitex-linalg scitex-datetime scitex-decorators \
|| echo "peer standalones partially unavailable; identity test importorskips the rest"

export PYTHONPATH="$TMPDIR/site:$PWD/src${PYTHONPATH:+:$PYTHONPATH}"

# Parallelise with pytest-xdist (baked in [dev]/[all,dev] as pytest-xdist>=3).
# scitex's suite is ~2460 tests; single-process it overran the job's old
# 30-min cap (2300 passed in ~28 min, cancelled at 96%). Each xdist worker is
# a SEPARATE PROCESS, so matplotlib's global rcParams / pyplot state and the
# scitex style-stack are naturally isolated per worker — the safe way to
# parallelise a matplotlib-heavy suite.
#
# Worker count: use ALL cores. Each matrix leg now runs on its own dedicated
# self-hosted node (one runner per node: scitex-01/02/03), so there is no
# co-tenant to yield half the box to — the old nproc//2 cap left 2x the cores
# idle. nice/ionice (below) handles the "yield to higher-priority work if the
# node is ever shared" concern instead of statically reserving half the CPUs.
# Floor 4. pyproject addopts carries `-v`; override to `-q` here — 2460 verbose
# lines x workers bloats the CI log and adds measurable overhead.
NPROC="$(nproc 2>/dev/null || echo 4)"
WORKERS=$NPROC
[ "$WORKERS" -lt 4 ] && WORKERS=4
echo "xdist workers=$WORKERS (nproc=$NPROC)"

# Warm the matplotlib font cache ONCE, single-process, before xdist forks the
# workers. This builds $MPLCONFIGDIR/fontlist-*.json a single time so every
# worker reads a complete, consistent cache instead of racing to build it
# concurrently (the source of the render1!=render2 reproducibility flakes).
# Fail-loud: if matplotlib can't even build its font cache, CI must surface it.
# matplotlib may not be a dependency of this package; only warm the
# font cache when it's importable (no-op otherwise — never fail the run
# on an optional warm-up).
if python -c "import matplotlib" 2>/dev/null; then
python -c "import matplotlib; matplotlib.use('Agg'); from matplotlib import font_manager; font_manager.fontManager; import matplotlib.pyplot as plt; f=plt.figure(); f.canvas.draw(); print('mpl font cache warmed at', matplotlib.get_cachedir())"
else
echo "matplotlib not importable — skipping font-cache warm-up (not a dep)"
fi

# Distribution: `--dist load` (per-TEST round-robin), NOT `--dist loadscope`.
# loadscope pins an entire MODULE's tests to ONE worker — and scitex's heavy
# suites are big SINGLE modules (e.g. tests/integration/test_all_plotters_*.py
# parametrize one test over all 47 plotters, ~28 s each). loadscope therefore
# ran all ~50+ cases of such a module SERIALLY on one worker (~25 min) while the
# rest idled. There are NO module/session/class-scoped fixtures in those heavy
# modules and the root conftest's autouse `_close_figures` resets pyplot state
# after EVERY test, so loadscope's "same worker per module" buys nothing here —
# it only serialized. `load` spreads the parametrized cases across ALL workers.
#
# nice -n 19 ionice -c 3: run at the lowest CPU + idle I/O priority so that if
# this node is ever shared with interactive/dev work, CI grabs otherwise-idle
# cores but YIELDS the CPU and disk to any higher-priority process — "all
# available CPUs, with priority handling". exec replaces the shell with nice,
# which execs ionice, which execs python (still PID-traceable, signals/exit
# code propagate to the runner step).
# Quarantine the heavy/segfault-prone + umbrella-skip suites (parity with the
# hardened ubuntu workflow): tests/e2e + the cross-package import gate are
# crash-prone, and tests/examples/test_09_dev.py hard-requires scitex.module ->
# scitex_hub, which is umbrella_skip BY DESIGN (never in [all]) so it can't run
# in the [all] SIF. Dropping these ignores is what broke the v2.30.3 release.
exec nice -n 19 ionice -c 3 \
python -m pytest tests/ -n "$WORKERS" --dist load -q \
--ignore=tests/e2e \
--ignore=tests/integration/test_cross_package_imports.py \
--ignore=tests/examples/test_09_dev.py \
--cov=src/scitex --cov-report=xml --cov-report=term \
-p no:cacheprovider
Loading
Loading