Team Ai
Apppublic

SidhaGarg/Cloud-DevOps-RLEnv

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
pre_submit_validate.sh368 linesDownload Raw Back to scripts
1#!/usr/bin/env bash2#3# pre_submit_validate.sh4#5# Extended pre-submission checks for OpenEnv hackathon submissions.6# This script complements scripts/validate-submission.sh by also checking7# inference contract requirements and baseline reproducibility.8 9set -euo pipefail10 11DOCKER_BUILD_TIMEOUT=60012INFERENCE_TIMEOUT=120013 14PING_URL=""15REPO_DIR="."16SKIP_DOCKER=false17SKIP_INFERENCE=false18PYTHON_BIN=""19OPENENV_BIN=""20OPENENV_USE_MODULE=false21DOCKER_CONTAINER_ID=""22INFERENCE_OUT_FILE=".pre-submit-inference.out"23 24usage() {25  cat <<'EOF'26Usage: scripts/pre_submit_validate.sh [options]27 28Options:29  --ping-url <url>        HF Space URL (e.g., https://team-space.hf.space)30  --repo-dir <path>       Repo root directory (default: current directory)31  --skip-docker           Skip docker build check32  --skip-inference        Skip inference baseline check33  -h, --help              Show this help message34 35Required environment variables for inference checks:36  API_BASE_URL37  MODEL_NAME38  HF_TOKEN39EOF40}41 42run_with_timeout() {43  local secs="$1"; shift44  if command -v timeout >/dev/null 2>&1; then45    timeout "$secs" "$@"46  elif command -v gtimeout >/dev/null 2>&1; then47    gtimeout "$secs" "$@"48  else49    "$@" &50    local pid=$!51    ( sleep "$secs" && kill "$pid" 2>/dev/null ) &52    local watcher=$!53    wait "$pid" 2>/dev/null54    local rc=$?55    kill "$watcher" 2>/dev/null || true56    wait "$watcher" 2>/dev/null || true57    return $rc58  fi59}60 61log() {62  printf "[%s] %s\n" "$(date -u +%H:%M:%S)" "$*"63}64 65die() {66  log "FAILED -- $*"67  exit 168}69 70pass() {71  log "PASSED -- $*"72}73 74cleanup() {75  if [ -n "$DOCKER_CONTAINER_ID" ]; then76    docker rm -f "$DOCKER_CONTAINER_ID" >/dev/null 2>&1 || true77  fi78  rm -f "$INFERENCE_OUT_FILE" >/dev/null 2>&1 || true79}80 81trap cleanup EXIT82 83resolve_python_bin() {84  local candidates=(85    "$REPO_DIR/.venv/bin/python"86    "$REPO_DIR/.venv/Scripts/python.exe"87    "$REPO_DIR/../.venv/bin/python"88    "$REPO_DIR/../.venv/Scripts/python.exe"89  )90 91  for c in "${candidates[@]}"; do92    if [ -x "$c" ]; then93      PYTHON_BIN="$c"94      return 095    fi96  done97 98  if command -v python >/dev/null 2>&1; then99    PYTHON_BIN="$(command -v python)"100    return 0101  fi102  if command -v python3 >/dev/null 2>&1; then103    PYTHON_BIN="$(command -v python3)"104    return 0105  fi106 107  return 1108}109 110resolve_openenv_cmd() {111  local candidates=(112    "$REPO_DIR/.venv/bin/openenv"113    "$REPO_DIR/.venv/Scripts/openenv.exe"114    "$REPO_DIR/../.venv/bin/openenv"115    "$REPO_DIR/../.venv/Scripts/openenv.exe"116  )117 118  for c in "${candidates[@]}"; do119    if [ -x "$c" ]; then120      OPENENV_BIN="$c"121      return 0122    fi123  done124 125  if command -v openenv >/dev/null 2>&1; then126    OPENENV_BIN="$(command -v openenv)"127    return 0128  fi129 130  return 1131}132 133while [ "$#" -gt 0 ]; do134  case "$1" in135    --ping-url)136      shift137      [ "$#" -gt 0 ] || die "--ping-url requires a value"138      PING_URL="$1"139      ;;140    --repo-dir)141      shift142      [ "$#" -gt 0 ] || die "--repo-dir requires a value"143      REPO_DIR="$1"144      ;;145    --skip-docker)146      SKIP_DOCKER=true147      ;;148    --skip-inference)149      SKIP_INFERENCE=true150      ;;151    -h|--help)152      usage153      exit 0154      ;;155    *)156      die "Unknown option: $1"157      ;;158  esac159  shift160done161 162REPO_DIR="$(cd "$REPO_DIR" && pwd)"163cd "$REPO_DIR"164 165log "Repo: $REPO_DIR"166 167resolve_python_bin || die "No usable Python interpreter found"168log "Python: $PYTHON_BIN"169 170if resolve_openenv_cmd; then171  log "OpenEnv CLI: $OPENENV_BIN"172else173  OPENENV_USE_MODULE=true174  log "OpenEnv CLI via module: $PYTHON_BIN -m openenv"175fi176 177log "Step 1/8: Checking OpenEnv standard file layout"178required_files=(179  "openenv.yaml"180  "models.py"181  "env.py"182  "inference.py"183  "server/app.py"184  "server/cloud_devops_env_environment.py"185)186for f in "${required_files[@]}"; do187  [ -f "$f" ] || die "Missing required file: $f"188done189pass "Core OpenEnv file layout looks valid"190 191log "Step 2/8: Checking inference contract requirements"192[ -f "inference.py" ] || die "inference.py must exist in repo root"193grep -q "from openai import OpenAI" inference.py || die "inference.py must import OpenAI client"194grep -q "OpenAI(" inference.py || die "inference.py must instantiate OpenAI client"195grep -q "\[START\]" inference.py || die "inference.py must emit [START] logs"196grep -q "\[STEP\]" inference.py || die "inference.py must emit [STEP] logs"197grep -q "\[END\]" inference.py || die "inference.py must emit [END] logs"198pass "Inference script contract checks passed"199 200log "Step 3/8: Validating OpenEnv manifest and typed models"201if [ "$OPENENV_USE_MODULE" = true ]; then202  "$PYTHON_BIN" -m openenv validate >/tmp/openenv-validate.out 2>&1 || {203    cat /tmp/openenv-validate.out204    die "openenv validate failed"205  }206else207  "$OPENENV_BIN" validate >/tmp/openenv-validate.out 2>&1 || {208    cat /tmp/openenv-validate.out209    die "openenv validate failed"210  }211fi212pass "openenv validate passed"213 214log "Step 4/8: Optional HF Space ping check"215if [ -n "$PING_URL" ]; then216  PING_URL="${PING_URL%/}"217  code=$(curl -s -o /tmp/pre-submit-ping.out -w "%{http_code}" -X POST \218    -H "Content-Type: application/json" -d '{}' \219    "$PING_URL/reset" --max-time 30 || printf "000")220  [ "$code" = "200" ] || die "HF Space /reset returned HTTP $code"221  pass "HF Space responds to /reset (HTTP 200)"222else223  log "SKIPPED -- no --ping-url provided"224fi225 226log "Step 5/8: Docker build + run check"227if [ "$SKIP_DOCKER" = true ]; then228  log "SKIPPED -- --skip-docker enabled"229else230  command -v docker >/dev/null 2>&1 || die "docker not found"231  if [ -f "Dockerfile" ]; then232    context="."233  elif [ -f "server/Dockerfile" ]; then234    context="server"235  else236    die "No Dockerfile found at root or server/"237  fi238  run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build "$context" >/tmp/pre-submit-docker.out 2>&1 || {239    tail -n 40 /tmp/pre-submit-docker.out240    die "docker build failed"241  }242  pass "Docker build succeeded"243 244  IMAGE_TAG="openenv-pre-submit-local"245  run_with_timeout "$DOCKER_BUILD_TIMEOUT" docker build -t "$IMAGE_TAG" "$context" >/tmp/pre-submit-docker-tagged.out 2>&1 || {246    tail -n 40 /tmp/pre-submit-docker-tagged.out247    die "docker build (tagged) failed"248  }249 250  DOCKER_CONTAINER_ID="$(docker run -d -p 127.0.0.1::8000 "$IMAGE_TAG" 2>/tmp/pre-submit-docker-run.err || true)"251  [ -n "$DOCKER_CONTAINER_ID" ] || {252    cat /tmp/pre-submit-docker-run.err253    die "docker run failed"254  }255 256  HOST_PORT="$(docker port "$DOCKER_CONTAINER_ID" 8000/tcp | tail -n 1 | awk -F: '{print $NF}')"257  [ -n "$HOST_PORT" ] || die "could not resolve mapped host port for container"258 259  HEALTH_OK=false260  for _ in $(seq 1 30); do261    health_code=$(curl -s -o /tmp/pre-submit-health.out -w "%{http_code}" \262      "http://127.0.0.1:${HOST_PORT}/health" --max-time 3 || printf "000")263    if [ "$health_code" = "200" ]; then264      HEALTH_OK=true265      break266    fi267    sleep 1268  done269  [ "$HEALTH_OK" = true ] || {270    docker logs "$DOCKER_CONTAINER_ID" | tail -n 50271    die "container did not become healthy on /health"272  }273 274  reset_code=$(curl -s -o /tmp/pre-submit-reset.out -w "%{http_code}" -X POST \275    -H "Content-Type: application/json" -d '{}' \276    "http://127.0.0.1:${HOST_PORT}/reset" --max-time 10 || printf "000")277  [ "$reset_code" = "200" ] || {278    docker logs "$DOCKER_CONTAINER_ID" | tail -n 50279    die "container /reset returned HTTP $reset_code"280  }281 282  pass "Containerized execution check passed (/health and /reset)"283 284  docker rm -f "$DOCKER_CONTAINER_ID" >/dev/null 2>&1 || true285  DOCKER_CONTAINER_ID=""286fi287 288log "Step 6/8: Environment variable checks"289if [ "$SKIP_INFERENCE" = true ]; then290  log "SKIPPED -- --skip-inference enabled"291else292  [ -n "${API_BASE_URL:-}" ] || die "API_BASE_URL is not set"293  [ -n "${MODEL_NAME:-}" ] || die "MODEL_NAME is not set"294  [ -n "${HF_TOKEN:-}" ] || die "HF_TOKEN is not set"295  pass "Required API_BASE_URL / MODEL_NAME / HF_TOKEN are set"296fi297 298log "Step 7/8: Baseline reproducibility (inference.py)"299if [ "$SKIP_INFERENCE" = true ]; then300  log "SKIPPED -- --skip-inference enabled"301else302  run_with_timeout "$INFERENCE_TIMEOUT" "$PYTHON_BIN" inference.py >"$INFERENCE_OUT_FILE" 2>&1 || {303    tail -n 80 "$INFERENCE_OUT_FILE"304    die "inference.py failed or timed out"305  }306  pass "inference.py completed within timeout"307fi308 309log "Step 8/8: Structured logs + task/grader checks"310if [ "$SKIP_INFERENCE" = true ]; then311  log "SKIPPED -- --skip-inference enabled"312else313  "$PYTHON_BIN" - "$INFERENCE_OUT_FILE" <<'PY'314import json315import sys316from pathlib import Path317 318path = Path(sys.argv[1])319text = path.read_text(encoding='utf-8', errors='replace').splitlines()320 321starts = []322ends = []323step_count = 0324 325for line in text:326    line = line.strip()327    if line.startswith('[START] '):328        payload = json.loads(line[len('[START] '):])329        starts.append(payload)330    elif line.startswith('[STEP] '):331        json.loads(line[len('[STEP] '):])332        step_count += 1333    elif line.startswith('[END] '):334        payload = json.loads(line[len('[END] '):])335        ends.append(payload)336 337if len(starts) < 3:338    raise SystemExit('Expected at least 3 [START] task logs')339 340unique_tasks = {str(s.get('task', '')) for s in starts if s.get('task')}341if len(unique_tasks) < 3:342    raise SystemExit('Expected at least 3 unique tasks in [START] logs')343 344if len(ends) != len(starts):345    raise SystemExit('Mismatch between [START] and [END] log counts')346 347if step_count == 0:348    raise SystemExit('No [STEP] logs found')349 350for i, end in enumerate(ends, start=1):351    score = float(end.get('score', -1.0))352    rewards = end.get('rewards', [])353    if not (0.0 <= score <= 1.0):354        raise SystemExit(f'END #{i} score out of range [0,1]: {score}')355    if not isinstance(rewards, list):356        raise SystemExit(f'END #{i} rewards must be a list')357    for r in rewards:358        rv = float(r)359        if not (-1.0 <= rv <= 1.0):360            raise SystemExit(f'END #{i} step reward out of sanity range [-1,1]: {rv}')361 362print('Structured logs and task/grader checks passed')363PY364  pass "Structured [START]/[STEP]/[END] logs and score-range checks passed"365fi366 367log "All checks passed. Submission is ready."368