From ed3bf57566b13b79861adc28655a3ce4c2aa9fc7 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Mon, 23 Feb 2026 22:26:49 +0000 Subject: [PATCH] add Woodpecker CI pipeline --- .woodpecker.yml | 225 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 .woodpecker.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..935f20f --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,225 @@ +when: + - event: push + branch: master + +clone: + git: + image: woodpeckerci/plugin-git + settings: + attempts: 5 + backoff: 10s + +steps: + - name: test + image: python:3.12-slim + commands: + - python -m venv .venv + - .venv/bin/pip install --quiet --upgrade pip + - >- + .venv/bin/pip install --quiet ".[api,news,sentiment,trading,backtester,dev]" + - .venv/bin/pytest tests/ -v --tb=short -m "not integration" + + - name: build-service-image + image: plugins/docker + depends_on: + - test + environment: + DOCKER_BUILDKIT: 1 + settings: + username: viktorbarzin + password: + from_secret: dockerhub-token + repo: viktorbarzin/trading-bot-service + dockerfile: docker/Dockerfile.service + context: . + build_args: + - EXTRAS=api,news,sentiment,trading,backtester + - SERVICE_MODULE=api_gateway + cache_from: + - viktorbarzin/trading-bot-service:latest + tags: + - "build-${CI_PIPELINE_NUMBER}" + + - name: build-dashboard-image + image: plugins/docker + depends_on: + - test + environment: + DOCKER_BUILDKIT: 1 + settings: + username: viktorbarzin + password: + from_secret: dockerhub-token + repo: viktorbarzin/trading-bot-dashboard + dockerfile: docker/Dockerfile.dashboard + context: . + build_args: + - NGINX_CONF=docker/nginx-k8s.conf + cache_from: + - viktorbarzin/trading-bot-dashboard:latest + tags: + - "build-${CI_PIPELINE_NUMBER}" + + - name: publish-images + image: alpine + depends_on: + - build-service-image + - build-dashboard-image + environment: + DOCKERHUB_TOKEN: + from_secret: dockerhub-token + commands: + - apk add --no-cache skopeo + - | + for REPO in trading-bot-service trading-bot-dashboard; do + skopeo copy \ + --src-creds "viktorbarzin:$DOCKERHUB_TOKEN" \ + --dest-creds "viktorbarzin:$DOCKERHUB_TOKEN" \ + "docker://docker.io/viktorbarzin/${REPO}:build-${CI_PIPELINE_NUMBER}" \ + "docker://docker.io/viktorbarzin/${REPO}:${CI_PIPELINE_NUMBER}" + skopeo copy \ + --src-creds "viktorbarzin:$DOCKERHUB_TOKEN" \ + --dest-creds "viktorbarzin:$DOCKERHUB_TOKEN" \ + "docker://docker.io/viktorbarzin/${REPO}:build-${CI_PIPELINE_NUMBER}" \ + "docker://docker.io/viktorbarzin/${REPO}:latest" + done + + - name: update-deployment + image: alpine + depends_on: + - publish-images + commands: + - apk add --no-cache curl jq + - | + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + SERVICE_IMAGE="viktorbarzin/trading-bot-service:${CI_PIPELINE_NUMBER}" + DASHBOARD_IMAGE="viktorbarzin/trading-bot-dashboard:${CI_PIPELINE_NUMBER}" + RESTART_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) + API="https://kubernetes:6443/apis/apps/v1/namespaces/trading-bot/deployments" + + # --- trading-bot-frontend: 2 containers --- + echo "Patching trading-bot-frontend..." + curl -sf -X PATCH "$API/trading-bot-frontend" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/strategic-merge-patch+json" \ + -k -d "{ + \"spec\":{ + \"paused\":null, + \"template\":{ + \"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"$RESTART_AT\"}}, + \"spec\":{\"containers\":[ + {\"name\":\"dashboard\",\"image\":\"$DASHBOARD_IMAGE\"}, + {\"name\":\"api-gateway\",\"image\":\"$SERVICE_IMAGE\"} + ]} + } + } + }" | jq '{name: .metadata.name, generation: .metadata.generation}' + + # --- trading-bot-workers: 6 containers --- + echo "Patching trading-bot-workers..." + curl -sf -X PATCH "$API/trading-bot-workers" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/strategic-merge-patch+json" \ + -k -d "{ + \"spec\":{ + \"paused\":null, + \"template\":{ + \"metadata\":{\"annotations\":{\"kubectl.kubernetes.io/restartedAt\":\"$RESTART_AT\"}}, + \"spec\":{\"containers\":[ + {\"name\":\"news-fetcher\",\"image\":\"$SERVICE_IMAGE\"}, + {\"name\":\"sentiment-analyzer\",\"image\":\"$SERVICE_IMAGE\"}, + {\"name\":\"signal-generator\",\"image\":\"$SERVICE_IMAGE\"}, + {\"name\":\"trade-executor\",\"image\":\"$SERVICE_IMAGE\"}, + {\"name\":\"learning-engine\",\"image\":\"$SERVICE_IMAGE\"}, + {\"name\":\"market-data\",\"image\":\"$SERVICE_IMAGE\"} + ]} + } + } + }" | jq '{name: .metadata.name, generation: .metadata.generation}' + + - name: verify-deploy + image: alpine + depends_on: + - update-deployment + commands: + - apk add --no-cache curl jq + - | + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + EXPECTED_SERVICE="viktorbarzin/trading-bot-service:${CI_PIPELINE_NUMBER}" + EXPECTED_DASHBOARD="viktorbarzin/trading-bot-dashboard:${CI_PIPELINE_NUMBER}" + BASE_API="https://kubernetes:6443/api/v1/namespaces/trading-bot/pods" + DEPLOY_API="https://kubernetes:6443/apis/apps/v1/namespaces/trading-bot/deployments" + + for DEPLOY in trading-bot-frontend trading-bot-workers; do + echo "Verifying $DEPLOY..." + PODS_API="$BASE_API?labelSelector=app%3D$DEPLOY" + + if [ "$DEPLOY" = "trading-bot-frontend" ]; then + EXPECTED_IMAGE="$EXPECTED_DASHBOARD" + else + EXPECTED_IMAGE="$EXPECTED_SERVICE" + fi + + DEPLOY_STATUS=$(curl -sfk "$DEPLOY_API/$DEPLOY" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json") + echo " Deployment status: replicas=$(echo "$DEPLOY_STATUS" | jq -r '.status.replicas // 0') updated=$(echo "$DEPLOY_STATUS" | jq -r '.status.updatedReplicas // 0') ready=$(echo "$DEPLOY_STATUS" | jq -r '.status.readyReplicas // 0')" + + FOUND=0 + for i in $(seq 1 60); do + RAW=$(curl -sfk "$PODS_API" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json") + + if [ "$i" -eq 1 ] || [ "$i" -eq 10 ] || [ "$i" -eq 30 ]; then + echo " DEBUG (attempt $i): All pods for $DEPLOY:" + echo "$RAW" | jq -r '[.items[] | { + name: .metadata.name, + ready: ([.status.containerStatuses[]? | .ready] | all), + phase: .status.phase, + restarts: ([.status.containerStatuses[]? | .restartCount] | add // 0) + }] | .[] | " \(.name) ready=\(.ready) phase=\(.phase) restarts=\(.restarts)"' 2>/dev/null || echo " (no pods or parse error)" + fi + + RESULT=$(echo "$RAW" | \ + jq --arg img "$EXPECTED_IMAGE" '[.items[] | select( + ([.status.containerStatuses[]? | .ready] | all) and + (.spec.containers[]? | .image | endswith($img)) + ) | {name: .metadata.name, started: .status.startTime}]') + + COUNT=$(echo "$RESULT" | jq 'length' 2>/dev/null || echo 0) + echo " Attempt $i/60: $COUNT pod(s) ready with image matching $EXPECTED_IMAGE" + + if [ "$COUNT" -gt 0 ] 2>/dev/null; then + echo "$RESULT" | jq -r '.[] | " \(.name) started=\(.started)"' + echo "$DEPLOY is live!" + FOUND=1 + break + fi + + sleep 5 + done + + if [ "$FOUND" -ne 1 ]; then + echo " FINAL DEBUG: All pods for $DEPLOY:" + echo "$RAW" | jq -r '[.items[] | { + name: .metadata.name, + ready: ([.status.containerStatuses[]? | .ready] | all), + phase: .status.phase, + restarts: ([.status.containerStatuses[]? | .restartCount] | add // 0) + }] | .[] | " \(.name) ready=\(.ready) phase=\(.phase) restarts=\(.restarts)"' 2>/dev/null || echo " (no pods or parse error)" + echo "ERROR: No new ready pod for $DEPLOY with image $EXPECTED_IMAGE appeared within 5 minutes" + exit 1 + fi + done + + - name: slack + image: woodpeckerci/plugin-slack + depends_on: + - verify-deploy + settings: + webhook: + from_secret: slack-webhook-url + channel: general + when: + - status: [success, failure]