feat(ci): add test-gate step + clean up detect-changes dead outputs

Add clippy + cargo test quality gate to CI pipeline DAG. The test-gate
runs on ci-compile-cpu nodes with persistent cargo cache PVC for fast
incremental builds. Also simplifies detect-changes by removing 5 unused
output parameters (service-packages, training-examples, deploy-list,
needs-services, needs-training) and ~170 lines of dead shell logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-12 09:12:40 +01:00
parent 65f29a8167
commit c06954f471

View File

@@ -30,6 +30,9 @@ spec:
items:
- key: .dockerconfigjson
path: config.json
- name: cargo-target-cpu
persistentVolumeClaim:
claimName: cargo-target-cpu
arguments:
parameters:
- name: commit-sha
@@ -111,6 +114,15 @@ spec:
value: foxhunt-training-runtime
when: "{{tasks.detect-changes.outputs.parameters.docker-images}} == true"
- name: test-gate
depends: "detect-changes && (rebuild-ci-builder-cpu.Succeeded || rebuild-ci-builder-cpu.Skipped)"
template: test-gate
arguments:
parameters:
- name: commit-sha
value: "{{workflow.parameters.commit-sha}}"
when: "{{tasks.detect-changes.outputs.parameters.needs-code}} == true"
- name: apply-argo-templates
depends: "detect-changes.Succeeded && (rebuild-ci-builder-cpu.Succeeded || rebuild-ci-builder-cpu.Skipped)"
template: apply-argo-templates
@@ -129,7 +141,7 @@ spec:
value: "{{workflow.parameters.commit-sha}}"
when: "{{tasks.detect-changes.outputs.parameters.needs-infra}} == true"
# ── detect-changes: granular per-binary change detection ──
# ── detect-changes: classify changed files to gate downstream steps ──
- name: detect-changes
nodeSelector:
k8s.scaleway.com/pool-name: platform
@@ -182,238 +194,41 @@ spec:
echo "false"
}
# --- Dependency tiers ---
# Tier 0: workspace root — rebuilds EVERYTHING
SHARED=$(check_paths "Cargo.toml Cargo.lock crates/common/ crates/config/")
# Tier 1: proto files used by services
PROTO=$(check_paths "bin/fxt/proto/")
# Tier 2: domain crates shared by subsets of services
TRADING_ENGINE=$(check_paths "crates/trading_engine/")
RISK=$(check_paths "crates/risk/")
STORAGE=$(check_paths "crates/storage/ crates/database/")
BROKER=$(check_paths "crates/broker crates/ctrader")
DATA=$(check_paths "crates/data/")
# Tier 3: ML crates — affect ml-training-service, trading-service, AND all training binaries
ML_CHANGED=$(check_paths "crates/ml/ crates/ml-core/ crates/ml-dqn/ crates/ml-ppo/ crates/ml-features/ crates/ml-supervised/ crates/ml-ensemble/ crates/ml-regime/ crates/ml-hyperopt/ crates/ml-data-validation/ crates/ml-labeling/ crates/ml-validation/ crates/ml-checkpoint/ crates/ml-risk/ crates/ml-backtesting/")
# Tier 4: service-specific directories
SVC_API=$(check_paths "services/api/")
SVC_TRADING=$(check_paths "services/trading_service/")
SVC_ML_TRAIN=$(check_paths "services/ml_training_service/")
SVC_BACKTEST=$(check_paths "services/backtesting_service/")
SVC_AGENT=$(check_paths "services/trading_agent_service/")
SVC_BROKER=$(check_paths "services/broker_gateway/")
SVC_DATA=$(check_paths "services/data_acquisition_service/")
# Tier 5: training-specific subdirectories within ml crate
ML_TRAINERS=$(check_paths "crates/ml/src/trainers/")
ML_HYPEROPT=$(check_paths "crates/ml/src/hyperopt/")
ML_EVAL=$(check_paths "crates/ml/src/evaluation/")
ML_EXAMPLES=$(check_paths "crates/ml/examples/")
TRAINING_UPLOADER=$(check_paths "crates/training_uploader/")
# Tier 6: infra and docker
DOCKER_IMAGES=$(check_paths "infra/docker/")
# --- Boolean gates for downstream DAG tasks ---
NEEDS_CODE=$(check_paths "Cargo.toml Cargo.lock crates/ services/ bin/fxt/")
NEEDS_DASHBOARD=$(check_paths "web-dashboard/")
INFRA_CI=$(check_paths "infra/k8s/argo/ci-pipeline infra/k8s/services/")
DOCKER_IMAGES=$(check_paths "infra/docker/")
NEEDS_INFRA=$(check_paths "infra/live/ infra/modules/")
NEEDS_ARGO_TEMPLATES=$(check_paths "infra/k8s/argo/")
# ========================================
# Build SERVICE package list
# ========================================
SERVICE_PKGS=""
add_svc() {
local pkg="$1"
# Deduplicate: only add if not already present
case " $SERVICE_PKGS " in
*" $pkg "*) ;;
*) SERVICE_PKGS="$SERVICE_PKGS $pkg" ;;
esac
}
if [ "$SHARED" = "true" ] || [ "$PROTO" = "true" ] || [ "$INFRA_CI" = "true" ]; then
# Shared changes → rebuild all services
SERVICE_PKGS="api trading-service ml-training-service backtesting-service trading-agent-service broker-gateway data-acquisition-service"
else
# trading_engine affects: api, trading-service, backtesting-service, trading-agent-service
if [ "$TRADING_ENGINE" = "true" ]; then
add_svc "api"; add_svc "trading-service"; add_svc "backtesting-service"; add_svc "trading-agent-service"
fi
# risk affects: api, trading-service
if [ "$RISK" = "true" ]; then
add_svc "api"; add_svc "trading-service"
fi
# storage/database affects: most services that persist data
if [ "$STORAGE" = "true" ]; then
add_svc "api"; add_svc "trading-service"; add_svc "ml-training-service"
add_svc "backtesting-service"; add_svc "trading-agent-service"
fi
# broker crates affect: broker-gateway, trading-service
if [ "$BROKER" = "true" ]; then
add_svc "broker-gateway"; add_svc "trading-service"
fi
# data crate affects: data-acquisition-service, backtesting-service
if [ "$DATA" = "true" ]; then
add_svc "data-acquisition-service"; add_svc "backtesting-service"
fi
# ML crates affect: ml-training-service, trading-service (ensemble inference)
if [ "$ML_CHANGED" = "true" ]; then
add_svc "ml-training-service"; add_svc "trading-service"
fi
# Service-specific directories
[ "$SVC_API" = "true" ] && add_svc "api"
[ "$SVC_TRADING" = "true" ] && add_svc "trading-service"
[ "$SVC_ML_TRAIN" = "true" ] && add_svc "ml-training-service"
[ "$SVC_BACKTEST" = "true" ] && add_svc "backtesting-service"
[ "$SVC_AGENT" = "true" ] && add_svc "trading-agent-service"
[ "$SVC_BROKER" = "true" ] && add_svc "broker-gateway"
[ "$SVC_DATA" = "true" ] && add_svc "data-acquisition-service"
fi
# Trim leading space
SERVICE_PKGS=$(echo "$SERVICE_PKGS" | sed 's/^ //')
# ========================================
# Build TRAINING example list
# ========================================
TRAINING_EXAMPLES=""
add_train() {
local ex="$1"
case " $TRAINING_EXAMPLES " in
*" $ex "*) ;;
*) TRAINING_EXAMPLES="$TRAINING_EXAMPLES $ex" ;;
esac
}
ALL_TRAIN_EXAMPLES="train_baseline_rl train_baseline_supervised evaluate_baseline evaluate_supervised hyperopt_baseline_rl hyperopt_baseline_supervised"
if [ "$SHARED" = "true" ] || [ "$ML_CHANGED" = "true" ]; then
# Shared or ML crate changes → rebuild all training binaries
TRAINING_EXAMPLES="$ALL_TRAIN_EXAMPLES"
add_train "training_uploader"
else
# Granular: specific ML subdirectories
if [ "$ML_TRAINERS" = "true" ]; then
add_train "train_baseline_rl"; add_train "train_baseline_supervised"
fi
if [ "$ML_HYPEROPT" = "true" ]; then
add_train "hyperopt_baseline_rl"; add_train "hyperopt_baseline_supervised"
fi
if [ "$ML_EVAL" = "true" ]; then
add_train "evaluate_baseline"; add_train "evaluate_supervised"
fi
# Individual example file changes
if [ "$ML_EXAMPLES" = "true" ]; then
for file in $CHANGED_FILES; do
case "$file" in
crates/ml/examples/*.rs)
# Extract example name from filename (strip path and .rs)
EXAMPLE_NAME=$(basename "$file" .rs)
case "$EXAMPLE_NAME" in
train_baseline_rl|train_baseline_supervised|evaluate_baseline|evaluate_supervised|hyperopt_baseline_rl|hyperopt_baseline_supervised)
add_train "$EXAMPLE_NAME" ;;
esac
;;
esac
done
fi
if [ "$TRAINING_UPLOADER" = "true" ]; then
add_train "training_uploader"
fi
fi
# Trim leading space
TRAINING_EXAMPLES=$(echo "$TRAINING_EXAMPLES" | sed 's/^ //')
# ========================================
# Build DEPLOY list (service name → K8s deployment name)
# ========================================
# trading-service excluded from auto-deploy for safety
DEPLOY_LIST=""
for pkg in $SERVICE_PKGS; do
case "$pkg" in
trading-service) ;; # excluded — must be explicitly enabled
*) DEPLOY_LIST="$DEPLOY_LIST $pkg" ;;
esac
done
DEPLOY_LIST=$(echo "$DEPLOY_LIST" | sed 's/^ //')
# ========================================
# Compute boolean gates
# ========================================
if [ -n "$SERVICE_PKGS" ]; then NEEDS_SERVICES="true"; else NEEDS_SERVICES="false"; fi
if [ -n "$TRAINING_EXAMPLES" ]; then NEEDS_TRAINING="true"; else NEEDS_TRAINING="false"; fi
if [ "$NEEDS_SERVICES" = "true" ] || [ "$NEEDS_TRAINING" = "true" ]; then
NEEDS_CODE="true"
else
NEEDS_CODE="false"
fi
echo "=== Build decisions ==="
echo "needs-services: $NEEDS_SERVICES"
echo "needs-training: $NEEDS_TRAINING"
echo "needs-code: $NEEDS_CODE"
echo "needs-dashboard: $NEEDS_DASHBOARD"
echo "docker-images: $DOCKER_IMAGES"
echo "needs-infra: $NEEDS_INFRA"
echo "needs-argo: $NEEDS_ARGO_TEMPLATES"
echo "docker-images: $DOCKER_IMAGES"
echo "service-packages: $SERVICE_PKGS"
echo "training-examples: $TRAINING_EXAMPLES"
echo "deploy-list: $DEPLOY_LIST"
echo "======================"
mkdir -p /tmp/outputs
echo -n "$NEEDS_SERVICES" > /tmp/outputs/needs-services
echo -n "$NEEDS_TRAINING" > /tmp/outputs/needs-training
echo -n "$NEEDS_CODE" > /tmp/outputs/needs-code
echo -n "$NEEDS_DASHBOARD" > /tmp/outputs/needs-dashboard
echo -n "$DOCKER_IMAGES" > /tmp/outputs/docker-images
echo -n "$NEEDS_CODE" > /tmp/outputs/needs-code
echo -n "$NEEDS_INFRA" > /tmp/outputs/needs-infra
echo -n "$NEEDS_ARGO_TEMPLATES" > /tmp/outputs/needs-argo-templates
echo -n "$SERVICE_PKGS" > /tmp/outputs/service-packages
echo -n "$TRAINING_EXAMPLES" > /tmp/outputs/training-examples
echo -n "$DEPLOY_LIST" > /tmp/outputs/deploy-list
SCRIPT
chmod +x /tmp/detect.sh
/tmp/detect.sh
outputs:
parameters:
- name: needs-services
- name: needs-code
valueFrom:
path: /tmp/outputs/needs-services
- name: needs-training
valueFrom:
path: /tmp/outputs/needs-training
path: /tmp/outputs/needs-code
- name: needs-dashboard
valueFrom:
path: /tmp/outputs/needs-dashboard
- name: docker-images
valueFrom:
path: /tmp/outputs/docker-images
- name: needs-code
valueFrom:
path: /tmp/outputs/needs-code
- name: service-packages
valueFrom:
path: /tmp/outputs/service-packages
- name: training-examples
valueFrom:
path: /tmp/outputs/training-examples
- name: deploy-list
valueFrom:
path: /tmp/outputs/deploy-list
- name: needs-infra
valueFrom:
path: /tmp/outputs/needs-infra
@@ -421,6 +236,87 @@ spec:
valueFrom:
path: /tmp/outputs/needs-argo-templates
# ── test-gate: clippy + cargo test quality gate ──
- name: test-gate
inputs:
parameters:
- name: commit-sha
nodeSelector:
k8s.scaleway.com/pool-name: ci-compile-cpu
tolerations:
- key: k8s.scaleway.com/pool-name
operator: Equal
value: ci-compile-cpu
effect: NoSchedule
container:
image: gitlab-registry.foxhunt.svc.cluster.local:5000/root/foxhunt/ci-builder-cpu:latest
imagePullPolicy: Always
command: ["/bin/sh", "-c"]
env:
- name: SQLX_OFFLINE
value: "true"
- name: CARGO_TERM_COLOR
value: always
- name: CARGO_TARGET_DIR
value: /cargo-target
- name: CARGO_HOME
value: /cargo-target/cargo-home
resources:
requests:
cpu: "14"
memory: 16Gi
limits:
cpu: "30"
memory: 32Gi
volumeMounts:
- name: git-ssh-key
mountPath: /etc/git-ssh
readOnly: true
- name: cargo-target-cpu
mountPath: /cargo-target
args:
- |
set -e
SHA="{{inputs.parameters.commit-sha}}"
# SSH setup
mkdir -p ~/.ssh
cp /etc/git-ssh/ssh-privatekey ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf 'Host *\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n' > ~/.ssh/config
chmod 600 ~/.ssh/config
REPO="ssh://git@gitlab-gitlab-shell.foxhunt.svc.cluster.local:2222/root/foxhunt.git"
WORKSPACE="/cargo-target/src"
git config --global --add safe.directory "$WORKSPACE"
if [ -d "$WORKSPACE/.git" ]; then
cd "$WORKSPACE"
CURRENT=$(git rev-parse HEAD 2>/dev/null || echo "none")
if [ "$CURRENT" = "$SHA" ]; then
echo "=== Already at $SHA ==="
else
git fetch origin
git checkout --force "$SHA"
git clean -fd
fi
else
git clone --filter=blob:none "$REPO" "$WORKSPACE"
cd "$WORKSPACE"
git checkout "$SHA"
fi
export PATH="${CARGO_HOME}/bin:${PATH}"
echo "=== Running clippy ==="
cargo clippy --workspace --all-targets -- -D warnings
echo "=== Running tests (lib only, no integration) ==="
cargo test --workspace --lib
echo "=== Test gate passed ==="
# ── build-web-dashboard: npm build + upload to MinIO ──
- name: build-web-dashboard
nodeSelector: