diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 00000000..04994372 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,225 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + MIX_ENV: test + ELIXIR_VERSION: "1.19.5" + OTP_VERSION: "28.0" + +jobs: + test: + runs-on: self-hosted + + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_DB: towerops_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + container: + image: hexpm/elixir:1.19.5-erlang-28.0-debian-bookworm-20250113-slim + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix- + + - name: Cache Hex packages + uses: actions/cache@v4 + with: + path: | + ~/.hex + ~/.mix + key: ${{ runner.os }}-hex-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }} + restore-keys: | + ${{ runner.os }}-hex- + + - name: Install system dependencies + run: | + apt-get update -qq + apt-get install -y -qq --no-install-recommends \ + libsnmp-dev snmp-mibs-downloader build-essential git + + - name: Install Hex and Rebar + run: | + mix local.hex --force + mix local.rebar --force + + - name: Install dependencies + run: mix deps.get + + - name: Compile (warnings as errors) + run: mix compile --warnings-as-errors + + - name: Setup database + run: mix ecto.setup + env: + POSTGRES_HOST: postgres + + - name: Run tests + run: mix test --warnings-as-errors --max-cases 8 + env: + POSTGRES_HOST: postgres + + - name: Generate coverage report + run: mix test --cover + env: + POSTGRES_HOST: postgres + + quality: + runs-on: self-hosted + + container: + image: hexpm/elixir:1.19.5-erlang-28.0-debian-bookworm-20250113-slim + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix- + + - name: Cache Hex packages + uses: actions/cache@v4 + with: + path: | + ~/.hex + ~/.mix + key: ${{ runner.os }}-hex-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }} + restore-keys: | + ${{ runner.os }}-hex- + + - name: Cache Dialyzer PLT + uses: actions/cache@v4 + with: + path: priv/plts + key: ${{ runner.os }}-plt-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-plt-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}- + + - name: Install system dependencies + run: | + apt-get update -qq + apt-get install -y -qq --no-install-recommends \ + libsnmp-dev snmp-mibs-downloader build-essential git + + - name: Install Hex and Rebar + run: | + mix local.hex --force + mix local.rebar --force + + - name: Install dependencies + run: mix deps.get + + - name: Compile + run: mix compile --warnings-as-errors + + - name: Check formatting + run: mix format --check-formatted + + - name: Run Credo + run: mix credo --strict + + - name: Run Dialyzer + run: mix dialyzer + continue-on-error: true + + build: + runs-on: self-hosted + needs: test + if: github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ secrets.REGISTRY_URL }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ secrets.REGISTRY_URL }}/graham/towerops-web + tags: | + type=ref,event=branch + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ secrets.REGISTRY_URL }}/graham/towerops-web:latest + cache-to: type=inline + + deploy: + runs-on: self-hosted + needs: build + if: github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup kubectl + run: | + if ! command -v kubectl &> /dev/null; then + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + fi + + - name: Deploy to Kubernetes + run: | + kubectl config use-context towerops/towerops:home-cluster-agent + DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + IMAGE_SHA=$(echo "${{ github.sha }}" | cut -c1-7) + kubectl set image deployment/towerops \ + towerops=${{ secrets.REGISTRY_URL }}/graham/towerops-web:main-${IMAGE_SHA} \ + -n towerops + kubectl set env deployment/towerops \ + DEPLOY_TIMESTAMP=${DEPLOY_TIMESTAMP} \ + -n towerops + env: + KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 44da5556..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,209 +0,0 @@ -# GitLab CI/CD Pipeline for Towerops Web (Forgejo-optimized) -# -# Optimized for self-hosted Forgejo runners with persistent storage: -# - Aggressive caching (deps, builds, system packages persist across jobs) -# - Shared artifacts from compile → test, quality, AND build -# - Minimal redundant work (compile once, use everywhere) -# - Longer artifact retention (persistent runner has storage) -# -# Performance: ~2 minutes with warm cache - -image: hexpm/elixir:1.19.5-erlang-28.0-debian-bookworm-20250113-slim - -variables: - MIX_ENV: test - MIX_HOME: $CI_PROJECT_DIR/.mix - HEX_HOME: $CI_PROJECT_DIR/.hex - # Enable git caching for submodules/deps - GIT_STRATEGY: fetch - GIT_DEPTH: 1 - -stages: - - test - - build - - deploy - -# Persistent caching (long-lived on self-hosted runners) -.deps_cache: &deps_cache - key: - files: - - mix.lock - paths: - - deps/ - - _build/test/ - policy: pull - -.hex_cache: &hex_cache - key: hex-1.19.5-otp-28 - paths: - - .hex/ - - .mix/ - policy: pull - -.system_cache: &system_cache - key: system-deps-v2 - paths: - - apt-cache/ - policy: pull - -# Job templates -.with_system_deps: - before_script: - - mkdir -p apt-cache - - export APT_CACHE_DIR=$CI_PROJECT_DIR/apt-cache - - apt-get update -qq - - apt-get install -y -qq --no-install-recommends - libsnmp-dev snmp-mibs-downloader build-essential - -o dir::cache::archives=$APT_CACHE_DIR - - mix local.hex --force - - mix local.rebar --force - -.elixir_only: - before_script: - - mix local.hex --force - - mix local.rebar --force - -# Warm up caches (manual or scheduled) -warm_cache: - extends: .with_system_deps - stage: test - cache: - - <<: *hex_cache - policy: pull-push - - <<: *deps_cache - policy: pull-push - - <<: *system_cache - policy: pull-push - script: - - mix deps.get - - mix compile - rules: - - if: $CI_PIPELINE_SOURCE == "schedule" - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH - changes: - - mix.lock - - when: manual - allow_failure: true - -# Compile once, use everywhere -compile: - extends: .with_system_deps - stage: test - cache: - - *hex_cache - - *deps_cache - - *system_cache - script: - - mix deps.get - - mix compile --warnings-as-errors - artifacts: - paths: - - deps/ - - _build/test/ - - .mix/ - - .hex/ - expire_in: 1 day # Longer on self-hosted - untracked: false - -# Test with PostgreSQL -test: - extends: .elixir_only - stage: test - needs: - - job: compile - artifacts: true - services: - - postgres:17-alpine - variables: - POSTGRES_DB: towerops_test - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_HOST: postgres - cache: [] # Use artifacts only - script: - - mix ecto.setup - - mix test --warnings-as-errors --max-cases 8 - coverage: '/\d+\.\d+\%\s+\|\s+Total/' - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: coverage.xml - when: always - expire_in: 7 days - -# Code quality (parallel with test) -quality: - extends: .elixir_only - stage: test - needs: - - job: compile - artifacts: true - cache: - - key: dialyzer-plt-1.19.5-otp-28 - paths: - - priv/plts/ - policy: pull-push - script: - - mix format --check-formatted - - mix credo --strict - - mix dialyzer - allow_failure: true - -# Docker build (uses BuildKit cache mounts for MIX_ENV=prod) -build: - stage: build - needs: - - job: test - artifacts: false # Just wait for tests to pass - tags: - - docker - image: docker:24 - services: - - docker:24-dind - variables: - DOCKER_BUILDKIT: "1" - BUILDKIT_PROGRESS: plain - before_script: - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY - script: - # Build with layer caching (persistent runner retains Docker cache) - - docker build - --build-arg BUILDKIT_INLINE_CACHE=1 - --cache-from $CI_REGISTRY_IMAGE:latest - -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG . - - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG - - | - if [ "$CI_COMMIT_BRANCH" = "main" ]; then - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest - docker push $CI_REGISTRY_IMAGE:latest - fi - rules: - - if: $CI_COMMIT_BRANCH == "main" - - if: $CI_COMMIT_TAG - -# Deploy to Kubernetes -deploy: - stage: deploy - needs: - - job: build - artifacts: false - tags: - - home - image: - name: bitnami/kubectl:latest - entrypoint: [""] - script: - - kubectl config use-context towerops/towerops:home-cluster-agent - - DEPLOY_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - - kubectl set image deployment/towerops towerops=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n towerops - - kubectl set env deployment/towerops DEPLOY_TIMESTAMP=$DEPLOY_TIMESTAMP -n towerops - environment: - name: production - kubernetes: - namespace: towerops - rules: - - if: $CI_COMMIT_BRANCH == "main" - when: manual