ci: add Forgejo build and deployment workflows

This commit is contained in:
Daniel Noulet 2026-10-08 10:36:33 +02:00
parent abb207d608
commit d3ee64282d
4 changed files with 181 additions and 155 deletions

View file

@ -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 <chart> <release> <image-name> <image-tag> [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" == "<no value>" ]] && 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

View file

@ -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
set -Eeuo pipefail
bash .forgejo/scripts/deploy-prd.sh deploy/helm/go-loose go-loose go-loose "${{ github.sha }}" \
--set-string ingress.className=nginx