From 67e172c80062095f092e904f9bbbdf94e6bcc09e Mon Sep 17 00:00:00 2001 From: ced4568 Date: Tue, 18 Aug 2026 17:13:25 -0500 Subject: [PATCH] feat(cicd): Gitea Actions pipeline, deterministic image tagging Automatic test/build/tag/staging-deploy on push to main; production promotion stays a manual, human-run step (deploy-production.sh) - never automated. SHA-tagged images always, semantic tags only on deliberate release, never :latest alone in staging/production. No paid CI provider or registry required (uses Gitea's own). Real regression job installs all services, applies real migrations against a service Postgres, runs the infra validation scripts. Validated as real YAML (python3 yaml parse). --- infra/ci/gitea-workflows/build-and-test.yml | 111 ++++++++++++++++++++ infra/scripts/ci/tag-and-push.sh | 42 ++++++++ 2 files changed, 153 insertions(+) create mode 100644 infra/ci/gitea-workflows/build-and-test.yml create mode 100755 infra/scripts/ci/tag-and-push.sh diff --git a/infra/ci/gitea-workflows/build-and-test.yml b/infra/ci/gitea-workflows/build-and-test.yml new file mode 100644 index 0000000..ed47751 --- /dev/null +++ b/infra/ci/gitea-workflows/build-and-test.yml @@ -0,0 +1,111 @@ +# Gitea Actions workflow — build/test on every push to main. Gitea Actions uses the same +# workflow syntax as GitHub Actions (a compatible runner), so this is portable if the canonical +# remote ever needs to move CI providers, without being GitHub Actions itself or requiring a +# paid CI provider. Runs against the canonical Gitea repo only — see docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md +# §18 for why this never touches the GitHub kubekosh fork. +# +# Deploy this file to .gitea/workflows/build-and-test.yml at the repo root to activate it (kept +# under infra/ci/ in this PR so it's reviewed alongside the rest of the infra package first, +# per the same "build now, activate deliberately" posture as everything else server-dependent +# in this phase — enabling Gitea Actions on the instance is itself a one-time admin step that +# may not be done yet). + +name: build-and-test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + regression: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_PASSWORD: postgres + ports: ["5432:5432"] + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s --health-timeout 5s --health-retries 10 + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies (all first-party services) + run: | + for d in backend frontend platform/api platform/gateway platform/provisioner; do + (cd "$d" && npm ci) + done + + - name: Syntax-check every service + run: | + for d in backend frontend/src platform/api platform/gateway platform/provisioner; do + find "$d" -name '*.js' -not -path '*/node_modules/*' -exec node --check {} \; + done + + - name: Build frontend + run: (cd frontend && npm run build) + + - name: Provisioner migrations against the service Postgres + run: | + cd platform/provisioner + PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5432/postgres \ + node -e "require('./src/db').runMigrations().then(m=>console.log('Applied:',m.join(', ')))" + + - name: Platform API migrations (depends on Provisioner's, same order as production) + run: | + cd platform/api + PLATFORM_DB_URL=postgres://postgres:postgres@localhost:5432/postgres \ + node -e "require('./src/db').runMigrations().then(m=>console.log('Applied:',m.join(', ')))" + + - name: Validate infra configs (Phase 4 local tests — no hardware needed) + run: | + ./infra/scripts/validate-compose.sh + ./infra/firewall/validate-firewall.sh + + # The real k3s-boot-dependent e2e suites (platform/test/e2e-phase*.js) need a full Docker + # Compose stack with --privileged Lab containers, which a shared CI runner typically + # can't grant safely — they stay a LOCAL/staging pre-promotion step + # (infra/scripts/staging-smoke-test.sh), not this job. See docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md §18. + + build-images: + needs: regression + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build and tag images + run: ./infra/scripts/ci/tag-and-push.sh + env: + REGISTRY: ${{ vars.REGISTRY }} + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + + deploy-staging: + needs: build-images + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Deploy to staging (automatic) + run: | + cd infra/ansible + ansible-playbook site.yml -i inventory/staging.ini \ + -e "image_tag=staging-${GITHUB_SHA::12}" \ + --vault-password-file <(echo "$VAULT_PASSWORD") + env: + VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} + + - name: Staging smoke test + run: ./infra/scripts/staging-smoke-test.sh + + # Production is NEVER automatic — a human runs infra/scripts/deploy-production.sh after + # reviewing the staging smoke test result, per the explicit "manual approval step" preference. + # No job here does that; see docs/RUNBOOK_NORMAL_DEPLOY.md for the actual command. diff --git a/infra/scripts/ci/tag-and-push.sh b/infra/scripts/ci/tag-and-push.sh new file mode 100755 index 0000000..6b33bcf --- /dev/null +++ b/infra/scripts/ci/tag-and-push.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Builds and pushes all five RootNode images, tagged by git SHA (always) and, on an explicit +# release, a semantic tag too. See infra/docker/IMAGE_STRATEGY.md for the full policy — this +# script is just the mechanical implementation of it. +# +# Required env: REGISTRY, REGISTRY_USER, REGISTRY_PASSWORD +# Optional env: RELEASE_TAG (e.g. v1.2.0 — if set, also tags+pushes this on top of the sha tag) +set -euo pipefail +cd "$(dirname "$0")/../../.." # repo root + +: "${REGISTRY:?REGISTRY is required}" +: "${REGISTRY_USER:?REGISTRY_USER is required}" +: "${REGISTRY_PASSWORD:?REGISTRY_PASSWORD is required}" + +SHA="$(git rev-parse HEAD)" +SHA_TAG="sha-${SHA:0:12}" + +echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin + +declare -A IMAGES=( + [rootnode/platform-api]="platform/api/Dockerfile ." + [rootnode/gateway]="platform/gateway/Dockerfile platform/gateway" + [rootnode/provisioner]="platform/provisioner/Dockerfile platform/provisioner" + [rootnode/egress-proxy]="platform/egress-proxy/Dockerfile platform/egress-proxy" + [rootnode/lab]="Dockerfile ." +) + +for image in "${!IMAGES[@]}"; do + read -r dockerfile context <<< "${IMAGES[$image]}" + full="$REGISTRY/$image" + echo "==> Building $full:$SHA_TAG (context: $context, dockerfile: $dockerfile)" + docker build -f "$dockerfile" -t "$full:$SHA_TAG" "$context" + docker push "$full:$SHA_TAG" + + if [ -n "${RELEASE_TAG:-}" ]; then + docker tag "$full:$SHA_TAG" "$full:$RELEASE_TAG" + docker push "$full:$RELEASE_TAG" + fi +done + +echo "==> Done. Built tag: $SHA_TAG${RELEASE_TAG:+, $RELEASE_TAG}" +echo "$SHA_TAG" > /tmp/rootnode-last-built-tag # convenience for a following CI step