ci: publish and deploy verified releases
This commit is contained in:
parent
3fa278d447
commit
99fde46ed3
6 changed files with 225 additions and 0 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
.codex
|
.codex
|
||||||
.claude
|
.claude
|
||||||
.cursor
|
.cursor
|
||||||
|
.ci
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
|
|
||||||
3
.forgejo/actionlint.yaml
Normal file
3
.forgejo/actionlint.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
self-hosted-runner:
|
||||||
|
labels:
|
||||||
|
- docker
|
||||||
181
.forgejo/workflows/pipeline.yml
Normal file
181
.forgejo/workflows/pipeline.yml
Normal file
|
|
@ -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
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -75,6 +75,7 @@ CLAUDE.local.md
|
||||||
|
|
||||||
.direnv/
|
.direnv/
|
||||||
.envrc
|
.envrc
|
||||||
|
.ci/
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Terraform. The lock file is committed on purpose; state never is.
|
# Terraform. The lock file is committed on purpose; state never is.
|
||||||
|
|
|
||||||
29
docs/ci-cd.md
Normal file
29
docs/ci-cd.md
Normal file
|
|
@ -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.
|
||||||
|
|
@ -209,6 +209,16 @@ task. Mark a task `[x]` only after its stated verification passes.
|
||||||
- [H] Maintain Forgejo Actions and GitHub mirror workflows.
|
- [H] Maintain Forgejo Actions and GitHub mirror workflows.
|
||||||
- [H] Create sealed secrets from off-repository plaintext inputs.
|
- [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
|
## End-to-end acceptance
|
||||||
|
|
||||||
- [ ] Provision two clean Fedora hosts.
|
- [ ] Provision two clean Fedora hosts.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue