name: CI on: push: branches: [main] schedule: - cron: '17 3 * * 1' concurrency: group: buzz-sheet-${{ gitea.ref }} cancel-in-progress: false env: BASE_URL: https://guide.sudloh.com NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID: ca-pub-9687404323559597 REGISTRY_IMAGE: registry.neko-piranha.ts.net/astral/buzz-sheet DEPLOY_NAMESPACE: buzz-sheet jobs: verify: name: Verify and audit runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Check out repository and history uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 persist-credentials: false - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: bun-version: 1.3.14 - name: Set up kubectl uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4 with: version: v1.34.1 - name: Verify application and manifests run: | bun install --frozen-lockfile bun run security:audit bun run test bun run typecheck bun run lint kubectl kustomize k8s/ >/dev/null - name: Scan Git history for secrets run: | docker run --rm --cap-drop=ALL --security-opt=no-new-privileges \ --volume "$PWD:/repo:ro" --workdir /repo \ ghcr.io/gitleaks/gitleaks@sha256:691af3c7c5a48b16f187ce3446d5f194838f91238f27270ed36eef6359a574d9 \ git --redact=100 --no-banner --log-opts=--all . build-and-deploy: name: Build, scan and deploy immutable images needs: verify if: >- gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' && vars.DEPLOY_ENABLED == 'true' runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Check out repository uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: persist-credentials: false - name: Build and scan application and migration images env: REVISION: ${{ gitea.sha }} run: | set -Eeuo pipefail docker build --target app \ --build-arg BASE_URL="$BASE_URL" \ --build-arg NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID="$NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID" \ --build-arg NEXT_DEPLOYMENT_ID="$REVISION" \ --build-arg VCS_REF="$REVISION" \ --tag "$REGISTRY_IMAGE:$REVISION" . docker build --target migration --build-arg VCS_REF="$REVISION" \ --tag "$REGISTRY_IMAGE:migrate-$REVISION" . docker save --output "$RUNNER_TEMP/buzz-app-image.tar" "$REGISTRY_IMAGE:$REVISION" docker save --output "$RUNNER_TEMP/buzz-migration-image.tar" "$REGISTRY_IMAGE:migrate-$REVISION" chmod 644 "$RUNNER_TEMP/buzz-app-image.tar" "$RUNNER_TEMP/buzz-migration-image.tar" # One scanner container shares its private cache across both images. docker run --rm --user 0:0 --cap-drop=ALL --security-opt=no-new-privileges \ --volume "$RUNNER_TEMP/buzz-app-image.tar:/scan/buzz-app-image.tar:ro" \ --volume "$RUNNER_TEMP/buzz-migration-image.tar:/scan/buzz-migration-image.tar:ro" \ --entrypoint /bin/sh \ aquasec/trivy@sha256:bcc376de8d77cfe086a917230e818dc9f8528e3c852f7b1aff648949b6258d1c \ -ec 'trivy image --input /scan/buzz-app-image.tar --cache-dir /tmp/trivy-cache --scanners vuln --severity HIGH,CRITICAL --exit-code 1 && trivy image --input /scan/buzz-migration-image.tar --cache-dir /tmp/trivy-cache --scanners vuln --severity HIGH,CRITICAL --exit-code 1' rm "$RUNNER_TEMP/buzz-app-image.tar" "$RUNNER_TEMP/buzz-migration-image.tar" docker push "$REGISTRY_IMAGE:$REVISION" | tee "$RUNNER_TEMP/buzz-app-push.log" docker push "$REGISTRY_IMAGE:migrate-$REVISION" | tee "$RUNNER_TEMP/buzz-migration-push.log" - name: Set up kubectl uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4 with: version: v1.34.1 - name: Migrate, then roll out verified digests env: REVISION: ${{ gitea.sha }} KUBE_CONFIG_B64: ${{ secrets.KUBE_CONFIG_B64 }} shell: bash run: | set -Eeuo pipefail kube_dir="$RUNNER_TEMP/buzz-sheet-kube" mkdir -p "$kube_dir" chmod 700 "$kube_dir" export KUBECONFIG="$kube_dir/config" trap 'rm -f "$KUBECONFIG"' EXIT printf '%s' "$KUBE_CONFIG_B64" | base64 --decode > "$KUBECONFIG" chmod 600 "$KUBECONFIG" test -s "$KUBECONFIG" insecure="$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.insecure-skip-tls-verify}')" if [ "$insecure" = true ]; then echo 'The deployment kubeconfig must validate the Kubernetes certificate.' >&2 exit 1 fi # Preserve the CA and certificate-valid server supplied in the kubeconfig. kubectl --namespace "$DEPLOY_NAMESPACE" get deployment buzz-sheet >/dev/null app_digest="$(sed -nE 's/.*digest: (sha256:[a-f0-9]{64}).*/\1/p' "$RUNNER_TEMP/buzz-app-push.log" | tail -n 1)" migration_digest="$(sed -nE 's/.*digest: (sha256:[a-f0-9]{64}).*/\1/p' "$RUNNER_TEMP/buzz-migration-push.log" | tail -n 1)" app_image="$REGISTRY_IMAGE@$app_digest" migration_image="$REGISTRY_IMAGE@$migration_digest" [[ "$app_image" =~ @sha256:[a-f0-9]{64}$ ]] [[ "$migration_image" =~ @sha256:[a-f0-9]{64}$ ]] migration_manifest="$RUNNER_TEMP/buzz-sheet-migration.yaml" kubectl kustomize k8s/migration > "$migration_manifest" sed -i "s#image: $REGISTRY_IMAGE:.*#image: $migration_image#" "$migration_manifest" grep -Fq "image: $migration_image" "$migration_manifest" kubectl --namespace "$DEPLOY_NAMESPACE" delete job buzz-sheet-migrate --ignore-not-found migration_resource="$(kubectl --namespace "$DEPLOY_NAMESPACE" create -f "$migration_manifest" -o name)" if ! kubectl --namespace "$DEPLOY_NAMESPACE" wait --for=condition=complete --timeout=10m "$migration_resource"; then kubectl --namespace "$DEPLOY_NAMESPACE" logs "$migration_resource" --all-containers=true || true exit 1 fi kubectl --namespace "$DEPLOY_NAMESPACE" patch configmap buzz-sheet-config --type=merge \ --patch "{\"data\":{\"NEXT_DEPLOYMENT_ID\":\"$REVISION\",\"BASE_URL\":\"$BASE_URL\"}}" kubectl --namespace "$DEPLOY_NAMESPACE" set image deployment/buzz-sheet app="$app_image" kubectl --namespace "$DEPLOY_NAMESPACE" set image deployment/buzz-sheet-worker worker="$app_image" kubectl --namespace "$DEPLOY_NAMESPACE" set image deployment/buzz-sheet-discord-worker discord-worker="$app_image" for deployment in buzz-sheet buzz-sheet-worker buzz-sheet-discord-worker; do kubectl --namespace "$DEPLOY_NAMESPACE" rollout status "deployment/$deployment" --timeout=10m done