diff --git a/.dockerignore b/.dockerignore index 1082ef0..7015ab9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ .codex .claude .cursor +.ci .idea .vscode AGENTS.md diff --git a/.forgejo/actionlint.yaml b/.forgejo/actionlint.yaml new file mode 100644 index 0000000..0735751 --- /dev/null +++ b/.forgejo/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - docker diff --git a/.forgejo/workflows/pipeline.yml b/.forgejo/workflows/pipeline.yml new file mode 100644 index 0000000..9074f8d --- /dev/null +++ b/.forgejo/workflows/pipeline.yml @@ -0,0 +1,181 @@ +name: CI and deploy + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + verify: + runs-on: docker + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Scan secrets + run: | + docker run --rm -v "$PWD:/repo" \ + ghcr.io/gitleaks/gitleaks:v8.30.1 \ + detect --source /repo --redact -v + + - name: Test API + run: | + docker run --rm -v "$PWD/apps/api:/src" -w /src golang:1.26 \ + bash -ec 'go vet ./... && go test -race ./...' + + - name: Test load generator + run: | + docker run --rm -v "$PWD/apps/loadgen:/src" -w /src golang:1.26 \ + bash -ec 'go vet ./... && go test -race ./... && test "$(wc -l < main.go)" -lt 200' + + - name: Lint Go + run: | + docker run --rm -v "$PWD:/repo" -w /repo/apps/api \ + golangci/golangci-lint:v2.12.2 golangci-lint run + docker run --rm -v "$PWD:/repo" -w /repo/apps/loadgen \ + golangci/golangci-lint:v2.12.2 golangci-lint run + + - name: Check reachable Go vulnerabilities + run: | + docker run --rm -v "$PWD/apps/api:/src" -w /src golang:1.26 \ + bash -ec 'go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...' + docker run --rm -v "$PWD/apps/loadgen:/src" -w /src golang:1.26 \ + bash -ec 'go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...' + + - name: Check frontend and shell syntax + run: | + docker run --rm -v "$PWD:/repo" -w /repo node:24-alpine \ + node --check apps/web/app.js + docker run --rm -v "$PWD:/repo" -w /repo bash:5.3 \ + bash -ec 'for file in scripts/k3d/lab.sh scripts/provision/bootstrap.sh scripts/provision/lab.sh; do bash -n "$file"; done' + + - name: Validate Compose and Kubernetes configuration + env: + CLUSTER_API_UPSTREAM: 192.0.2.1:8080 + NEREUS_HOST: nereus.example.test + run: | + docker compose -f compose.yaml config -q + docker compose -f build/compose.mini.yaml config -q + for path in \ + deploy/base \ + deploy/overlays/dev \ + deploy/overlays/prod \ + deploy/rollouts \ + deploy/observability + do + docker run --rm -v "$PWD:/work" -w /work \ + registry.k8s.io/kubectl:v1.33.4 kustomize "$path" >/dev/null + done + + - name: Scan configuration + run: | + docker run --rm -v "$PWD:/repo" -w /repo aquasec/trivy:0.74.0 \ + config --severity HIGH,CRITICAL --exit-code 1 \ + --skip-dirs .git --skip-dirs terraform . + + publish: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: verify + runs-on: docker + env: + API_IMAGE: git.fiwlabs.dev/fiwdev/nereus-api:${{ github.sha }} + LOADGEN_IMAGE: git.fiwlabs.dev/fiwdev/nereus-loadgen:${{ github.sha }} + WEB_IMAGE: git.fiwlabs.dev/fiwdev/nereus-web:${{ github.sha }} + DOCKER_CONFIG: ${{ github.workspace }}/.ci/docker + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Log in to registry + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + install -d -m 0700 "$DOCKER_CONFIG" + printf '%s' "$REGISTRY_PASSWORD" | \ + docker login git.fiwlabs.dev --username "$REGISTRY_USERNAME" --password-stdin + + - name: Build and push images + run: | + docker build -f build/api.Dockerfile \ + --build-arg "APP_VERSION=${{ github.sha }}" \ + -t "$API_IMAGE" . + docker build -f build/loadgen.Dockerfile -t "$LOADGEN_IMAGE" . + docker build -f build/web.Dockerfile -t "$WEB_IMAGE" . + + for image in "$API_IMAGE" "$LOADGEN_IMAGE" "$WEB_IMAGE" + do + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ + aquasec/trivy:0.74.0 image --severity HIGH,CRITICAL \ + --ignore-unfixed --exit-code 1 "$image" + done + + docker push "$API_IMAGE" + docker push "$LOADGEN_IMAGE" + docker push "$WEB_IMAGE" + + - name: Remove registry credentials + if: always() + run: rm -rf .ci + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: publish + runs-on: docker + env: + API_IMAGE: git.fiwlabs.dev/fiwdev/nereus-api:${{ github.sha }} + LOADGEN_IMAGE: git.fiwlabs.dev/fiwdev/nereus-loadgen:${{ github.sha }} + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Prepare cluster access + env: + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }} + run: | + install -d -m 0700 .ci + printf '%s' "$KUBECONFIG_B64" | base64 -d >.ci/kubeconfig + chmod 0600 .ci/kubeconfig + printf '%s' "$REGISTRY_PASSWORD" | \ + docker --config "$PWD/.ci/docker" login git.fiwlabs.dev \ + --username "$REGISTRY_USERNAME" --password-stdin + + - name: Apply and verify rollout + run: | + kube() { + docker run --rm --network host \ + -v "$PWD:/work" -w /work \ + registry.k8s.io/kubectl:v1.33.4 \ + --kubeconfig=/work/.ci/kubeconfig "$@" + } + + kube create namespace nereus --dry-run=client -o yaml | kube apply -f - + kube create secret generic nereus-registry \ + --namespace nereus \ + --type kubernetes.io/dockerconfigjson \ + --from-file=.dockerconfigjson=/work/.ci/docker/config.json \ + --dry-run=client -o yaml | kube apply -f - + + kube get secret nereus-db --namespace nereus >/dev/null + kube apply -k deploy/observability + kube apply -k deploy/overlays/prod + kube patch rollout.argoproj.io nereus-api --namespace nereus --type merge \ + --patch "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"api\",\"image\":\"$API_IMAGE\"}]}}}}" + kube set image deployment/nereus-loadgen --namespace nereus \ + "loadgen=$LOADGEN_IMAGE" + kube set image deployment/nereus-loadgen-preview --namespace nereus \ + "loadgen=$LOADGEN_IMAGE" + + kube rollout status deployment/nereus-loadgen --namespace nereus --timeout=5m + kube rollout status deployment/nereus-loadgen-preview --namespace nereus --timeout=5m + kube wait rollout.argoproj.io/nereus-api --namespace nereus \ + --for=jsonpath='{.status.phase}'=Healthy --timeout=10m + kube get rollout.argoproj.io/nereus-api --namespace nereus + + - name: Remove cluster credentials + if: always() + run: rm -rf .ci diff --git a/.gitignore b/.gitignore index e88d691..4e7c10c 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ CLAUDE.local.md .direnv/ .envrc +.ci/ # --------------------------------------------------------------------------- # Terraform. The lock file is committed on purpose; state never is. diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 0000000..2568adf --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,29 @@ +# Forgejo CI/CD + +`.forgejo/workflows/pipeline.yml` runs verification for pushes and pull +requests. A push to `main` also publishes immutable commit-SHA tags for the API, +load generator, and web images, then deploys the API and load generator through +the production overlay and Argo Rollouts. + +Two load-generator Deployments keep traffic on both the active and preview +Services. Preview traffic is required for the pre-promotion analysis to measure +the candidate revision instead of treating absent samples as success. + +The repository needs a dedicated Forgejo runner labelled `docker`. It must have +Docker with Compose support, outbound access to the configured registries, and +network access to the Kubernetes API endpoint contained in the kubeconfig. +Keep this runner private to trusted repositories because deployment jobs can +access the Docker socket and production credentials. + +Configure these encrypted repository or organization Actions secrets in the +Forgejo UI: + +- `REGISTRY_USERNAME`: account allowed to push the three Nereus packages. +- `REGISTRY_PASSWORD`: package-scoped token for that account. +- `KUBECONFIG_B64`: base64 encoding of a least-privilege deployment kubeconfig. + +The workflow never prints these values. It writes the kubeconfig to the +ephemeral workspace with mode `0600`, removes it in an `always()` step, and +creates the Kubernetes registry pull secret through a pipe so its generated +manifest is not logged or committed. The pre-existing `nereus-db` Secret is +required and is only checked for presence. diff --git a/docs/roadmap.md b/docs/roadmap.md index 033b865..13d2dd9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -209,6 +209,16 @@ task. Mark a task `[x]` only after its stated verification passes. - [H] Maintain Forgejo Actions and GitHub mirror workflows. - [H] Create sealed secrets from off-repository plaintext inputs. +## Forgejo CI/CD + +- [x] Run API and load-generator race tests, vet, and pinned lint in containers. +- [x] Scan committed history and the working tree for secrets. +- [x] Scan release configuration and images for high or critical findings. +- [x] Build immutable commit-SHA API, load-generator, and web image tags. +- [x] Keep active and preview traffic flowing during rollout analysis. +- [~] Run the Forgejo workflow after a private `docker` runner and encrypted secrets are configured. +- [~] Publish images and execute the first automated production rollout. + ## End-to-end acceptance - [ ] Provision two clean Fedora hosts.