From d3ee64282dc065f0559d0b452380e6666b0c0fa1 Mon Sep 17 00:00:00 2001 From: Daniel Noulet Date: Thu, 8 Oct 2026 10:36:33 +0200 Subject: [PATCH] ci: add Forgejo build and deployment workflows --- .forgejo/scripts/deploy-prd.sh | 112 +++++++++++++++++ .forgejo/workflows/ci-cd.yaml | 211 +++++++++----------------------- deploy/helm/go-loose/Chart.lock | 6 + deploy/helm/go-loose/Chart.yaml | 7 +- 4 files changed, 181 insertions(+), 155 deletions(-) create mode 100644 .forgejo/scripts/deploy-prd.sh create mode 100644 deploy/helm/go-loose/Chart.lock diff --git a/.forgejo/scripts/deploy-prd.sh b/.forgejo/scripts/deploy-prd.sh new file mode 100644 index 0000000..8002b39 --- /dev/null +++ b/.forgejo/scripts/deploy-prd.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +NAMESPACE=go-insane-prd +REGISTRY=nexus.urpi.be +CERT_MANAGER_VERSION=v1.19.1 +LOCAL_PATH_VERSION=v0.0.37 +LOCK_NAME=forgejo-prd-deploy-lock +if [[ $# -lt 4 ]]; then echo "Usage: $0 [helm args...]" >&2; exit 2; fi +CHART=$1; RELEASE=$2; IMAGE_NAME=$3; IMAGE_TAG=$4; shift 4 +: "${KUBECONFIG_PRD:?Set the Forgejo secret KUBECONFIG_PRD to the PRD kubeconfig contents}" +: "${NEXUS_USERNAME:?Set the Forgejo secret NEXUS_USERNAME}" +: "${NEXUS_PASSWORD:?Set the Forgejo secret NEXUS_PASSWORD}" +KUBECONFIG="${RUNNER_TEMP:-/tmp}/kubeconfig-prd-${GITHUB_RUN_ID:-$$}" +umask 077; printf '%s\n' "$KUBECONFIG_PRD" > "$KUBECONFIG"; export KUBECONFIG +LOCK_ACQUIRED=false +cleanup() { if [[ "$LOCK_ACQUIRED" == true ]]; then kubectl -n "$NAMESPACE" delete configmap "$LOCK_NAME" --ignore-not-found --wait=false >/dev/null 2>&1 || true; fi; rm -f "$KUBECONFIG"; } +trap cleanup EXIT +secret_value() { + local name=$1 key=$2 encoded + encoded="$(kubectl -n "$NAMESPACE" get secret "$name" -o go-template="{{index .data \"${key}\"}}" 2>/dev/null || true)" + [[ -z "$encoded" || "$encoded" == "" ]] && return 0 + printf '%s' "$encoded" | base64 -d 2>/dev/null || true +} +kubectl get --raw=/readyz >/dev/null +kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - +for attempt in {1..360}; do if kubectl -n "$NAMESPACE" create configmap "$LOCK_NAME" --from-literal=owner="${GITHUB_REPOSITORY:-local}:${GITHUB_RUN_ID:-$$}" >/dev/null 2>&1; then LOCK_ACQUIRED=true; break; fi; sleep 5; done +if [[ "$LOCK_ACQUIRED" != true ]]; then echo "Timed out waiting for another go-insane-prd deploy to release its lock." >&2; exit 1; fi +kubectl -n "$NAMESPACE" create secret docker-registry nexus-registry --docker-server="$REGISTRY" --docker-username="$NEXUS_USERNAME" --docker-password="$NEXUS_PASSWORD" --dry-run=client -o yaml | kubectl apply -f - +DEFAULT_STORAGE_CLASS="$(kubectl get storageclass -o=jsonpath='{range .items[*]}{.metadata.name}{" "}{.metadata.annotations.storageclass\.kubernetes\.io/is-default-class}{"\n"}{end}' | awk '$2 == "true" { print $1; exit }')" +if [[ -z "$DEFAULT_STORAGE_CLASS" ]]; then + if ! kubectl get storageclass local-path >/dev/null 2>&1 || ! kubectl -n local-path-storage get deployment local-path-provisioner >/dev/null 2>&1; then kubectl apply -f "https://raw.githubusercontent.com/rancher/local-path-provisioner/${LOCAL_PATH_VERSION}/deploy/local-path-storage.yaml"; kubectl -n local-path-storage rollout status deployment/local-path-provisioner --timeout=5m; fi + kubectl annotate storageclass local-path storageclass.kubernetes.io/is-default-class=true --overwrite +fi +if ! kubectl get ingressclass nginx >/dev/null 2>&1; then + helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update + helm repo update ingress-nginx + helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx --create-namespace --set controller.service.type=NodePort --set controller.hostPort.enabled=true --set controller.kind=DaemonSet --set controller.admissionWebhooks.enabled=false --wait --timeout 10m +fi +if ! kubectl get crd clusterissuers.cert-manager.io >/dev/null 2>&1 || ! kubectl -n cert-manager get deployment cert-manager >/dev/null 2>&1; then + kubectl apply --validate=false -f "https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml" + kubectl -n cert-manager rollout status deployment/cert-manager --timeout=5m + kubectl -n cert-manager rollout status deployment/cert-manager-cainjector --timeout=5m + kubectl -n cert-manager rollout status deployment/cert-manager-webhook --timeout=5m +fi +if ! kubectl get clusterissuer letsencrypt-prod >/dev/null 2>&1; then + kubectl apply -f - <<'ISSUER' +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: admin@urpi.be + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: nginx +ISSUER +fi +kubectl wait --for=condition=Ready clusterissuer/letsencrypt-prod --timeout=2m +[[ -d "$CHART" ]] || { echo "Helm chart not found: $CHART" >&2; exit 1; } +if [[ -f "$CHART/Chart.lock" ]]; then helm dependency build "$CHART"; fi +HELM_ARGS=(--namespace "$NAMESPACE" --create-namespace) +if [[ "$RELEASE" != go-guess ]]; then HELM_ARGS+=(--set-string "image.repository=${REGISTRY}/docker-hosted/${IMAGE_NAME}" --set-string "image.tag=${IMAGE_TAG}"); fi +[[ ! -f "$CHART/values-prd.yaml" ]] || HELM_ARGS+=(--values "$CHART/values-prd.yaml") +case "$RELEASE" in + go-loose) + if ! kubectl -n "$NAMESPACE" get secret go-loose-runtime >/dev/null 2>&1; then + db_password="$(secret_value go-loose-postgresql postgres-password)" + if [[ -z "$db_password" ]] && kubectl -n "$NAMESPACE" get pvc -o name | grep -Eq 'go-loose.*postgresql|data-go-loose'; then echo "Existing go-loose database storage has no recoverable password secret; refusing to rotate its credentials." >&2; exit 1; fi + [[ -n "$db_password" ]] || db_password="$(openssl rand -hex 24)" + session_secret="$(secret_value go-loose-env GO_LOOSE_SESSION_SECRET)" + [[ -n "$session_secret" ]] || session_secret="$(openssl rand -hex 32)" + kubectl -n "$NAMESPACE" create secret generic go-loose-runtime --from-literal=postgresql-password="$db_password" --from-literal=session-secret="$session_secret" + fi + db_password="$(secret_value go-loose-runtime postgresql-password)" + session_secret="$(secret_value go-loose-runtime session-secret)" + database_url="postgres://goloose:${db_password}@go-loose-postgresql:5432/goloose?sslmode=disable" + HELM_ARGS+=(--set-string "postgresql.auth.password=${db_password}" --set-string "envSecrets.GO_LOOSE_DATABASE_URL=${database_url}" --set-string "envSecrets.GO_LOOSE_SESSION_SECRET=${session_secret}") + ;; + go-mad) + if ! kubectl -n "$NAMESPACE" get secret go-mad-runtime >/dev/null 2>&1; then + db_password="$(secret_value go-mad-postgresql password)"; [[ -n "$db_password" ]] || db_password="$(secret_value go-mad-postgresql postgres-password)" + if [[ -z "$db_password" ]] && kubectl -n "$NAMESPACE" get pvc -o name | grep -Eq 'go-mad.*postgresql|data-go-mad'; then echo "Existing go-mad database storage has no recoverable password secret; refusing to rotate its credentials." >&2; exit 1; fi + [[ -n "$db_password" ]] || db_password="$(openssl rand -hex 24)" + kubectl -n "$NAMESPACE" create secret generic go-mad-runtime --from-literal=postgresql-password="$db_password" --from-literal=jwt-secret="$(openssl rand -hex 32)" + fi + db_password="$(secret_value go-mad-runtime postgresql-password)"; jwt_secret="$(secret_value go-mad-runtime jwt-secret)" + HELM_ARGS+=(--set-string "postgresql.auth.password=${db_password}" --set-string "go-mad.jwtSecret=${jwt_secret}") + ;; + go-first) + if ! kubectl -n "$NAMESPACE" get secret go-first-runtime >/dev/null 2>&1; then + kubectl -n "$NAMESPACE" create secret generic go-first-runtime --from-literal=jwt-secret="$(openssl rand -hex 32)" + fi + ;; + go-places) + : "${GO_LOOSE_JWT_SECRET:?Set Forgejo secret GO_LOOSE_JWT_SECRET to the same signing key used by go-loose}" + kubectl -n "$NAMESPACE" create secret generic go-loose-jwt --from-literal=secret="$GO_LOOSE_JWT_SECRET" --dry-run=client -o yaml | kubectl apply -f - + ;; + go-bananas) + : "${GO_BANANAS_OIDC_CLIENT_SECRET:?Set Forgejo secret GO_BANANAS_OIDC_CLIENT_SECRET}" + session_secret="$(secret_value go-bananas-auth session-secret)" + if [[ -z "$session_secret" ]]; then session_secret="$(openssl rand -hex 32)"; fi + kubectl -n "$NAMESPACE" create secret generic go-bananas-auth --from-literal=session-secret="$session_secret" --from-literal=oidc-client-secret="$GO_BANANAS_OIDC_CLIENT_SECRET" --dry-run=client -o yaml | kubectl apply -f - + ;; +esac +helm upgrade --install "$RELEASE" "$CHART" "${HELM_ARGS[@]}" "$@" --wait --timeout 15m +kubectl -n "$NAMESPACE" get deployments,statefulsets,pods,services,ingresses diff --git a/.forgejo/workflows/ci-cd.yaml b/.forgejo/workflows/ci-cd.yaml index 3f513a2..bafb178 100644 --- a/.forgejo/workflows/ci-cd.yaml +++ b/.forgejo/workflows/ci-cd.yaml @@ -1,58 +1,41 @@ -name: CI/CD +name: Build, test, publish, deploy on: push: - branches: [main, develop] - tags: ['v*'] pull_request: - branches: [main, develop] + workflow_dispatch: env: - REGISTRY: nexus-registry.urpi.be + GO_VERSION: "1.27.1" + REGISTRY: nexus.urpi.be IMAGE_NAME: docker-hosted/go-loose - GO_VERSION: '1.27.1' jobs: - build-and-test: - name: Build and Test + build-test: runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: go.mod cache: true - - - name: Download dependencies - run: go mod download - - - name: Build - run: go build ./... - - - name: Run unit tests - run: go test -race ./... -count=1 - - - name: Run vet - run: go vet ./... - - - name: Check formatting + - name: Build and test Go service run: | - if [ "$(gofmt -l . | grep -v vendor | grep -v dist | wc -l)" -gt 0 ]; then - echo "Code is not formatted:" - gofmt -l . | grep -v vendor | grep -v dist - exit 1 - fi + go mod download + go vet ./... + go test -race ./... -count=1 + go build ./... + - uses: azure/setup-helm@v4 + with: + version: v3.18.6 + - name: Validate production chart + run: | + helm dependency build deploy/helm/go-loose + helm lint deploy/helm/go-loose -f deploy/helm/go-loose/values-prd.yaml integration-test: - name: Integration Tests runs-on: ubuntu-latest - needs: build-and-test + needs: build-test services: postgres: image: postgres:17-alpine @@ -60,151 +43,71 @@ jobs: POSTGRES_DB: goloose POSTGRES_USER: goloose POSTGRES_PASSWORD: goloose - ports: ['5432:5432'] + ports: [5432:5432] options: >- --health-cmd="pg_isready -U goloose -d goloose" --health-interval=5s --health-timeout=3s --health-retries=10 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: go.mod cache: true - - - name: Download dependencies - run: go mod download - - - name: Create test databases + - name: Run database-backed tests + env: + PGPASSWORD: goloose + GO_LOOSE_TEST_DATABASE_URL: postgres://goloose:goloose@localhost:5432/goloose_test?sslmode=disable + GO_LOOSE_TEST_SERVER_DATABASE_URL: postgres://goloose:goloose@localhost:5432/goloose_server_test?sslmode=disable + GO_LOOSE_BEHAVIOUR_DATABASE_URL: postgres://goloose:goloose@localhost:5432/goloose_behaviour_test?sslmode=disable run: | sleep 5 createdb -h localhost -U goloose goloose_test || true createdb -h localhost -U goloose goloose_server_test || true createdb -h localhost -U goloose goloose_behaviour_test || true - - - name: Run integration tests - env: - GO_LOOSE_TEST_DATABASE_URL: postgres://goloose:goloose@localhost:5432/goloose_test?sslmode=disable - GO_LOOSE_TEST_SERVER_DATABASE_URL: postgres://goloose:goloose@localhost:5432/goloose_server_test?sslmode=disable - GO_LOOSE_BEHAVIOUR_DATABASE_URL: postgres://goloose:goloose@localhost:5432/goloose_behaviour_test?sslmode=disable - run: | go test -race ./internal/store/ ./internal/server/ -count=1 GO_LOOSE_BEHAVIOUR=1 go test ./internal/behaviour/ -count=1 -v - docker: - name: Build and Push Docker Image + publish: + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + needs: [build-test, integration-test] runs-on: ubuntu-latest - needs: [build-and-test, integration-test] - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Nexus Registry - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.NEXUS_USERNAME }} password: ${{ secrets.NEXUS_PASSWORD }} - - - name: Extract metadata for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - - name: Build and push Docker image - uses: docker/build-push-action@v5 + - uses: docker/build-push-action@v6 with: context: . + file: Dockerfile push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - -# deploy-tst: -# name: Deploy to TST -# runs-on: ubuntu-latest -# needs: docker -# if: github.event_name == 'push' && github.ref == 'refs/heads/develop' -# permissions: -# contents: read -# id-token: write -# environment: tst -# -# steps: -# - name: Checkout repository -# uses: actions/checkout@v4 -# -# - name: Set up Helm -# uses: azure/setup-helm@v4 -# with: -# version: 'v3.14.0' -# -# - name: Set up kubectl -# uses: azure/k8s-set-context@v4 -# with: -# kubeconfig: ${{ secrets.KUBECONFIG_TST }} -# -# - name: Deploy to TST with Helm -# run: | -# helm upgrade --install go-loose ./deploy/helm/go-loose \ -# --namespace go-loose \ -# --create-namespace \ -# --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ -# --set image.tag=${{ github.sha }} \ -# --set image.pullSecrets[0].name=nexus-registry \ -# --wait --timeout 5m + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest deploy-prd: - name: Deploy to PRD + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + needs: publish runs-on: ubuntu-latest - needs: docker - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') - permissions: - contents: read - id-token: write - environment: prd - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Helm - uses: azure/setup-helm@v4 + - uses: actions/checkout@v4 + - uses: azure/setup-helm@v4 with: - version: 'v3.14.0' - - - name: Set up kubectl - uses: azure/k8s-set-context@v4 + version: v3.18.6 + - uses: azure/setup-kubectl@v4 with: - kubeconfig: ${{ secrets.KUBECONFIG_PRD }} - - - name: Deploy to PRD with Helm + version: v1.32.2 + - name: Bootstrap resources and deploy go-loose + env: + KUBECONFIG_PRD: ${{ secrets.KUBECONFIG_PRD }} + NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} run: | - helm upgrade --install go-loose ./deploy/helm/go-loose \ - --namespace go-loose \ - --create-namespace \ - --values ./deploy/helm/go-loose/values-prd.yaml \ - --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \ - --set image.tag=${{ github.ref_name }} \ - --wait --timeout 5m \ No newline at end of file + set -Eeuo pipefail + bash .forgejo/scripts/deploy-prd.sh deploy/helm/go-loose go-loose go-loose "${{ github.sha }}" \ + --set-string ingress.className=nginx diff --git a/deploy/helm/go-loose/Chart.lock b/deploy/helm/go-loose/Chart.lock new file mode 100644 index 0000000..10b4a65 --- /dev/null +++ b/deploy/helm/go-loose/Chart.lock @@ -0,0 +1,6 @@ +dependencies: + - name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 16.7.27 +digest: sha256:e8b999a3aaad6328a13d0c666207650897b69c61367c947d363b2acbe0ea1b42 +generated: "2026-10-04T19:38:06.343578353+02:00" diff --git a/deploy/helm/go-loose/Chart.yaml b/deploy/helm/go-loose/Chart.yaml index 3f6e183..8423780 100644 --- a/deploy/helm/go-loose/Chart.yaml +++ b/deploy/helm/go-loose/Chart.yaml @@ -5,6 +5,11 @@ type: application version: 1.0.0 appVersion: "1.0.0" kubeVersion: ">=1.28.0-0" +dependencies: + - name: postgresql + version: "16.x.x" + repository: "https://charts.bitnami.com/bitnami" + condition: postgresql.enabled keywords: - authentication - authorization @@ -20,4 +25,4 @@ maintainers: url: https://github.com/urpi annotations: category: Security - licenses: MIT \ No newline at end of file + licenses: MIT