sqlinsight-pro/.forgejo/workflows/deploy-hub.yml
Søren Eggert Lundsteen Olsen 83c247d536 Hub: support case numbers, customer case API, mail, support report
- Case numbers SUP-000001, SUP-000002 … per customer, unique per customer
  (new index; a migration step renames any old duplicates first). The
  portal suggests the next number on the finding page and under the new
  "Ny sag" on the customer page; an empty field gets the next number, a
  typed one that is taken is refused. Concurrent creates retry.
- SupportCase gains Source (Staff/Customer), description, contact name,
  e-mail, phone, the customer's Windows user and ClientRequestId.
- POST/GET /api/v1/cases for the customer's agent (license with priority
  support): creates a case idempotently on ClientRequestId, links the
  finding it came from, lists the customer's cases incl. staff-created.
  Customer/license creation is shared with the findings endpoint.
- Optional mail to support for customer cases (MailKit; prefers AUTH LOGIN
  as Microsoft 365 needs). A failed mail is logged, never fails the call.
  deploy-hub.yml writes optional SUPPORT_SMTP_* secrets to .env.
- Supportrapport: created, closed, open now, average/median resolution
  time, oldest open case and customer-created share per customer for a
  period (default 90 days), plus the cases; CSV for Danish Excel.
- Cases page: newest first, customer cases marked, contact/description.
- Customer page: findings can be filtered to one instance (?instans=id,
  ignored for another customer's instance); active findings per instance
  by severity.
- dotnet-ef as a local tool (dotnet-tools.json) for the migration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:33:55 +02:00

168 lines
7.4 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# =============================================================================
# Deploy SQLInsight Hub til appserveren ved push til main.
#
# Kører på den self-hostede appserver-runner (host-mode, docker.sock).
# Runner-containeren har ikke INSTALL_DIR mountet, så .env, compose-filen og
# 'docker compose up' håndteres via kortlivede helper-containere med
# INSTALL_DIR bind-mountet fra værten.
#
# Alle miljøværdier (også IP'er) kommer fra Forgejo repo-secrets.
#
# Påkrævede secrets: HOST_BIND HOST_PORT DB_SERVER DB_PORT DB_NAME DB_USER DB_PASSWORD KNOWN_PROXY
# Valgfri: TRUSTED_KEY_ID + TRUSTED_KEY (ekstra offentlig licensnøgle; k20261001 er indbygget)
# Valgfri: mail til support ved nye kundesager: SUPPORT_SMTP_HOST SUPPORT_SMTP_PORT (587) SUPPORT_SMTP_USER
# SUPPORT_SMTP_PASSWORD SUPPORT_SMTP_FROM SUPPORT_SMTP_TO (kommasepareret) SUPPORT_PORTAL_URL (link i mailen)
# =============================================================================
name: deploy hub
on:
push:
branches: [main]
paths:
- "src/**"
- "deploy/hub/**"
- "Directory.*.props"
- "global.json"
- ".forgejo/workflows/deploy-hub.yml"
workflow_dispatch:
concurrency:
group: deploy-sqlinsight-hub
cancel-in-progress: false
env:
INSTALL_DIR: /opt/sqlinsight-hub
IMAGE: sqlinsight-hub
CONTAINER: sqlinsight-hub
jobs:
deploy:
runs-on: [self-hosted, appserver]
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build image
run: |
set -e
docker build -f deploy/hub/Dockerfile -t "${IMAGE}:latest" -t "${IMAGE}:${{ github.sha }}" .
- name: Write .env + docker-compose.yml to INSTALL_DIR
env:
HOST_BIND: ${{ secrets.HOST_BIND }}
HOST_PORT: ${{ secrets.HOST_PORT }}
DB_SERVER: ${{ secrets.DB_SERVER }}
DB_PORT: ${{ secrets.DB_PORT }}
DB_NAME: ${{ secrets.DB_NAME }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
KNOWN_PROXY: ${{ secrets.KNOWN_PROXY }}
TRUSTED_KEY_ID: ${{ secrets.TRUSTED_KEY_ID }}
TRUSTED_KEY: ${{ secrets.TRUSTED_KEY }}
SUPPORT_SMTP_HOST: ${{ secrets.SUPPORT_SMTP_HOST }}
SUPPORT_SMTP_PORT: ${{ secrets.SUPPORT_SMTP_PORT }}
SUPPORT_SMTP_USER: ${{ secrets.SUPPORT_SMTP_USER }}
SUPPORT_SMTP_PASSWORD: ${{ secrets.SUPPORT_SMTP_PASSWORD }}
SUPPORT_SMTP_FROM: ${{ secrets.SUPPORT_SMTP_FROM }}
SUPPORT_SMTP_TO: ${{ secrets.SUPPORT_SMTP_TO }}
SUPPORT_PORTAL_URL: ${{ secrets.SUPPORT_PORTAL_URL }}
run: |
set -eu
required="HOST_BIND HOST_PORT DB_SERVER DB_PORT DB_NAME DB_USER DB_PASSWORD KNOWN_PROXY"
missing=0
for v in $required; do
if [ -z "$(printenv "$v" || true)" ]; then
echo "ERROR: required secret $v is not set in repo settings"
missing=1
fi
done
[ "$missing" -eq 0 ] || exit 1
# Værdier skrives single-quoted, så compose ikke udvider '$' i passwords
for v in $required TRUSTED_KEY_ID TRUSTED_KEY; do
case "$(printenv "$v" || true)" in
*"'"*|*";"*) echo "ERROR: secret $v indeholder ' eller ; – ikke understøttet"; exit 1 ;;
esac
done
# Mail-værdierne må gerne indeholde ; (fx i et kodeord), men ikke '
for v in SUPPORT_SMTP_HOST SUPPORT_SMTP_PORT SUPPORT_SMTP_USER SUPPORT_SMTP_PASSWORD SUPPORT_SMTP_FROM SUPPORT_SMTP_TO SUPPORT_PORTAL_URL; do
case "$(printenv "$v" || true)" in
*"'"*) echo "ERROR: secret $v indeholder ' – ikke understøttet"; exit 1 ;;
esac
done
tmp="$(mktemp -d)"
{
printf "HOST_BIND='%s'\n" "$HOST_BIND"
printf "HOST_PORT='%s'\n" "$HOST_PORT"
printf "ConnectionStrings__Hub='Host=%s;Port=%s;Database=%s;Username=%s;Password=%s;GSS Encryption Mode=Disable'\n" \
"$DB_SERVER" "$DB_PORT" "$DB_NAME" "$DB_USER" "$DB_PASSWORD"
printf "SQLInsight__Hub__KnownProxies__0='%s'\n" "$KNOWN_PROXY"
if [ -n "${TRUSTED_KEY_ID:-}" ] && [ -n "${TRUSTED_KEY:-}" ]; then
printf "SQLInsight__Hub__TrustedKeys__%s='%s'\n" "$TRUSTED_KEY_ID" "$TRUSTED_KEY"
fi
# Mail til support, når en kunde opretter en sag (kun når host, afsender og modtager er sat)
if [ -n "${SUPPORT_SMTP_HOST:-}" ] && [ -n "${SUPPORT_SMTP_FROM:-}" ] && [ -n "${SUPPORT_SMTP_TO:-}" ]; then
n="SQLInsight__Hub__Support__Notify__"
printf "%sEnabled='true'\n" "$n"
printf "%sSmtpHost='%s'\n" "$n" "$SUPPORT_SMTP_HOST"
printf "%sSmtpPort='%s'\n" "$n" "${SUPPORT_SMTP_PORT:-587}"
printf "%sFrom='%s'\n" "$n" "$SUPPORT_SMTP_FROM"
[ -z "${SUPPORT_SMTP_USER:-}" ] || printf "%sUserName='%s'\n" "$n" "$SUPPORT_SMTP_USER"
[ -z "${SUPPORT_SMTP_PASSWORD:-}" ] || printf "%sPassword='%s'\n" "$n" "$SUPPORT_SMTP_PASSWORD"
[ -z "${SUPPORT_PORTAL_URL:-}" ] || printf "%sPortalUrl='%s'\n" "$n" "$SUPPORT_PORTAL_URL"
i=0
for to in $(printf "%s" "$SUPPORT_SMTP_TO" | tr "," " "); do
printf "%sTo__%s='%s'\n" "$n" "$i" "$to"
i=$((i + 1))
done
else
echo "NOTE: SUPPORT_SMTP_* ikke sat – nye kundesager vises kun i portalen (ingen mail)" >&2
fi
} > "$tmp/.env"
cp deploy/hub/docker-compose.prod.yml "$tmp/docker-compose.yml"
tar -C "$tmp" -cf - .env docker-compose.yml | docker run --rm -i \
-v "${INSTALL_DIR}:/out" alpine:3 \
sh -c 'tar -C /out -xf - && chown root:root /out/.env /out/docker-compose.yml && chmod 600 /out/.env && chmod 644 /out/docker-compose.yml'
rm -rf "$tmp"
- name: Deploy (compose up)
run: |
set -eu
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "${INSTALL_DIR}:${INSTALL_DIR}" \
-w "${INSTALL_DIR}" \
docker:cli \
docker compose --project-name sqlinsight-hub up -d --force-recreate --remove-orphans
- name: Wait for /health
env:
HOST_BIND: ${{ secrets.HOST_BIND }}
HOST_PORT: ${{ secrets.HOST_PORT }}
run: |
set -eu
# aspnet-imaget har hverken curl eller wget – poll udefra
for i in $(seq 1 30); do
if docker run --rm alpine:3 wget -qO- -T 3 "http://${HOST_BIND}:${HOST_PORT}/health" 2>/dev/null | grep -q '"ok"'; then
echo "${CONTAINER} is healthy"
exit 0
fi
echo "attempt $i: not ready"
sleep 5
done
echo "${CONTAINER} did not answer /health within 150s — last 100 log lines:"
docker logs --tail 100 "${CONTAINER}" || true
exit 1
- name: Prune old images
if: always()
run: |
docker image prune -f || true
# Behold latest + de 3 nyeste sha-tags
docker images "${IMAGE}" --format '{{.Tag}} {{.CreatedAt}}' \
| grep -v '^latest ' | sort -k2 -r | awk 'NR>3 {print $1}' \
| xargs -r -I{} docker image rm "${IMAGE}:{}" || true