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).
This commit is contained in:
@@ -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.
|
||||||
Executable
+42
@@ -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
|
||||||
Reference in New Issue
Block a user