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"