Deploy hub to SEOSoft appserver; run CI on the self-hosted runner
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Successful in 1m10s
deploy hub / deploy (push) Successful in 19s
CI / MSI-installer (push) Successful in 50s

deploy-hub.yml builds deploy/hub/Dockerfile on the appserver runner, writes
.env from repo secrets and starts docker-compose.prod.yml (external
PostgreSQL, /data bind-mounted), then polls /health.

ci.yml used runs-on: docker, which no runner has, so jobs never started.
Build/tests and the MSI now run inside the SDK image via deploy/ci/Dockerfile,
because the host-mode runner ignores container: and has no apt-get.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Søren Eggert Lundsteen Olsen 2026-09-29 15:37:56 +02:00
parent 188a27381a
commit 4fdeff87fa
4 changed files with 205 additions and 30 deletions

View file

@ -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|.*<Version>\(.*\)</Version>.*|\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:

View file

@ -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

24
deploy/ci/Dockerfile Normal file
View file

@ -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/ /

View file

@ -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 <brugernavn> "<navn>"
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"