# Compile-and-Deploy: manual workflow for building and deploying service binaries. # # DAG: # create-tag ──> compile-services ──> upload-release ──> deploy-services # # Image builds handled by ci-pipeline, not here. # Training compilation lives in compile-and-train-template.yaml. --- apiVersion: argoproj.io/v1alpha1 kind: WorkflowTemplate metadata: name: compile-and-deploy namespace: foxhunt labels: app.kubernetes.io/name: compile-and-deploy app.kubernetes.io/part-of: foxhunt spec: entrypoint: pipeline onExit: notify-result serviceAccountName: argo-workflow podMetadata: labels: app.kubernetes.io/part-of: foxhunt app.kubernetes.io/component: compile-and-deploy podGC: strategy: OnPodCompletion ttlStrategy: secondsAfterCompletion: 3600 activeDeadlineSeconds: 3600 # 1 hour (compile + deploy) arguments: parameters: - name: commit-sha value: HEAD - name: service-packages value: "api trading-service backtesting-service trading-agent-service broker-gateway data-acquisition-service" volumes: - name: git-ssh-key secret: secretName: argo-git-ssh-key defaultMode: 256 - name: cargo-target-cpu persistentVolumeClaim: claimName: cargo-target-cpu templates: - name: pipeline dag: tasks: - name: create-tag template: create-tag - name: compile-services depends: "create-tag" template: compile-services arguments: parameters: - name: tag value: "{{tasks.create-tag.outputs.parameters.tag}}" - name: service-packages value: "{{workflow.parameters.service-packages}}" - name: upload-release depends: "compile-services" template: upload-release arguments: parameters: - name: tag value: "{{tasks.create-tag.outputs.parameters.tag}}" - name: deploy-services depends: "upload-release" template: deploy-services arguments: parameters: - name: tag value: "{{tasks.create-tag.outputs.parameters.tag}}" - name: deploy-list value: "{{workflow.parameters.service-packages}}" # ── create-tag: CalVer auto-tag on code changes ── - name: create-tag nodeSelector: k8s.scaleway.com/pool-name: platform container: image: curlimages/curl:8.12.1 command: ["/bin/sh", "-c"] env: - name: GITLAB_PAT valueFrom: secretKeyRef: name: gitlab-pat key: token resources: requests: cpu: 100m memory: 64Mi limits: cpu: 200m memory: 128Mi args: - | set -e GITLAB="http://gitlab-webservice-default.foxhunt.svc.cluster.local:8181" PROJECT_ID=1 SHA="{{workflow.parameters.commit-sha}}" # Compute CalVer prefix: vYYYY.MM PREFIX="v$(date +%Y.%m)" # Query existing tags for this month TAGS=$(curl -sf \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/${PROJECT_ID}/repository/tags?search=${PREFIX}" \ || echo "[]") # Parse highest N from vYYYY.MM.N tags LAST_N=$(echo "$TAGS" | grep -oP "\"name\":\"${PREFIX}\.\K[0-9]+" | sort -n | tail -1) if [ -z "$LAST_N" ]; then NEXT_N=1 else NEXT_N=$((LAST_N + 1)) fi TAG="${PREFIX}.${NEXT_N}" echo "Creating tag: ${TAG} at ${SHA}" # Create the tag RESULT=$(curl -sf -X POST \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ -d "tag_name=${TAG}" \ -d "ref=${SHA}" \ "${GITLAB}/api/v4/projects/${PROJECT_ID}/repository/tags") echo "$RESULT" | grep -q "$TAG" || { echo "Tag creation failed: $RESULT"; exit 1; } echo "Tag ${TAG} created successfully" mkdir -p /tmp/outputs echo -n "$TAG" > /tmp/outputs/tag outputs: parameters: - name: tag valueFrom: path: /tmp/outputs/tag # ── compile-services: selective per-binary build, incremental on local RWO PVC ── - name: compile-services metadata: labels: app.kubernetes.io/component: compile inputs: parameters: - name: tag - name: service-packages nodeSelector: k8s.scaleway.com/pool-name: ci-compile-cpu tolerations: - key: node.cilium.io/agent-not-ready operator: Exists 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 - name: GITLAB_PAT valueFrom: secretKeyRef: name: gitlab-pat key: token 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="{{workflow.parameters.commit-sha}}" SERVICE_PKGS="{{inputs.parameters.service-packages}}" # 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" # PVC may be owned by a different UID from a previous run git config --global --add safe.directory "$WORKSPACE" # Persistent checkout on PVC — only changed files get new mtimes, # so cargo skips recompiling unchanged workspace crates. 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, skipping checkout ===" else echo "=== Updating checkout: $(echo $CURRENT | cut -c1-8) -> $(echo $SHA | cut -c1-8) ===" git fetch origin git checkout --force "$SHA" git clean -fd fi else echo "=== Initial clone (first run) ===" git clone --filter=blob:none "$REPO" "$WORKSPACE" cd "$WORKSPACE" git checkout "$SHA" fi echo "Checked out $(git rev-parse --short HEAD)" # Ensure cargo home registry is on PVC export PATH="${CARGO_HOME}/bin:${PATH}" export FOXHUNT_BUILD_VERSION="{{inputs.parameters.tag}}" # Prune build artifacts if PVC exceeds 25GB (prevents unbounded growth) TARGET_SIZE_MB=$(du -sm "$CARGO_TARGET_DIR" 2>/dev/null | cut -f1 || echo 0) echo "PVC usage: ${TARGET_SIZE_MB}MB" if [ "$TARGET_SIZE_MB" -gt 25000 ]; then echo "PVC exceeds 25GB limit, pruning build artifacts..." rm -rf "$CARGO_TARGET_DIR/release" "$CARGO_TARGET_DIR/debug" fi # Guard: empty package list would build entire workspace if [ -z "$SERVICE_PKGS" ]; then echo "ERROR: service-packages is empty, refusing to build entire workspace" exit 1 fi # Build only the affected service packages (incremental via persistent target dir) CARGO_ARGS="" for pkg in $SERVICE_PKGS; do CARGO_ARGS="$CARGO_ARGS -p $pkg" done echo "=== Building service binaries: $SERVICE_PKGS (incremental) ===" cargo build --release $CARGO_ARGS # Collect built binaries mkdir -p "$WORKSPACE/bin/services" for pkg in $SERVICE_PKGS; do bin_name=$(echo "$pkg" | tr '-' '_') cp "$CARGO_TARGET_DIR/release/$pkg" "$WORKSPACE/bin/services/" 2>/dev/null \ || cp "$CARGO_TARGET_DIR/release/$bin_name" "$WORKSPACE/bin/services/" 2>/dev/null \ || { echo "Binary not found for $pkg"; ls "$CARGO_TARGET_DIR/release/"; exit 1; } done strip "$WORKSPACE/bin/services/"* echo "=== Service binaries ===" ls -lh "$WORKSPACE/bin/services/" echo "=== Uploading service binaries to GitLab packages ===" GITLAB="http://gitlab-webservice-default.foxhunt.svc.cluster.local:8181" TAG="${FOXHUNT_BUILD_VERSION}" for bin in "$WORKSPACE/bin/services/"*; do BIN_NAME=$(basename "$bin") echo "Uploading ${BIN_NAME} (${TAG})..." curl -f --upload-file "$bin" \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/1/packages/generic/foxhunt-services/${TAG}/${BIN_NAME}" done # Update 'latest' per-file (preserves unbuilt binaries from prior runs) echo "=== Updating 'latest' package ===" LATEST_PKG=$(curl -sf -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/1/packages?package_name=foxhunt-services&package_version=latest" \ | grep -oP '"id":\K[0-9]+' | head -1) for bin in "$WORKSPACE/bin/services/"*; do BIN_NAME=$(basename "$bin") # Delete existing file by name before uploading replacement if [ -n "$LATEST_PKG" ]; then FILE_ID=$(curl -sf -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/1/packages/${LATEST_PKG}/package_files" \ | grep -oP "\"id\":([0-9]+),\"package_id\":${LATEST_PKG}[^}]*\"file_name\":\"${BIN_NAME}\"" \ | grep -oP '"id":\K[0-9]+' | head -1) if [ -n "$FILE_ID" ]; then curl -sf -X DELETE -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/1/packages/${LATEST_PKG}/package_files/${FILE_ID}" || true fi fi curl -f --upload-file "$bin" \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/1/packages/generic/foxhunt-services/latest/${BIN_NAME}" || true done echo "=== Service compile + upload done ($SERVICE_PKGS) ===" # ── upload-release: create GitLab Release with package links ── - name: upload-release inputs: parameters: - name: tag nodeSelector: k8s.scaleway.com/pool-name: platform container: image: curlimages/curl:8.12.1 command: ["/bin/sh", "-c"] env: - name: GITLAB_PAT valueFrom: secretKeyRef: name: gitlab-pat key: token resources: requests: cpu: 100m memory: 64Mi limits: cpu: 200m memory: 128Mi args: - | set -e GITLAB="http://gitlab-webservice-default.foxhunt.svc.cluster.local:8181" PROJECT_ID=1 TAG="{{inputs.parameters.tag}}" echo "=== Creating GitLab Release ${TAG} ===" # Get commits since previous tag for release notes PREV_TAG=$(curl -sf \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/${PROJECT_ID}/repository/tags?per_page=2" \ | grep -oP '"name":"\K[^"]+' | sed -n '2p') if [ -n "$PREV_TAG" ]; then COMMITS=$(curl -sf \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/${PROJECT_ID}/repository/compare?from=${PREV_TAG}&to=${TAG}" \ | grep -oP '"title":"\K[^"]+' | head -20 \ | sed 's/^/- /' || echo "- Release ${TAG}") DESCRIPTION="## Changes since ${PREV_TAG}\n\n${COMMITS}" else DESCRIPTION="## Initial release\n\nFirst CalVer release." fi # Create the release RESPONSE=$(curl -sf -X POST \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ -H "Content-Type: application/json" \ -d "{ \"tag_name\": \"${TAG}\", \"name\": \"${TAG}\", \"description\": \"$(printf '%s' "$DESCRIPTION" | sed 's/"/\\"/g')\" }" \ "${GITLAB}/api/v4/projects/${PROJECT_ID}/releases") || { echo "WARN: Release creation failed (may already exist)" } echo "Release ${TAG} created" echo "$RESPONSE" | head -5 # Add package links as release assets for pkg_name in foxhunt-services foxhunt-training; do PKG_CHECK=$(curl -sf \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ "${GITLAB}/api/v4/projects/${PROJECT_ID}/packages?package_name=${pkg_name}&package_version=${TAG}" \ || echo "[]") if echo "$PKG_CHECK" | grep -q "$TAG"; then curl -sf -X POST \ -H "PRIVATE-TOKEN: ${GITLAB_PAT}" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"${pkg_name}\", \"url\": \"${GITLAB}/-/packages?type=generic&search=${pkg_name}&version=${TAG}\", \"link_type\": \"package\" }" \ "${GITLAB}/api/v4/projects/${PROJECT_ID}/releases/${TAG}/assets/links" || true echo "Linked ${pkg_name} package to release" fi done echo "=== Release ${TAG} complete ===" # ── deploy-services: selective rolling restart for affected deployments ── - name: deploy-services inputs: parameters: - name: tag - name: deploy-list serviceAccountName: argo-workflow nodeSelector: k8s.scaleway.com/pool-name: platform container: image: curlimages/curl:8.12.1 command: ["/bin/sh", "-c"] resources: requests: cpu: 100m memory: 64Mi limits: cpu: 200m memory: 128Mi args: - | set -e if ! command -v kubectl >/dev/null 2>&1; then echo "=== Installing kubectl ===" curl -sLo /tmp/kubectl "https://dl.k8s.io/release/v1.31.4/bin/linux/amd64/kubectl" chmod +x /tmp/kubectl export PATH="/tmp:$PATH" fi TAG="{{inputs.parameters.tag}}" DEPLOY_LIST="{{inputs.parameters.deploy-list}}" if [ -z "$DEPLOY_LIST" ]; then echo "=== No services to deploy ===" exit 0 fi echo "=== Deploying release ${TAG}: $DEPLOY_LIST ===" for svc in $DEPLOY_LIST; do echo "Patching $svc with FOXHUNT_RELEASE=${TAG}..." kubectl -n foxhunt patch deployment "$svc" -p "{ \"spec\":{\"template\":{ \"metadata\":{\"annotations\":{\"foxhunt.io/release\":\"${TAG}\"}}, \"spec\":{\"initContainers\":[{ \"name\":\"fetch-binary\", \"env\":[{\"name\":\"FOXHUNT_RELEASE\",\"value\":\"${TAG}\"}] }]} }} }" || echo "WARN: $svc patch failed (may not exist)" done echo "=== Waiting for rollouts ===" for svc in $DEPLOY_LIST; do kubectl -n foxhunt rollout status deployment "$svc" --timeout=120s || echo "WARN: $svc rollout timeout" done echo "=== Deploy ${TAG} complete ($DEPLOY_LIST) ===" # ── notify-result: post workflow outcome to Mattermost ── - name: notify-result nodeSelector: k8s.scaleway.com/pool-name: platform container: image: curlimages/curl:8.12.1 command: ["/bin/sh", "-c"] env: - name: WEBHOOK_URL valueFrom: secretKeyRef: name: notification-webhook key: webhook-url optional: true resources: requests: cpu: 50m memory: 32Mi limits: cpu: 100m memory: 64Mi args: - | STATUS="{{workflow.status}}" NAME="{{workflow.name}}" if [ -z "$WEBHOOK_URL" ] || echo "$WEBHOOK_URL" | grep -q "PLACEHOLDER"; then echo "No webhook configured, skipping notification" exit 0 fi if [ "$STATUS" = "Succeeded" ]; then EMOJI=":white_check_mark:" else EMOJI=":x:" fi PAYLOAD="{\"username\":\"Argo CI\",\"text\":\"${EMOJI} **${NAME}** — ${STATUS} ({{workflow.duration}}s)\"}" curl -sf -X POST -H 'Content-Type: application/json' \ -d "$PAYLOAD" "$WEBHOOK_URL" || echo "WARN: webhook post failed"