ci: add Forgejo build and deployment workflows
This commit is contained in:
parent
951dd8d26b
commit
d0ea6e9eec
3 changed files with 306 additions and 18 deletions
175
.forgejo/scripts/deploy-prd.sh
Normal file
175
.forgejo/scripts/deploy-prd.sh
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#!/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
|
||||
|
||||
# Create the workload namespace and its Nexus pull credential idempotently.
|
||||
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 -
|
||||
|
||||
# Stateful charts use the cluster default StorageClass. Install the Rancher local
|
||||
# path provisioner only when the cluster has no default class at all.
|
||||
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
|
||||
|
||||
# Applications' production charts request nginx ingress and Let's Encrypt TLS.
|
||||
# Install only missing cluster prerequisites; leave existing installations intact.
|
||||
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
|
||||
|
||||
if [[ ! -d "$CHART" ]]; then
|
||||
echo "Helm chart not found: $CHART" >&2
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
if [[ -f "$CHART/values-prd.yaml" ]]; then
|
||||
HELM_ARGS+=(--values "$CHART/values-prd.yaml")
|
||||
fi
|
||||
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
|
||||
101
.forgejo/workflows/ci-cd.yaml
Normal file
101
.forgejo/workflows/ci-cd.yaml
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
name: Build, test, publish, deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.27.1"
|
||||
NODE_VERSION: "24"
|
||||
REGISTRY: nexus.urpi.be
|
||||
IMAGE_NAME: docker-hosted/go-bananas
|
||||
|
||||
jobs:
|
||||
build-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
web/package-lock.json
|
||||
sdk/package-lock.json
|
||||
- name: Build and test Go server
|
||||
run: |
|
||||
go vet ./...
|
||||
make test-go
|
||||
make build-go
|
||||
- name: Build and test admin UI
|
||||
run: |
|
||||
npm ci --no-audit --no-fund
|
||||
npm run lint
|
||||
npm test
|
||||
npm run build
|
||||
working-directory: web
|
||||
- name: Build and test SDK
|
||||
run: |
|
||||
npm ci --no-audit --no-fund
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build
|
||||
working-directory: sdk
|
||||
- uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.18.6
|
||||
- name: Validate production chart
|
||||
run: helm lint --strict deploy/helm/go-bananas -f deploy/helm/go-bananas/values-prd.yaml
|
||||
|
||||
publish:
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
||||
needs: build-test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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 }}
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
deploy-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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.18.6
|
||||
- uses: azure/setup-kubectl@v4
|
||||
with:
|
||||
version: v1.32.2
|
||||
- name: Bootstrap resources and deploy go-bananas
|
||||
env:
|
||||
KUBECONFIG_PRD: ${{ secrets.KUBECONFIG_PRD }}
|
||||
NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }}
|
||||
NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }}
|
||||
GO_BANANAS_OIDC_CLIENT_SECRET: ${{ secrets.GO_BANANAS_OIDC_CLIENT_SECRET }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
: "${GO_BANANAS_OIDC_CLIENT_SECRET:?Add Forgejo secret GO_BANANAS_OIDC_CLIENT_SECRET}"
|
||||
bash .forgejo/scripts/deploy-prd.sh deploy/helm/go-bananas go-bananas go-bananas "${{ github.sha }}" \
|
||||
--set-string identity.oidc.issuer=https://auth.urpi.be/realms/urpi \
|
||||
--set-string identity.oidc.clientId=go-bananas \
|
||||
--set-string identity.oidc.existingSecret=go-bananas-auth \
|
||||
--set identity.oidc.generateSecret=false
|
||||
|
|
@ -1,20 +1,31 @@
|
|||
# PRD values for go-bananas
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
# Production values for go-bananas. Secret values remain in the Kubernetes Secret
|
||||
# created by the Forgejo deployment job.
|
||||
global:
|
||||
imagePullSecrets:
|
||||
- name: nexus-registry
|
||||
|
||||
deployment:
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: nexus.urpi.be/docker-hosted/go-bananas
|
||||
tag: "0.0.1"
|
||||
registry: nexus.urpi.be
|
||||
repository: docker-hosted/go-bananas
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
config:
|
||||
environment: production
|
||||
publicBaseUrl: https://bananas.urpi.be
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
type: persistentVolumeClaim
|
||||
size: 2Gi
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
sessionSecure: true
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
|
|
@ -32,16 +43,17 @@ ingress:
|
|||
hosts:
|
||||
- bananas.urpi.be
|
||||
|
||||
certManager:
|
||||
enabled: true
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
identity:
|
||||
mode: oidc
|
||||
oidc:
|
||||
issuer: https://auth.urpi.be/realms/urpi
|
||||
clientId: go-bananas
|
||||
existingSecret: go-bananas-auth
|
||||
generateSecret: false
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue