Skip to content

fix: make api handlers public, rename events/id path to avoid route c… #44

fix: make api handlers public, rename events/id path to avoid route c…

fix: make api handlers public, rename events/id path to avoid route c… #44

Workflow file for this run

name: Build & Deploy to Civo K3s
on:
push:
branches: [main]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}
jobs:
# ── Detect which files changed ────────────────────────────────────────────
changes:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
# Shared Rust lib/deps — triggers ALL Rust binary rebuilds
rust-shared: ${{ steps.filter.outputs.rust-shared }}
# Per-binary source changes
tracker-core: ${{ steps.filter.outputs.tracker-core }}
sse-gateway: ${{ steps.filter.outputs.sse-gateway }}
webhook-consumer: ${{ steps.filter.outputs.webhook-consumer }}
risingwave-consumer: ${{ steps.filter.outputs.risingwave-consumer }}
r2-archiver: ${{ steps.filter.outputs.r2-archiver }}
event-filter: ${{ steps.filter.outputs.event-filter }}
# Python/Node services
polars-query: ${{ steps.filter.outputs.polars-query }}
polars-lite: ${{ steps.filter.outputs.polars-lite }}
ai-query: ${{ steps.filter.outputs.ai-query }}
# K8s manifests (deploy-only, no rebuild)
k8s: ${{ steps.filter.outputs.k8s }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
rust-shared:
- 'Cargo.toml'
- 'Cargo.lock'
- 'src/lib.rs'
- 'src/config.rs'
- 'src/crypto.rs'
- 'src/event.rs'
- 'src/health.rs'
- 'src/producer.rs'
- 'src/ingest_token_cache.rs'
- 'src/rate_limiter.rs'
- 'src/tenant_cache.rs'
- 'src/tracking_url_cache.rs'
- 'docker/Dockerfile.core'
- 'packages/aggregate-schema/**'
tracker-core:
- 'src/main.rs'
- 'src/routes.rs'
sse-gateway:
- 'src/bin/sse_gateway.rs'
webhook-consumer:
- 'src/bin/webhook_consumer.rs'
risingwave-consumer:
- 'src/bin/risingwave_consumer.rs'
r2-archiver:
- 'src/bin/r2_archiver.rs'
event-filter:
- 'src/bin/event_filter.rs'
polars-query:
- 'packages/polars-query/**'
- 'docker/Dockerfile.polars-query'
polars-lite:
- 'packages/polars-lite/**'
- 'docker/Dockerfile.polars-lite'
ai-query:
- 'packages/ai-query/**'
- 'docker/Dockerfile.ai-query'
k8s:
- 'k8s/**'
# ── Build Rust binaries (only when their source or shared deps change) ────
build-tracker-core:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.rust-shared == 'true' ||
needs.changes.outputs.tracker-core == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.core
build-args: BINARY=tracker-core
push: true
tags: |
${{ env.IMAGE_PREFIX }}/tracker-core:latest
${{ env.IMAGE_PREFIX }}/tracker-core:${{ github.sha }}
cache-from: type=gha,scope=tracker-core
cache-to: type=gha,mode=max,scope=tracker-core
build-sse-gateway:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.rust-shared == 'true' ||
needs.changes.outputs.sse-gateway == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.core
build-args: BINARY=sse-gateway
push: true
tags: |
${{ env.IMAGE_PREFIX }}/sse-gateway:latest
${{ env.IMAGE_PREFIX }}/sse-gateway:${{ github.sha }}
cache-from: type=gha,scope=sse-gateway
cache-to: type=gha,mode=max,scope=sse-gateway
build-webhook-consumer:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.rust-shared == 'true' ||
needs.changes.outputs.webhook-consumer == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.core
build-args: BINARY=webhook-consumer
push: true
tags: |
${{ env.IMAGE_PREFIX }}/webhook-consumer:latest
${{ env.IMAGE_PREFIX }}/webhook-consumer:${{ github.sha }}
cache-from: type=gha,scope=webhook-consumer
cache-to: type=gha,mode=max,scope=webhook-consumer
build-risingwave-consumer:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.rust-shared == 'true' ||
needs.changes.outputs.risingwave-consumer == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.core
build-args: BINARY=risingwave-consumer
push: true
tags: |
${{ env.IMAGE_PREFIX }}/risingwave-consumer:latest
${{ env.IMAGE_PREFIX }}/risingwave-consumer:${{ github.sha }}
cache-from: type=gha,scope=risingwave-consumer
cache-to: type=gha,mode=max,scope=risingwave-consumer
build-r2-archiver:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.rust-shared == 'true' ||
needs.changes.outputs.r2-archiver == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.core
build-args: BINARY=r2-archiver
push: true
tags: |
${{ env.IMAGE_PREFIX }}/r2-archiver:latest
${{ env.IMAGE_PREFIX }}/r2-archiver:${{ github.sha }}
cache-from: type=gha,scope=r2-archiver
cache-to: type=gha,mode=max,scope=r2-archiver
build-event-filter:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.rust-shared == 'true' ||
needs.changes.outputs.event-filter == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.core
build-args: BINARY=event-filter
push: true
tags: |
${{ env.IMAGE_PREFIX }}/event-filter:latest
${{ env.IMAGE_PREFIX }}/event-filter:${{ github.sha }}
cache-from: type=gha,scope=event-filter
cache-to: type=gha,mode=max,scope=event-filter
# ── Build Python/Node services (only when changed) ────────────────────────
build-polars-query:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.polars-query == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.polars-query
push: true
tags: |
${{ env.IMAGE_PREFIX }}/polars-query:latest
${{ env.IMAGE_PREFIX }}/polars-query:${{ github.sha }}
cache-from: type=gha,scope=polars-query
cache-to: type=gha,mode=max,scope=polars-query
build-polars-lite:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.polars-lite == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.polars-lite
push: true
tags: |
${{ env.IMAGE_PREFIX }}/polars-lite:latest
${{ env.IMAGE_PREFIX }}/polars-lite:${{ github.sha }}
cache-from: type=gha,scope=polars-lite
cache-to: type=gha,mode=max,scope=polars-lite
build-ai-query:
needs: changes
if: >-
github.event_name == 'workflow_dispatch' ||
needs.changes.outputs.ai-query == 'true'
runs-on: ubuntu-latest
permissions: { contents: read, packages: write }
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" }
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.ai-query
push: true
tags: |
${{ env.IMAGE_PREFIX }}/ai-query:latest
${{ env.IMAGE_PREFIX }}/ai-query:${{ github.sha }}
cache-from: type=gha,scope=ai-query
cache-to: type=gha,mode=max,scope=ai-query
# ── Deploy to Civo K3s ────────────────────────────────────────────────────
# Runs when ANY build completes or K8s manifests change.
# Skipped builds keep their previous :latest tag — only rebuilt images get pinned to :sha.
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
# Run unless a build FAILED (skipped builds are fine)
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') }}
needs:
- changes
- build-tracker-core
- build-sse-gateway
- build-webhook-consumer
- build-risingwave-consumer
- build-r2-archiver
- build-event-filter
- build-polars-query
- build-polars-lite
- build-ai-query
steps:
- uses: actions/checkout@v4
- name: Install kubectl
uses: azure/setup-kubectl@v3
- name: Set kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.CIVO_KUBECONFIG }}" | base64 -d > $HOME/.kube/config
- name: Create/update ghcr.io pull secret
run: |
kubectl -n tracker delete secret ghcr-pull-secret --ignore-not-found
kubectl -n tracker create secret docker-registry ghcr-pull-secret \
--docker-server=ghcr.io \
--docker-username=${{ github.actor }} \
--docker-password=${{ secrets.GHCR_PAT }}
- name: Create/update tracker-secrets
run: |
kubectl -n tracker delete secret tracker-secrets --ignore-not-found
kubectl -n tracker create secret generic tracker-secrets \
--from-literal=HMAC_SECRET="${{ secrets.HMAC_SECRET }}" \
--from-literal=PLATFORM_API_KEY="${{ secrets.PLATFORM_API_KEY }}" \
--from-literal=ADMIN_API_KEY="${{ secrets.ADMIN_API_KEY }}" \
--from-literal=TURSO_URL="${{ secrets.TURSO_URL }}" \
--from-literal=TURSO_AUTH_TOKEN="${{ secrets.TURSO_AUTH_TOKEN }}" \
--from-literal=RISINGWAVE_URL="${{ secrets.RISINGWAVE_URL }}" \
--from-literal=R2_ENDPOINT="${{ secrets.R2_ENDPOINT }}" \
--from-literal=R2_ACCESS_KEY_ID="${{ secrets.R2_ACCESS_KEY_ID }}" \
--from-literal=R2_SECRET_ACCESS_KEY="${{ secrets.R2_SECRET_ACCESS_KEY }}" \
--from-literal=LANCEDB_API_KEY="${{ secrets.LANCEDB_API_KEY }}" \
--from-literal=SLM_API_KEY="${{ secrets.SLM_API_KEY }}"
- name: Pin image tags for rebuilt services only
env:
SHA: ${{ github.sha }}
# Job results: 'success' if built, 'skipped' if path filter didn't match
R_TRACKER_CORE: ${{ needs.build-tracker-core.result }}
R_SSE_GATEWAY: ${{ needs.build-sse-gateway.result }}
R_WEBHOOK: ${{ needs.build-webhook-consumer.result }}
R_RISINGWAVE: ${{ needs.build-risingwave-consumer.result }}
R_R2: ${{ needs.build-r2-archiver.result }}
R_FILTER: ${{ needs.build-event-filter.result }}
R_POLARS_Q: ${{ needs.build-polars-query.result }}
R_POLARS_L: ${{ needs.build-polars-lite.result }}
R_AI: ${{ needs.build-ai-query.result }}
run: |
cd k8s
pin() {
local svc=$1 result=$2
if [ "$result" = "success" ]; then
echo " → pinning $svc to :$SHA"
sed -i "s|ghcr.io/inventhq/${svc}:latest|ghcr.io/inventhq/${svc}:${SHA}|g" *.yaml
else
echo " → $svc skipped (keeping :latest)"
fi
}
pin "tracker-core" "$R_TRACKER_CORE"
pin "sse-gateway" "$R_SSE_GATEWAY"
pin "webhook-consumer" "$R_WEBHOOK"
pin "risingwave-consumer" "$R_RISINGWAVE"
pin "r2-archiver" "$R_R2"
pin "event-filter" "$R_FILTER"
pin "polars-query" "$R_POLARS_Q"
pin "polars-lite" "$R_POLARS_L"
pin "ai-query" "$R_AI"
- name: Apply manifests
run: |
kubectl apply -k k8s/
kubectl apply -f k8s/cert-manager.yaml
kubectl apply -f k8s/ingress.yaml
- name: Wait for rollout
run: |
kubectl -n tracker rollout status deployment/tracker-core --timeout=300s
kubectl -n tracker rollout status deployment/sse-gateway --timeout=300s
kubectl -n tracker rollout status deployment/event-filter --timeout=300s
kubectl -n tracker rollout status deployment/webhook-consumer --timeout=300s
kubectl -n tracker rollout status deployment/risingwave-consumer --timeout=300s
kubectl -n tracker rollout status deployment/r2-archiver --timeout=300s
kubectl -n tracker rollout status deployment/polars-query --timeout=300s
kubectl -n tracker rollout status deployment/polars-lite --timeout=300s
kubectl -n tracker rollout status deployment/ai-query --timeout=300s
kubectl -n tracker rollout status statefulset/iggy --timeout=300s
- name: Verify health
run: |
sleep 10
kubectl -n tracker get pods
kubectl -n tracker get svc