#!/usr/bin/env bash # Upload a BOM to Astable from CI. POSTs the multipart form to # /api/v1/bom/upload, then polls the returned jobId until the worker # finishes (done/failed). Prints the result JSON on done; exits non-zero # on any failure (timeout, parse error, worker failure, …) so the CI # job fails visibly. # # Env vars (required): # ASTABLE_URL — base URL (e.g. https://astable.timhadwen.com) # ASTABLE_API_TOKEN — bearer token minted at /manage/api-tokens # # Args: # --file — BOM CSV to upload (required) # --part-ipn — InvPart.ipn of the assembly (required) # --branch — drives revision targeting (required) # --commit-sha — for audit (optional) # --commit-message — drives "main" revision label (optional) # --poll-interval — default 3 # --timeout — default 300 (5 min) # # Designed to be runnable under gitlab-ci-local — just set ASTABLE_URL # to your dev URL (e.g. http://host.docker.internal:3000 if Astable is # on the host) and gitlab-ci-local will pick it up from .env or the # `--variable` flag. set -euo pipefail POLL_INTERVAL=3 TIMEOUT=300 FILE="" CPL="" GERBERS="" PANEL_GERBERS="" STEP="" PCB_PDF="" SCHEMATIC_PDF="" IBOM_HTML="" BOM_XLSX="" PCBA_IMAGE="" PCB_IMAGE="" DRC_REPORT="" ERC_REPORT="" PART_IPN="" BRANCH="" COMMIT_SHA="" COMMIT_MESSAGE="" while [[ $# -gt 0 ]]; do case "$1" in --file) FILE="$2"; shift 2 ;; --cpl) CPL="$2"; shift 2 ;; --gerbers) GERBERS="$2"; shift 2 ;; --panel-gerbers) PANEL_GERBERS="$2"; shift 2 ;; --step) STEP="$2"; shift 2 ;; --pcb-pdf) PCB_PDF="$2"; shift 2 ;; --schematic-pdf) SCHEMATIC_PDF="$2"; shift 2 ;; --ibom-html) IBOM_HTML="$2"; shift 2 ;; --bom-xlsx) BOM_XLSX="$2"; shift 2 ;; --pcba-image) PCBA_IMAGE="$2"; shift 2 ;; --pcb-image) PCB_IMAGE="$2"; shift 2 ;; --drc-report) DRC_REPORT="$2"; shift 2 ;; --erc-report) ERC_REPORT="$2"; shift 2 ;; --part-ipn) PART_IPN="$2"; shift 2 ;; --branch) BRANCH="$2"; shift 2 ;; --commit-sha) COMMIT_SHA="$2"; shift 2 ;; --commit-message) COMMIT_MESSAGE="$2"; shift 2 ;; --poll-interval) POLL_INTERVAL="$2"; shift 2 ;; --timeout) TIMEOUT="$2"; shift 2 ;; *) echo "Unknown arg: $1" >&2; exit 2 ;; esac done : "${ASTABLE_URL:?ASTABLE_URL must be set}" : "${ASTABLE_API_TOKEN:?ASTABLE_API_TOKEN must be set}" : "${FILE:?--file is required}" : "${PART_IPN:?--part-ipn is required}" : "${BRANCH:?--branch is required}" [[ -f "$FILE" ]] || { echo "BOM file not found: $FILE" >&2; exit 2; } [[ -z "$CPL" || -f "$CPL" ]] || { echo "CPL file not found: $CPL" >&2; exit 2; } [[ "$BRANCH" == "dev" || "$BRANCH" == "main" ]] || { echo "--branch must be dev or main" >&2; exit 2; } echo "[bom-upload] POST $ASTABLE_URL/api/v1/bom/upload" echo "[bom-upload] partIpn=$PART_IPN branch=$BRANCH file=$FILE${CPL:+ cpl=$CPL}" CURL_ARGS=( -fsS -X POST -H "authorization: Bearer $ASTABLE_API_TOKEN" -F "file=@$FILE" -F "partIpn=$PART_IPN" -F "branch=$BRANCH" -F "commitSha=$COMMIT_SHA" -F "commitMessage=$COMMIT_MESSAGE" ) [[ -n "$CPL" ]] && CURL_ARGS+=(-F "cpl=@$CPL") [[ -n "$GERBERS" ]] && CURL_ARGS+=(-F "gerbers=@$GERBERS") [[ -n "$PANEL_GERBERS" ]] && CURL_ARGS+=(-F "panelGerbers=@$PANEL_GERBERS") [[ -n "$STEP" ]] && CURL_ARGS+=(-F "step=@$STEP") [[ -n "$PCB_PDF" ]] && CURL_ARGS+=(-F "pcbPdf=@$PCB_PDF") [[ -n "$SCHEMATIC_PDF" ]] && CURL_ARGS+=(-F "schematicPdf=@$SCHEMATIC_PDF") [[ -n "$IBOM_HTML" ]] && CURL_ARGS+=(-F "ibomHtml=@$IBOM_HTML") [[ -n "$BOM_XLSX" ]] && CURL_ARGS+=(-F "bomXlsx=@$BOM_XLSX") [[ -n "$PCBA_IMAGE" ]] && CURL_ARGS+=(-F "pcbaImage=@$PCBA_IMAGE") [[ -n "$PCB_IMAGE" ]] && CURL_ARGS+=(-F "pcbImage=@$PCB_IMAGE") [[ -n "$DRC_REPORT" ]] && CURL_ARGS+=(-F "drcReport=@$DRC_REPORT") [[ -n "$ERC_REPORT" ]] && CURL_ARGS+=(-F "ercReport=@$ERC_REPORT") RESPONSE=$(curl "${CURL_ARGS[@]}" "$ASTABLE_URL/api/v1/bom/upload") JOB_ID=$(echo "$RESPONSE" | jq -r '.jobId // empty') POLL_URL=$(echo "$RESPONSE" | jq -r '.pollUrl // empty') LINE_COUNT=$(echo "$RESPONSE" | jq -r '.lineCount // 0') if [[ -z "$JOB_ID" || -z "$POLL_URL" ]]; then echo "[bom-upload] enqueue failed: $RESPONSE" >&2 exit 1 fi echo "[bom-upload] queued ($LINE_COUNT lines parsed) → job $JOB_ID" START=$(date +%s) while true; do NOW=$(date +%s) ELAPSED=$((NOW - START)) if (( ELAPSED > TIMEOUT )); then echo "[bom-upload] timed out after ${TIMEOUT}s" >&2 exit 1 fi STATE=$(curl -fsS \ -H "authorization: Bearer $ASTABLE_API_TOKEN" \ "$ASTABLE_URL$POLL_URL") STATUS=$(echo "$STATE" | jq -r '.status') STEP=$(echo "$STATE" | jq -r '.progress.message // empty') case "$STATUS" in done) echo "[bom-upload] done" echo "$STATE" | jq -r '.result | " added=\(.added) updated=\(.updated) removed=\(.removed) missingPartsCreated=\(.missingPartsCreated) unresolvedSuppliers=\(.unresolvedSuppliers)"' REV=$(echo "$STATE" | jq -r '.result.revision.label // "(none)"') echo " revision: $REV" exit 0 ;; failed) ERROR=$(echo "$STATE" | jq -r '.error // "(no error message)"') echo "[bom-upload] failed: $ERROR" >&2 exit 1 ;; pending|running) [[ -n "$STEP" ]] && echo " · $STATUS — $STEP" sleep "$POLL_INTERVAL" ;; *) echo "[bom-upload] unexpected status: $STATUS" >&2 echo "$STATE" >&2 exit 1 ;; esac done