diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
index 6126a96..af3f8b6 100644
--- a/.forgejo/workflows/ci.yml
+++ b/.forgejo/workflows/ci.yml
@@ -6,52 +6,36 @@ on:
tags: ["v*"]
pull_request:
-env:
- DOTNET_CLI_TELEMETRY_OPTOUT: "1"
- DOTNET_NOLOGO: "1"
-
jobs:
build:
name: Build og unit tests
- # "docker" er standard-label for en Forgejo runner. Ret til jeres runners label, hvis det er et andet.
- runs-on: docker
- container:
- image: mcr.microsoft.com/dotnet/sdk:10.0
+ # Self-hosted appserver-runner (host-mode). Selve bygget kører i SDK-imaget via deploy/ci/Dockerfile.
+ runs-on: [self-hosted, appserver]
+ timeout-minutes: 30
steps:
- name: Checkout
- run: |
- git init -q .
- git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
- git -c http.extraHeader="Authorization: token ${{ secrets.GITHUB_TOKEN }}" fetch -q --depth 1 origin "${{ github.sha }}"
- git checkout -q FETCH_HEAD
- - name: Restore
- run: dotnet restore
- - name: Build
- run: dotnet build --no-restore --configuration Release
- - name: Unit tests
- run: dotnet test --no-build --configuration Release --filter "Category!=Integration"
+ uses: actions/checkout@v4
+ - name: Restore, build og unit tests
+ run: docker build --progress=plain -f deploy/ci/Dockerfile --target test -t "sqlinsight-ci:${{ github.sha }}" .
+ - name: Ryd op
+ if: always()
+ run: docker image rm "sqlinsight-ci:${{ github.sha }}" 2>/dev/null || true
msi:
name: MSI-installer
needs: build
- runs-on: docker
- container:
- image: mcr.microsoft.com/dotnet/sdk:10.0
+ runs-on: [self-hosted, appserver]
+ timeout-minutes: 30
steps:
- name: Checkout
- run: |
- git init -q .
- git remote add origin "${{ github.server_url }}/${{ github.repository }}.git"
- git -c http.extraHeader="Authorization: token ${{ secrets.GITHUB_TOKEN }}" fetch -q --depth 1 origin "${{ github.sha }}"
- git checkout -q FETCH_HEAD
- - name: Værktøjer (wixl, msitools; nodejs til upload-artifact)
- run: apt-get update -q && apt-get install -y -q --no-install-recommends wixl msitools python3 nodejs
+ uses: actions/checkout@v4
- name: Byg og kontrollér MSI
# Et tag som v1.2.0 giver version 1.2.0; ellers versionen fra Directory.Build.props.
run: |
version="${GITHUB_REF_NAME#v}"
case "$GITHUB_REF" in refs/tags/v*) ;; *) version=$(sed -n 's|.*\(.*\).*|\1|p' Directory.Build.props) ;; esac
- installer/build-msi.sh "$version"
+ docker buildx build --progress=plain -f deploy/ci/Dockerfile --target msi --build-arg VERSION="$version" --output type=local,dest=dist .
+ ls -l dist/*.msi
- name: Gem MSI
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
diff --git a/.forgejo/workflows/deploy-hub.yml b/.forgejo/workflows/deploy-hub.yml
new file mode 100644
index 0000000..ccd1aea
--- /dev/null
+++ b/.forgejo/workflows/deploy-hub.yml
@@ -0,0 +1,137 @@
+# =============================================================================
+# 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 (offentlig licensnøgle fra keygen)
+# =============================================================================
+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 }}
+ 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
+
+ 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"
+ else
+ echo "WARNING: TRUSTED_KEY_ID/TRUSTED_KEY ikke sat – hubben kan ikke verificere licenser" >&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
diff --git a/deploy/ci/Dockerfile b/deploy/ci/Dockerfile
new file mode 100644
index 0000000..73736bf
--- /dev/null
+++ b/deploy/ci/Dockerfile
@@ -0,0 +1,24 @@
+# CI-byg til den self-hostede Forgejo runner (host-mode: jobs kører i runnerens
+# egen Alpine-container, så 'container:' og apt-get virker ikke dér).
+# Build og tests kører i stedet i SDK-imaget via docker build:
+# docker build -f deploy/ci/Dockerfile --target test .
+# docker buildx build -f deploy/ci/Dockerfile --target msi --build-arg VERSION=1.2.0 --output type=local,dest=dist .
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS test
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
+WORKDIR /src
+COPY . .
+RUN dotnet restore \
+ && dotnet build --no-restore --configuration Release \
+ && dotnet test --no-build --configuration Release --filter "Category!=Integration"
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS msi-build
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
+RUN apt-get update -q && apt-get install -y -q --no-install-recommends wixl msitools python3 \
+ && rm -rf /var/lib/apt/lists/*
+WORKDIR /src
+COPY . .
+ARG VERSION=1.0.0
+RUN installer/build-msi.sh "$VERSION"
+
+FROM scratch AS msi
+COPY --from=msi-build /src/dist/ /
diff --git a/deploy/hub/docker-compose.prod.yml b/deploy/hub/docker-compose.prod.yml
new file mode 100644
index 0000000..c53715e
--- /dev/null
+++ b/deploy/hub/docker-compose.prod.yml
@@ -0,0 +1,30 @@
+# SQLInsight Hub i SEOSofts drift (appserver). Kopieres til INSTALL_DIR af
+# .forgejo/workflows/deploy-hub.yml sammen med en .env, der genereres fra
+# Forgejo repo-secrets. PostgreSQL kører på en separat DB-server, ikke i compose.
+#
+# Image `sqlinsight-hub:latest` bygges lokalt af runneren; intet registry-push.
+#
+# Manuel drift på appserveren:
+# cd /opt/sqlinsight-hub && docker compose ps | logs -f | restart
+# docker exec -it sqlinsight-hub dotnet SQLInsight.Hub.dll add-staff ""
+
+services:
+ hub:
+ image: sqlinsight-hub:latest
+ container_name: sqlinsight-hub
+ restart: unless-stopped
+ env_file:
+ - .env
+ environment:
+ SQLInsight__Hub__UpdateFeedPath: /data/feed
+ volumes:
+ # keys/ (login-cookies) og feed/. Tag backup; mistes keys, logges alle ud.
+ - ./data:/data
+ ports:
+ # Bind til DMZ-IP (HOST_BIND i .env). Reverse proxyen terminerer TLS.
+ - "${HOST_BIND}:${HOST_PORT}:8080"
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "3"