From 6995763ccb41b7c3973d98464b3b1cb84da9850e Mon Sep 17 00:00:00 2001 From: zwitschi Date: Sun, 2 Aug 2026 17:45:14 +0200 Subject: [PATCH] add CI/CD workflows, production docker-compose, and update dev compose with Gitea registry and monitoring improvements --- .env.example | 13 +- .gitea/workflows/cd.yml | 49 +++++++ .gitea/workflows/ci.yml | 172 ++++++++++++++++++++++++ .gitea/workflows/dependency-updates.yml | 58 ++++++++ .gitea/workflows/release.yml | 120 +++++++++++++++++ docker-compose.prod.yml | 143 ++++++++++++++++++++ docker-compose.yml | 30 ++++- 7 files changed, 580 insertions(+), 5 deletions(-) create mode 100644 .gitea/workflows/cd.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/dependency-updates.yml create mode 100644 .gitea/workflows/release.yml create mode 100644 docker-compose.prod.yml diff --git a/.env.example b/.env.example index 180f156..e1f1481 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,12 @@ CLOUDFLARE_API_TOKEN=your-cloudflare-api-token CLOUDFLARE_ACCOUNT_ID=your-account-id MEDIA_DOMAIN=media.example.com +# Gitea Container Registry (for self-hosted Docker images) +GITEA_REGISTRY_URL=git.allucanget.biz +GITEA_USERNAME=your-gitea-username +GITEA_TOKEN=your-gitea-token +GITEA_REGISTRY_NAMESPACE=zwitschi/linearvoid + # Chat / Application Database (PostgreSQL or SQLite for local) DATABASE_URL=postgres://user:password@localhost:5432/linearvoid # Or for SQLite (local development): @@ -39,4 +45,9 @@ GRAFANA_PORT=3001 # Development flags NODE_ENV=development -GO_ENV=development \ No newline at end of file +GO_ENV=development + +# Gitea Configuration +GITEA_SERVER_URL=https://git.allucanget.biz +GITEA_TOKEN=your-gitea-token +GITEA_USERNAME=your-gitea-username \ No newline at end of file diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml new file mode 100644 index 0000000..35a2e4f --- /dev/null +++ b/.gitea/workflows/cd.yml @@ -0,0 +1,49 @@ +name: CD + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: [self-hosted, linux, docker] + environment: production + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.allucanget.biz + username: ${{ secrets.GITEA_USERNAME }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Pull latest images + run: | + docker pull git.allucanget.biz/zwitschi/linearvoid/playout:latest || true + docker pull git.allucanget.biz/zwitschi/linearvoid/chat:latest || true + docker pull git.allucanget.biz/zwitschi/linearvoid/frontend:latest || true + docker pull git.allucanget.biz/zwitschi/linearvoid/nginx:latest || true + + - name: Deploy with docker-compose + run: | + docker compose -f docker-compose.prod.yml pull + docker compose -f docker-compose.prod.yml up -d --remove-orphans + + - name: Health check + run: | + sleep 10 + curl -f http://localhost:8080/health || exit 1 + curl -f http://localhost:3000/health || exit 1 + curl -f http://localhost:8081/health || exit 1 + + - name: Cleanup old images + run: | + docker image prune -f --filter "until=24h" diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..b38e82c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,172 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: [self-hosted, linux, docker] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: frontend/package-lock.json + + - name: Install Frontend Dependencies + run: cd frontend && npm ci + + - name: Lint Frontend + run: cd frontend && npm run lint || echo "Linting not configured yet" + + - name: Build Frontend + run: cd frontend && npm run build + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.22" + cache: true + + - name: Validate Playlist Normalization + run: cd backend/playout && go run src/playlist.go playlist.txt || echo "Validation script not yet complete" + + - name: Build Docker Images + run: docker compose build --no-cache + + - name: Run Tests + run: echo "Tests will be added in future CI steps (per TODO.md)" + + docker: + needs: test + runs-on: [self-hosted, linux, docker] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.allucanget.biz + username: ${{ secrets.GITEA_USERNAME }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract metadata for Playout + id: meta-playout + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/playout + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Playout image + uses: docker/build-push-action@v5 + with: + context: ./backend/playout + push: true + tags: ${{ steps.meta-playout.outputs.tags }} + labels: ${{ steps.meta-playout.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Extract metadata for Chat + id: meta-chat + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/chat + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Chat image + uses: docker/build-push-action@v5 + with: + context: ./backend/chat + push: true + tags: ${{ steps.meta-chat.outputs.tags }} + labels: ${{ steps.meta-chat.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Extract metadata for Frontend + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/frontend + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Frontend image + uses: docker/build-push-action@v5 + with: + context: ./frontend + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Extract metadata for Nginx + id: meta-nginx + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/nginx + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Nginx image + uses: docker/build-push-action@v5 + with: + context: ./infrastructure/nginx + push: true + tags: ${{ steps.meta-nginx.outputs.tags }} + labels: ${{ steps.meta-nginx.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + release: + needs: docker + runs-on: [self-hosted, linux, docker] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }} diff --git a/.gitea/workflows/dependency-updates.yml b/.gitea/workflows/dependency-updates.yml new file mode 100644 index 0000000..8e50f31 --- /dev/null +++ b/.gitea/workflows/dependency-updates.yml @@ -0,0 +1,58 @@ +name: Dependency Updates + +on: + schedule: + - cron: '0 2 * * 1' # Every Monday at 2 AM UTC + workflow_dispatch: + +jobs: + update-dependencies: + runs-on: [self-hosted, linux, docker] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITEA_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Update Frontend Dependencies + run: | + cd frontend + npm update + npm audit fix || true + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + cache: true + + - name: Update Go Dependencies + run: | + cd backend/playout && go get -u ./... && go mod tidy + cd ../chat && go get -u ./... && go mod tidy + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITEA_TOKEN }} + commit-message: 'chore: update dependencies' + title: 'chore: weekly dependency updates' + body: | + Automated weekly dependency updates. + + This PR updates: + - Frontend npm dependencies + - Go module dependencies + + Please review the changes and ensure tests pass before merging. + branch: dependency-updates + delete-branch: true + labels: dependencies, automated \ No newline at end of file diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..c95c1bc --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,120 @@ +name: Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Version to release (e.g., v1.0.0)" + required: true + type: string + +jobs: + release: + runs-on: [self-hosted, linux, docker] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.allucanget.biz + username: ${{ secrets.GITEA_USERNAME }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Extract version + id: version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + else + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Extract metadata for Playout + id: meta-playout + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/playout + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push Playout image + uses: docker/build-push-action@v5 + with: + context: ./backend/playout + push: true + tags: ${{ steps.meta-playout.outputs.tags }} + labels: ${{ steps.meta-playout.outputs.labels }} + + - name: Extract metadata for Chat + id: meta-chat + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/chat + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push Chat image + uses: docker/build-push-action@v5 + with: + context: ./backend/chat + push: true + tags: ${{ steps.meta-chat.outputs.tags }} + labels: ${{ steps.meta-chat.outputs.labels }} + + - name: Extract metadata for Frontend + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/frontend + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push Frontend image + uses: docker/build-push-action@v5 + with: + context: ./frontend + push: true + tags: ${{ steps.meta-frontend.outputs.tags }} + labels: ${{ steps.meta-frontend.outputs.labels }} + + - name: Extract metadata for Nginx + id: meta-nginx + uses: docker/metadata-action@v5 + with: + images: git.allucanget.biz/zwitschi/linearvoid/nginx + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push Nginx image + uses: docker/build-push-action@v5 + with: + context: ./infrastructure/nginx + push: true + tags: ${{ steps.meta-nginx.outputs.tags }} + labels: ${{ steps.meta-nginx.outputs.labels }} + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + tag_name: ${{ steps.version.outputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..e59c868 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,143 @@ +services: + playout: + image: git.allucanget.biz/zwitschi/linearvoid/playout:latest + container_name: linearvoid-playout + restart: unless-stopped + env_file: + - .env + volumes: + - ./data/playlist:/app/playlist:ro + - ./data/media:/app/media:ro + - ./data/hls:/app/hls:rw + networks: + - linearvoid-net + depends_on: + - redis + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + chat: + image: git.allucanget.biz/zwitschi/linearvoid/chat:latest + container_name: linearvoid-chat + restart: unless-stopped + env_file: + - .env + ports: + - "${CHAT_PORT:-3000}:3000" + volumes: + - ./data/chat:/app:rw + networks: + - linearvoid-net + depends_on: + - redis + - db + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + nginx: + image: git.allucanget.biz/zwitschi/linearvoid/nginx:latest + container_name: linearvoid-nginx + restart: unless-stopped + ports: + - "${NGINX_PORT:-8080}:80" + volumes: + - ./data/hls:/usr/share/nginx/html/hls:ro + networks: + - linearvoid-net + depends_on: + - playout + - chat + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + redis: + image: redis:7-alpine + container_name: linearvoid-redis + restart: unless-stopped + command: redis-server --appendonly yes + volumes: + - redis-data:/data + networks: + - linearvoid-net + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + db: + image: postgres:15-alpine + container_name: linearvoid-db + restart: unless-stopped + environment: + POSTGRES_DB: linearvoid + POSTGRES_USER: linearvoid + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-linearvoid} + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - linearvoid-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U linearvoid"] + interval: 10s + timeout: 5s + retries: 5 + + prometheus: + image: prom/prometheus:latest + container_name: linearvoid-prometheus + restart: unless-stopped + volumes: + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus + ports: + - "9090:9090" + networks: + - linearvoid-net + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/etc/prometheus/console_libraries" + - "--web.console.templates=/etc/prometheus/consoles" + - "--web.enable-lifecycle" + + grafana: + image: grafana/grafana:latest + container_name: linearvoid-grafana + restart: unless-stopped + ports: + - "3001:3000" + volumes: + - grafana-data:/var/lib/grafana + - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards + - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin} + - GF_USERS_ALLOW_SIGN_UP=false + networks: + - linearvoid-net + depends_on: + - prometheus + +networks: + linearvoid-net: + driver: bridge + +volumes: + redis-data: + postgres-data: + prometheus-data: + grafana-data: diff --git a/docker-compose.yml b/docker-compose.yml index fca92da..3dd043f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: "3.9" services: playout: + image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/playout:latest build: context: ./backend/playout dockerfile: Dockerfile @@ -25,6 +26,7 @@ services: start_period: 10s chat: + image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/chat:latest build: context: ./backend/chat dockerfile: Dockerfile @@ -35,7 +37,7 @@ services: ports: - "${CHAT_PORT:-3000}:3000" volumes: - - ./data/chat:/app:rw # SQLite database and logs + - ./data/chat:/app:rw networks: - linearvoid-net depends_on: @@ -49,6 +51,7 @@ services: start_period: 10s nginx: + image: ${GITEA_REGISTRY_URL:-git.allucanget.biz}/${GITEA_REGISTRY_NAMESPACE:-zwitschi/linearvoid}/nginx:latest build: context: ./infrastructure/nginx dockerfile: Dockerfile @@ -82,31 +85,48 @@ services: environment: POSTGRES_DB: linearvoid POSTGRES_USER: linearvoid - POSTGRES_PASSWORD: linearvoid + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-linearvoid} volumes: - postgres-data:/var/lib/postgresql/data networks: - linearvoid-net prometheus: - image: prom/prometheus + image: prom/prometheus:latest container_name: linearvoid-prometheus restart: unless-stopped volumes: - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus-data:/prometheus ports: - "9090:9090" networks: - linearvoid-net + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--web.console.libraries=/etc/prometheus/console_libraries" + - "--web.console.templates=/etc/prometheus/consoles" + - "--web.enable-lifecycle" grafana: - image: grafana/grafana + image: grafana/grafana:latest container_name: linearvoid-grafana restart: unless-stopped ports: - "3001:3000" + volumes: + - grafana-data:/var/lib/grafana + - ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards + - ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin} + - GF_USERS_ALLOW_SIGN_UP=false networks: - linearvoid-net + depends_on: + - prometheus networks: linearvoid-net: @@ -115,3 +135,5 @@ networks: volumes: redis-data: postgres-data: + prometheus-data: + grafana-data: