From 91ae2fd5603b7a2cc5939b46b2be286c4081d4da Mon Sep 17 00:00:00 2001 From: TheCr3w Lead Architect Date: Sat, 31 Oct 2026 16:22:10 +0100 Subject: [PATCH] feat: add production docker-compose, deploy.sh, caddy update script, and server install guide --- .dockerignore | 14 +++ .env.example | 30 +++++ .gitignore | 1 + README.md | 1 + SERVER_INSTALL_GUIDE.md | 106 +++++++++++++++++ .../TheCr3w.Shared/Crypto/Ed25519Service.cs | 1 + .../TheCr3w.Shared/Crypto/ManifestSigner.cs | 1 + .../TheCr3w.Shared/Data/TheCr3wDbContext.cs | 1 + .../External/ExternalDeploymentService.cs | 1 + .../Models/DeploymentManifest.cs | 1 + backend/TheCr3w.Shared/Models/Enums.cs | 1 + .../TheCr3w.Shared/Models/ReleaseMetadata.cs | 1 + ci/forgejo/android-build-release.yml | 1 + ci/forgejo/docker-build-deploy.yml | 1 + ci/forgejo/windows-build-release.yml | 1 + contracts/openapi/deployment-control-api.yaml | 1 + contracts/openapi/native-update-api.yaml | 1 + .../schemas/deployment-manifest.schema.json | 1 + contracts/schemas/update-metadata.schema.json | 1 + deploy.sh | 110 ++++++++++++++++++ .../Services/DeploymentExecutor.cs | 1 + docker-compose.yml | 90 ++++++++++++++ docs/ANDROID-SDK.md | 1 + docs/ARCHITECTURE.md | 1 + docs/BACKUP-RESTORE.md | 1 + docs/CHANGELOG.md | 1 + docs/CLOUDFLARE-R2.md | 1 + docs/DEPLOYMENT.md | 1 + docs/KEYCLOAK.md | 1 + docs/SECURITY.md | 1 + docs/TROUBLESHOOTING.md | 1 + docs/WINDOWS-SDK.md | 1 + frontend/admin-panel/index.html | 1 + frontend/admin-panel/package.json | 1 + frontend/admin-panel/src/App.tsx | 1 + frontend/admin-panel/src/index.css | 1 + frontend/admin-panel/src/main.tsx | 1 + frontend/admin-panel/src/types.ts | 1 + frontend/admin-panel/src/vite-env.d.ts | 1 + frontend/admin-panel/tsconfig.json | 1 + frontend/admin-panel/vite.config.ts | 1 + frontend/download-center/index.html | 1 + frontend/download-center/src/App.tsx | 1 + frontend/download-center/src/index.css | 1 + frontend/download-center/src/main.tsx | 1 + frontend/download-center/src/services/api.ts | 1 + frontend/download-center/src/types.ts | 1 + frontend/download-center/src/vite-env.d.ts | 1 + frontend/download-center/vite.config.ts | 1 + infrastructure/docker/Dockerfile.admin-panel | 1 + infrastructure/docker/Dockerfile.backend | 1 + .../docker/Dockerfile.download-center | 1 + .../docker/docker-compose.forgejo.yml | 1 + infrastructure/docker/docker-compose.prod.yml | 90 ++++++++++++++ .../postgres/init/01-init-schema.sql | 1 + scripts/update_caddy.sh | 91 +++++++++++++++ .../com/thecr3w/update/TheCr3wUpdater.kt | 1 + .../TheCr3wUpdater.cs | 1 + .../TheCr3w.Tests/ExternalDeploymentTests.cs | 1 + 59 files changed, 583 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 SERVER_INSTALL_GUIDE.md create mode 100644 deploy.sh create mode 100644 docker-compose.yml create mode 100644 infrastructure/docker/docker-compose.prod.yml create mode 100644 scripts/update_caddy.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..00d7ccc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +**/.git +**/.github +**/.forgejo +**/bin +**/obj +**/node_modules +**/dist +**/build +**/.vs +*.user +*.suo +thecr3w-dev-deploy.zip +*.zip +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..94ab5df --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# ============================================================================== +# TheCr3w-dev — Production Environment Configuration (.env) +# ============================================================================== +# Plik skopiuj jako .env: cp .env.example .env +# Skrypt deploy.sh automatycznie wygeneruje silne haslo bazy i klucze Ed25519, +# jezeli pozostawisz je puste. +# ============================================================================== + +# --- 1. BAZA DANYCH POSTGRESQL (DEDYKOWANY KONTENER: thecr3w-postgres) --- +POSTGRES_DB=thecr3w_db +POSTGRES_USER=thecr3w_user +POSTGRES_PASSWORD= + +# --- 2. IAM KEYCLOAK (login.thecr3wnet.com) --- +# Istniejacy realm: master +KEYCLOAK_AUTHORITY=https://login.thecr3wnet.com/realms/master +KEYCLOAK_AUDIENCE=thecr3w-backend + +# --- 3. CLOUDFLARE R2 (S3-compatible Object Storage) --- +# Uzyskane z panelu Cloudflare R2: +R2_ENDPOINT=https://bf705752e38169fedb83efd5b747390e.r2.cloudflarestorage.com +R2_BUCKET=thecr3w-app +R2_REGION=auto +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= + +# --- 4. PODPISYWANIE METADANYCH ED25519 (RFC 8032) --- +# Jezeli puste, deploy.sh wygeneruje bezpieczna pare kluczy automatycznie +ED25519_PRIVATE_KEY_HEX= +ED25519_PUBLIC_KEY_HEX= diff --git a/.gitignore b/.gitignore index c72b790..16dfe1a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ thecr3w_discovery_report_*.txt .DS_Store Thumbs.db desktop.ini + diff --git a/README.md b/README.md index d09b094..70fe39d 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,4 @@ thecr3w-dev/ - [Specyfikacja OpenAPI Deployment Control](file:///d:/cr3w_dev/contracts/openapi/deployment-control-api.yaml) - [Schemat Bazy Danych PostgreSQL](file:///d:/cr3w_dev/infrastructure/postgres/init/01-init-schema.sql) - [Schemat Manifestu Wdrożeniowego](file:///d:/cr3w_dev/contracts/schemas/deployment-manifest.schema.json) + diff --git a/SERVER_INSTALL_GUIDE.md b/SERVER_INSTALL_GUIDE.md new file mode 100644 index 0000000..367f733 --- /dev/null +++ b/SERVER_INSTALL_GUIDE.md @@ -0,0 +1,106 @@ +# THECR3W-DEV — INSTRUKCJA WGRANIA I WDROŻENIA NA SERWERZE +## Środowisko: Netcup VPS (IP: 89.58.46.176) + +Paczka `thecr3w-dev-deploy.zip` zawiera kompletny kod źródłowy platformy, skrypty instalacji i konfiguracji, kontenery Docker oraz szablony integracji z Caddy i Keycloak. + +--- + +## Krok 1: Przesłanie paczki ZIP na serwer + +W terminalu lokalnym (PowerShell lub Linux/macOS) wykonaj: +```bash +scp thecr3w-dev-deploy.zip root@89.58.46.176:/opt/ +``` +*(lub użyj programu WinSCP / FileZilla z protokołem SFTP).* + +--- + +## Krok 2: Rozpakowanie na serwerze + +Zaloguj się na serwer przez SSH: +```bash +ssh root@89.58.46.176 +``` + +Przejdź do katalogu `/opt` i rozpakuj paczkę: +```bash +cd /opt +unzip thecr3w-dev-deploy.zip -d thecr3w +cd /opt/thecr3w +``` + +Nadaj uprawnienia wykonywania skryptom: +```bash +chmod +x deploy.sh scripts/*.sh +``` + +--- + +## Krok 3: Konfiguracja zmiennych (Opcjonalnie) + +Przejrzyj plik `.env` (jeśli nie istnieje, skrypt `deploy.sh` utworzy go automatycznie z `.env.example` i wygeneruje silne hasło bazy PostgreSQL): +```bash +cp -n .env.example .env +nano .env +``` +W `.env` możesz uzupełnić: +- `R2_ACCESS_KEY_ID=` (klucz Cloudflare R2) +- `R2_SECRET_ACCESS_KEY=` (sekret Cloudflare R2) + +--- + +## Krok 4: Uruchomienie budowania i instalacji + +Uruchom główny skrypt wdrożeniowy: +```bash +./deploy.sh +``` + +Skrypt automatycznie: +1. Zweryfikuje obecność Dockera i sieci `proxy`. +2. Zbuduje lokalnie na serwerze obrazy kontenerów (`thecr3w-backend`, `thecr3w-download-center`, `thecr3w-admin-panel`). +3. Uruchomi dedykowany kontener PostgreSQL 16 (`thecr3w-postgres`) i zainicjalizuje strukturę tabel oraz indeksów. +4. Uruchomi backend i frontendy w sieci `proxy`. +5. Sprawdzi stan usług testem healthcheck (`http://localhost:5000/health`). + +--- + +## Krok 5: Aktywacja domen w Caddy (Zero-Downtime) + +Aby podpiąć domeny `app.thecr3wnet.com` oraz `download.thecr3wnet.com` bez przerywania działania innych usług (w tym istniejącego Keycloak czy aplikacji na `thecr3wnet.com`), uruchom: +```bash +./scripts/update_caddy.sh +``` + +Skrypt: +1. Utworzy kopię zapasową `/opt/proxy/Caddyfile.bak_`. +2. Dopisze bezpiecznie bloki reverse proxy dla TheCr3w. +3. Przeprowadzi walidację składni w kontenerze `caddy`. +4. Przeładuje Caddy w trybie zero-downtime (`caddy reload`). + +--- + +## Krok 6: Weryfikacja działania + +Po zakończeniu sprawdź działanie w przeglądarce: +- **The Cr3w Download Center:** `https://download.thecr3wnet.com` +- **Native Update API (Health):** `https://app.thecr3wnet.com/health` +- **Native Update API (OpenAPI spec):** `https://app.thecr3wnet.com/api/v1/apps` + +--- + +## Przydatne polecenia utrzymaniowe + +```bash +# Podgląd stanu kontenerów TheCr3w: +docker ps --filter "name=thecr3w" + +# Podgląd logów backendu w czasie rzeczywistym: +docker logs -f thecr3w-backend + +# Restart całego stacku TheCr3w: +docker compose restart + +# Zatrzymanie stacku: +docker compose down +``` diff --git a/backend/TheCr3w.Shared/Crypto/Ed25519Service.cs b/backend/TheCr3w.Shared/Crypto/Ed25519Service.cs index 036aabf..c09d398 100644 --- a/backend/TheCr3w.Shared/Crypto/Ed25519Service.cs +++ b/backend/TheCr3w.Shared/Crypto/Ed25519Service.cs @@ -125,3 +125,4 @@ public static class Ed25519Service return Convert.ToHexString(hash).ToLowerInvariant(); } } + diff --git a/backend/TheCr3w.Shared/Crypto/ManifestSigner.cs b/backend/TheCr3w.Shared/Crypto/ManifestSigner.cs index 0b672a4..f6e882c 100644 --- a/backend/TheCr3w.Shared/Crypto/ManifestSigner.cs +++ b/backend/TheCr3w.Shared/Crypto/ManifestSigner.cs @@ -56,3 +56,4 @@ public static class ManifestSigner return Ed25519Service.ComputeSha256(canonicalBytes); } } + diff --git a/backend/TheCr3w.Shared/Data/TheCr3wDbContext.cs b/backend/TheCr3w.Shared/Data/TheCr3wDbContext.cs index 4e050f0..b2ed512 100644 --- a/backend/TheCr3w.Shared/Data/TheCr3wDbContext.cs +++ b/backend/TheCr3w.Shared/Data/TheCr3wDbContext.cs @@ -52,3 +52,4 @@ public class TheCr3wDbContext : DbContext .HasIndex(m => new { m.EnvironmentId, m.Status }); } } + diff --git a/backend/TheCr3w.Shared/External/ExternalDeploymentService.cs b/backend/TheCr3w.Shared/External/ExternalDeploymentService.cs index 4302f97..3329ce2 100644 --- a/backend/TheCr3w.Shared/External/ExternalDeploymentService.cs +++ b/backend/TheCr3w.Shared/External/ExternalDeploymentService.cs @@ -89,3 +89,4 @@ public class ExternalDeploymentService : IExternalDeploymentService return string.Equals(actualFingerprint.Trim(), expectedFingerprint.Trim(), StringComparison.OrdinalIgnoreCase); } } + diff --git a/backend/TheCr3w.Shared/Models/DeploymentManifest.cs b/backend/TheCr3w.Shared/Models/DeploymentManifest.cs index 4b994b3..24e73b1 100644 --- a/backend/TheCr3w.Shared/Models/DeploymentManifest.cs +++ b/backend/TheCr3w.Shared/Models/DeploymentManifest.cs @@ -78,3 +78,4 @@ public class RollbackStrategyConfig [JsonPropertyName("target_immutable_digest")] public string TargetImmutableDigest { get; set; } = "none"; } + diff --git a/backend/TheCr3w.Shared/Models/Enums.cs b/backend/TheCr3w.Shared/Models/Enums.cs index 51bce4f..09623f5 100644 --- a/backend/TheCr3w.Shared/Models/Enums.cs +++ b/backend/TheCr3w.Shared/Models/Enums.cs @@ -65,3 +65,4 @@ public enum SyncProtocol Ftps, Ftp } + diff --git a/backend/TheCr3w.Shared/Models/ReleaseMetadata.cs b/backend/TheCr3w.Shared/Models/ReleaseMetadata.cs index 4ca2c19..be5d120 100644 --- a/backend/TheCr3w.Shared/Models/ReleaseMetadata.cs +++ b/backend/TheCr3w.Shared/Models/ReleaseMetadata.cs @@ -55,3 +55,4 @@ public class ReleaseMetadata [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? SignatureEd25519 { get; set; } } + diff --git a/ci/forgejo/android-build-release.yml b/ci/forgejo/android-build-release.yml index 68eba29..0050566 100644 --- a/ci/forgejo/android-build-release.yml +++ b/ci/forgejo/android-build-release.yml @@ -61,3 +61,4 @@ jobs: -H "Content-Type: application/json" \ -H "Authorization: Bearer ${{ secrets.THECR3W_API_TOKEN }}" \ -d "{\"version\":\"${{ github.ref_name }}\",\"version_code\":${{ github.run_number }},\"platform\":\"android\",\"architecture\":\"arm64-v8a\",\"channel\":\"stable\",\"file_size\":$APK_SIZE,\"sha256\":\"$APK_SHA256\",\"r2_object_key\":\"$OBJECT_KEY\"}" + diff --git a/ci/forgejo/docker-build-deploy.yml b/ci/forgejo/docker-build-deploy.yml index 4f8957f..b60eca2 100644 --- a/ci/forgejo/docker-build-deploy.yml +++ b/ci/forgejo/docker-build-deploy.yml @@ -80,3 +80,4 @@ jobs: -H "Content-Type: application/json" \ -H "Authorization: Bearer ${{ secrets.THECR3W_API_TOKEN }}" \ -d @manifest.json + diff --git a/ci/forgejo/windows-build-release.yml b/ci/forgejo/windows-build-release.yml index a7ed542..22a14d7 100644 --- a/ci/forgejo/windows-build-release.yml +++ b/ci/forgejo/windows-build-release.yml @@ -24,3 +24,4 @@ jobs: shell: bash run: | echo "Publikacja wydania Windows do Cloudflare R2 i Native Update API..." + diff --git a/contracts/openapi/deployment-control-api.yaml b/contracts/openapi/deployment-control-api.yaml index 24505ab..001854f 100644 --- a/contracts/openapi/deployment-control-api.yaml +++ b/contracts/openapi/deployment-control-api.yaml @@ -425,3 +425,4 @@ components: type: boolean correlation_id: type: string + diff --git a/contracts/openapi/native-update-api.yaml b/contracts/openapi/native-update-api.yaml index ada8c71..8f4e96c 100644 --- a/contracts/openapi/native-update-api.yaml +++ b/contracts/openapi/native-update-api.yaml @@ -604,3 +604,4 @@ components: created_at: type: string format: date-time + diff --git a/contracts/schemas/deployment-manifest.schema.json b/contracts/schemas/deployment-manifest.schema.json index d593902..9bb3c14 100644 --- a/contracts/schemas/deployment-manifest.schema.json +++ b/contracts/schemas/deployment-manifest.schema.json @@ -131,3 +131,4 @@ }, "additionalProperties": false } + diff --git a/contracts/schemas/update-metadata.schema.json b/contracts/schemas/update-metadata.schema.json index 5ed792a..e6f32f6 100644 --- a/contracts/schemas/update-metadata.schema.json +++ b/contracts/schemas/update-metadata.schema.json @@ -90,3 +90,4 @@ }, "additionalProperties": false } + diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..7014de5 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# ============================================================================== +# TheCr3w-dev — Master Production Deployment Script +# System: Ubuntu 24.04 LTS (Netcup VPS) +# ============================================================================== + +set -eo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}==============================================================================${NC}" +echo -e "${BLUE} THECR3W-DEV — INSTALACJA, BUDOWANIE I WDROŻENIE PLATFORMY ${NC}" +echo -e "${BLUE}==============================================================================${NC}" + +# 1. Sprawdzenie uprawnień +if [ "$EUID" -ne 0 ] && ! groups | grep -q '\bdocker\b'; then + echo -e "${RED}[BŁĄD] Skrypt musi być uruchomiony z uprawnieniami roota lub użytkownika w grupie docker.${NC}" + exit 1 +fi + +# 2. Sprawdzenie Docker i Docker Compose +echo -e "\n${YELLOW}[1/6] Weryfikacja środowiska Docker...${NC}" +if ! command -v docker &>/dev/null; then + echo -e "${RED}[BŁĄD] Docker nie jest zainstalowany na serwerze!${NC}" + exit 1 +fi +echo -e " Docker: $(docker --version)" + +if ! docker compose version &>/dev/null; then + echo -e "${RED}[BŁĄD] Docker Compose plugin v2 nie jest dostępny!${NC}" + exit 1 +fi +echo -e " Compose: $(docker compose version)" + +# 3. Sprawdzenie zewnętrznej sieci 'proxy' (Caddy) +echo -e "\n${YELLOW}[2/6] Weryfikacja sieci Docker 'proxy' (Caddy)...${NC}" +if docker network inspect proxy &>/dev/null; then + echo -e " ${GREEN}✓ Sieć 'proxy' istnieje.${NC}" +else + echo -e " Tworzenie sieci 'proxy' dla integracji z Caddy..." + docker network create proxy + echo -e " ${GREEN}✓ Utworzono sieć 'proxy'.${NC}" +fi + +# 4. Konfiguracja pliku .env +echo -e "\n${YELLOW}[3/6] Konfiguracja zmiennych środowiskowych (.env)...${NC}" +if [ ! -f ".env" ]; then + echo -e " Kopiowanie .env.example do .env..." + cp .env.example .env +fi + +# Automatyczne wygenerowanie bezpiecznego hasła bazy danych jeśli brak +CURRENT_DB_PASS=$(grep -E '^POSTGRES_PASSWORD=' .env | cut -d'=' -f2- || true) +if [ -z "$CURRENT_DB_PASS" ]; then + NEW_DB_PASS=$(openssl rand -hex 24) + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$NEW_DB_PASS/" .env + else + sed -i "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$NEW_DB_PASS/" .env + fi + echo -e " ${GREEN}✓ Wygenerowano silne, losowe hasło PostgreSQL.${NC}" +fi + +# 5. Budowanie obrazów Docker +echo -e "\n${YELLOW}[4/6] Budowanie obrazów kontenerów platformy TheCr3w-dev...${NC}" +echo -e " Budowanie backendu .NET 10, Download Center oraz Admin Panelu..." +docker compose build --parallel + +echo -e " ${GREEN}✓ Wszystkie obrazy zostały pomyślnie zbudowane lokalnie.${NC}" + +# 6. Uruchomienie usług +echo -e "\n${YELLOW}[5/6] Uruchamianie bazy PostgreSQL i inicjalizacja schematu...${NC}" +docker compose up -d thecr3w-postgres + +echo -e " Oczekiwanie na gotowość bazy PostgreSQL..." +until docker exec thecr3w-postgres pg_isready -U thecr3w_user -d thecr3w_db &>/dev/null; do + sleep 2 + echo -n "." +done +echo -e "\n ${GREEN}✓ Baza PostgreSQL gotowa i zainicjalizowana.${NC}" + +echo -e "\n Uruchamianie usług aplikacyjnych (Backend, Download Center, Admin Panel)..." +docker compose up -d + +# 7. Weryfikacja działania kontenerów +echo -e "\n${YELLOW}[6/6] Sprawdzanie stanu uruchomionych usług...${NC}" +sleep 5 + +docker ps --filter "name=thecr3w" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +# Test lokalnego healthcheck backendu +echo -e "\n Weryfikacja healthcheck Native Update API..." +if docker exec thecr3w-backend wget -qO- http://localhost:5000/health | grep -q "Healthy"; then + echo -e " ${GREEN}✓ Native Update API działa prawidłowo (Status: Healthy).${NC}" +else + echo -e " ${YELLOW}Oczekiwanie na pełną inicjalizację API...${NC}" +fi + +echo -e "\n${GREEN}==============================================================================${NC}" +echo -e "${GREEN} WDROŻENIE KONTENERÓW THECR3W-DEV ZAKOŃCZONE SUKCESEM! ${NC}" +echo -e "${GREEN}==============================================================================${NC}" + +echo -e "\nNastępny krok: Aktualizacja reverse proxy Caddy." +echo -e "Uruchom dedykowany skrypt aktualizacji Caddyfile:" +echo -e " ${BLUE}sudo ./scripts/update_caddy.sh${NC}" +echo -e "lub ręcznie dopisz bloki domenowe zgodnie z docs/DEPLOYMENT.md." diff --git a/deployment/agent/TheCr3w.DeploymentAgent/Services/DeploymentExecutor.cs b/deployment/agent/TheCr3w.DeploymentAgent/Services/DeploymentExecutor.cs index cf33dca..d352294 100644 --- a/deployment/agent/TheCr3w.DeploymentAgent/Services/DeploymentExecutor.cs +++ b/deployment/agent/TheCr3w.DeploymentAgent/Services/DeploymentExecutor.cs @@ -168,3 +168,4 @@ public class DeploymentExecutor return false; } } + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f7c4a9d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,90 @@ +version: '3.8' + +networks: + proxy: + external: true + thecr3w_internal: + driver: bridge + internal: true + +volumes: + thecr3w_postgres_data: + driver: local + +services: + thecr3w-postgres: + image: postgres:16-alpine + container_name: thecr3w-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-thecr3w_db} + POSTGRES_USER: ${POSTGRES_USER:-thecr3w_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Brak hasla bazy w .env} + volumes: + - thecr3w_postgres_data:/var/lib/postgresql/data + - ./infrastructure/postgres/init:/docker-entrypoint-initdb.d:ro + networks: + - thecr3w_internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-thecr3w_user} -d ${POSTGRES_DB:-thecr3w_db}"] + interval: 10s + timeout: 5s + retries: 5 + + thecr3w-backend: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.backend + container_name: thecr3w-backend + restart: unless-stopped + depends_on: + thecr3w-postgres: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__DefaultConnection: Host=thecr3w-postgres;Port=5432;Database=${POSTGRES_DB:-thecr3w_db};Username=${POSTGRES_USER:-thecr3w_user};Password=${POSTGRES_PASSWORD} + Keycloak__Authority: ${KEYCLOAK_AUTHORITY:-https://login.thecr3wnet.com/realms/master} + Keycloak__Audience: ${KEYCLOAK_AUDIENCE:-thecr3w-backend} + R2__Endpoint: ${R2_ENDPOINT:-https://bf705752e38169fedb83efd5b747390e.r2.cloudflarestorage.com} + R2__Bucket: ${R2_BUCKET:-thecr3w-app} + R2__Region: ${R2_REGION:-auto} + R2__AccessKeyId: ${R2_ACCESS_KEY_ID:-} + R2__SecretAccessKey: ${R2_SECRET_ACCESS_KEY:-} + Signing__Ed25519PrivateKeyHex: ${ED25519_PRIVATE_KEY_HEX:-} + Signing__Ed25519PublicKeyHex: ${ED25519_PUBLIC_KEY_HEX:-} + networks: + - proxy + - thecr3w_internal + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:5000/health || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + + thecr3w-download-center: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.download-center + container_name: thecr3w-download-center + restart: unless-stopped + networks: + - proxy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:80/ || exit 1"] + interval: 10s + timeout: 3s + retries: 3 + + thecr3w-admin-panel: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.admin-panel + container_name: thecr3w-admin-panel + restart: unless-stopped + networks: + - proxy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:80/ || exit 1"] + interval: 10s + timeout: 3s + retries: 3 diff --git a/docs/ANDROID-SDK.md b/docs/ANDROID-SDK.md index 2fc276d..38dc7aa 100644 --- a/docs/ANDROID-SDK.md +++ b/docs/ANDROID-SDK.md @@ -43,3 +43,4 @@ CoroutineScope(Dispatchers.IO).launch { } } ``` + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 511258c..5fee12f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -142,3 +142,4 @@ W celu eliminacji ryzyka eskalacji uprawnień w administracyjnym realm `master`, 1. Klient odpytuje `GET /api/v1/apps/{app_id}/releases/{release_id}/download`. 2. Backend weryfikuje status publikacji wydania i generuje Pre-signed S3 GET URL o ważności 900 sekund (15 minut). 3. Klient pobiera plik binarny bezpośrednio z Cloudflare CDN/R2 z pełną prędkością łącza i weryfikuje SHA256. + diff --git a/docs/BACKUP-RESTORE.md b/docs/BACKUP-RESTORE.md index 33b2952..2baf095 100644 --- a/docs/BACKUP-RESTORE.md +++ b/docs/BACKUP-RESTORE.md @@ -57,3 +57,4 @@ gunzip -c /opt/backups/thecr3w/thecr3w_db_YYYYMMDD_HHMMSS.sql.gz | docker exec - # 3. Uruchomienie backendu i frontendu docker compose -f /opt/thecr3w/docker-compose.prod.yml up -d ``` + diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bf93266..4be421c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -33,3 +33,4 @@ Wszystkie istotne zmiany i wydania projektu są rejestrowane w tym dokumencie. - **Stage 8 (Security & Production Readiness):** - Przeprowadzono testy kryptograficzne, integralnościowe i API (100% testów zaliczonych). - Przygotowano kompletną dokumentację architektoniczną, bezpieczeństwa, runbooki backupu i troubleshootingu. + diff --git a/docs/CLOUDFLARE-R2.md b/docs/CLOUDFLARE-R2.md index cc49ac6..2043cfe 100644 --- a/docs/CLOUDFLARE-R2.md +++ b/docs/CLOUDFLARE-R2.md @@ -56,3 +56,4 @@ Wszystkie operacje I/O z Cloudflare R2 realizowane są w modelu Pre-signed URL: - Endpoint API: `POST /api/v1/admin/storage/upload-url`. - Czas wygasania adresu URL uploadu: **1800 sekund (30 minut)**. - Zawiera walidację oczekiwanego nagłówka `Content-Type` oraz maksymalnego rozmiaru pliku. + diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index f42f250..e56e9f1 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -127,3 +127,4 @@ docker exec caddy caddy validate --config /etc/caddy/Caddyfile # 3. Przeładowanie bez przestojów (Zero-downtime reload) docker exec caddy caddy reload --config /etc/caddy/Caddyfile ``` + diff --git a/docs/KEYCLOAK.md b/docs/KEYCLOAK.md index 0de6471..8d5144d 100644 --- a/docs/KEYCLOAK.md +++ b/docs/KEYCLOAK.md @@ -74,3 +74,4 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) }; }); ``` + diff --git a/docs/SECURITY.md b/docs/SECURITY.md index c51b5f2..37b1188 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -55,3 +55,4 @@ - Wszelkie operacje wdrożeniowe są przekazywane przez izolowany interfejs IPC/Agenta wdrożeniowego. - Dozwolona jest wyłącznie realizacja wdrożeń na podstawie podpisanych manifestów wdrożeniowych (`deployment_manifests`), których skrót SHA256 został wcześniej zatwierdzony przez uprawnionego Approvera. - Wdrożenia produkcyjne po zmiennym tagu `:latest` są **programowo blokowane** przez walidację schematu. + diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 2d785fe..27a094f 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -46,3 +46,4 @@ - **Rozwiązanie:** - Ponowne wywołanie endpointu pobierania w celu wygenerowania świeżego URL. - Weryfikacja synchronizacji NTP na serwerze: `timedatectl status`. + diff --git a/docs/WINDOWS-SDK.md b/docs/WINDOWS-SDK.md index 21b38f5..d33d093 100644 --- a/docs/WINDOWS-SDK.md +++ b/docs/WINDOWS-SDK.md @@ -48,3 +48,4 @@ if (update.UpdateAvailable) TheCr3wUpdater.LaunchInstallerAndExit(tempPath, "/SILENT"); } ``` + diff --git a/frontend/admin-panel/index.html b/frontend/admin-panel/index.html index 4cfd9eb..70084cb 100644 --- a/frontend/admin-panel/index.html +++ b/frontend/admin-panel/index.html @@ -10,3 +10,4 @@ + diff --git a/frontend/admin-panel/package.json b/frontend/admin-panel/package.json index 1a3693e..ddaa3ab 100644 --- a/frontend/admin-panel/package.json +++ b/frontend/admin-panel/package.json @@ -20,3 +20,4 @@ "vite": "^6.0.0" } } + diff --git a/frontend/admin-panel/src/App.tsx b/frontend/admin-panel/src/App.tsx index 4678bd8..a7d20b6 100644 --- a/frontend/admin-panel/src/App.tsx +++ b/frontend/admin-panel/src/App.tsx @@ -427,3 +427,4 @@ export const App: React.FC = () => { ); }; + diff --git a/frontend/admin-panel/src/index.css b/frontend/admin-panel/src/index.css index f59aa25..359e21d 100644 --- a/frontend/admin-panel/src/index.css +++ b/frontend/admin-panel/src/index.css @@ -181,3 +181,4 @@ body { .btn-primary:hover { background: var(--primary-hover); } .btn-danger { background: var(--danger); color: white; } .btn-secondary { background: var(--bg-sidebar); color: var(--text-main); border: 1px solid var(--border-color); } + diff --git a/frontend/admin-panel/src/main.tsx b/frontend/admin-panel/src/main.tsx index d0aea34..608c6c1 100644 --- a/frontend/admin-panel/src/main.tsx +++ b/frontend/admin-panel/src/main.tsx @@ -8,3 +8,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render( ); + diff --git a/frontend/admin-panel/src/types.ts b/frontend/admin-panel/src/types.ts index 3b26fd5..781183e 100644 --- a/frontend/admin-panel/src/types.ts +++ b/frontend/admin-panel/src/types.ts @@ -59,3 +59,4 @@ export interface AuditEventItem { success: boolean; ipAddress?: string; } + diff --git a/frontend/admin-panel/src/vite-env.d.ts b/frontend/admin-panel/src/vite-env.d.ts index 11f02fe..ed77210 100644 --- a/frontend/admin-panel/src/vite-env.d.ts +++ b/frontend/admin-panel/src/vite-env.d.ts @@ -1 +1,2 @@ /// + diff --git a/frontend/admin-panel/tsconfig.json b/frontend/admin-panel/tsconfig.json index 571c6d4..fecf774 100644 --- a/frontend/admin-panel/tsconfig.json +++ b/frontend/admin-panel/tsconfig.json @@ -19,3 +19,4 @@ }, "include": ["src"] } + diff --git a/frontend/admin-panel/vite.config.ts b/frontend/admin-panel/vite.config.ts index d90fc28..cea77c1 100644 --- a/frontend/admin-panel/vite.config.ts +++ b/frontend/admin-panel/vite.config.ts @@ -7,3 +7,4 @@ export default defineConfig({ port: 3001 } }); + diff --git a/frontend/download-center/index.html b/frontend/download-center/index.html index 7bfee4c..800f5da 100644 --- a/frontend/download-center/index.html +++ b/frontend/download-center/index.html @@ -11,3 +11,4 @@ + diff --git a/frontend/download-center/src/App.tsx b/frontend/download-center/src/App.tsx index 9853ffd..a72cdc8 100644 --- a/frontend/download-center/src/App.tsx +++ b/frontend/download-center/src/App.tsx @@ -315,3 +315,4 @@ export const App: React.FC = () => { ); }; + diff --git a/frontend/download-center/src/index.css b/frontend/download-center/src/index.css index a3c9129..7701e12 100644 --- a/frontend/download-center/src/index.css +++ b/frontend/download-center/src/index.css @@ -314,3 +314,4 @@ body { color: var(--text-secondary); font-size: 0.85rem; } + diff --git a/frontend/download-center/src/main.tsx b/frontend/download-center/src/main.tsx index d0aea34..608c6c1 100644 --- a/frontend/download-center/src/main.tsx +++ b/frontend/download-center/src/main.tsx @@ -8,3 +8,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render( ); + diff --git a/frontend/download-center/src/services/api.ts b/frontend/download-center/src/services/api.ts index a2e6c6d..1acb9be 100644 --- a/frontend/download-center/src/services/api.ts +++ b/frontend/download-center/src/services/api.ts @@ -45,3 +45,4 @@ export async function getDownloadUrl(appId: string, releaseId: string): Promise< return null; } } + diff --git a/frontend/download-center/src/types.ts b/frontend/download-center/src/types.ts index ab1e16f..9f3df73 100644 --- a/frontend/download-center/src/types.ts +++ b/frontend/download-center/src/types.ts @@ -29,3 +29,4 @@ export interface DownloadUrlResponse { sha256: string; signature_ed25519: string; } + diff --git a/frontend/download-center/src/vite-env.d.ts b/frontend/download-center/src/vite-env.d.ts index 11f02fe..ed77210 100644 --- a/frontend/download-center/src/vite-env.d.ts +++ b/frontend/download-center/src/vite-env.d.ts @@ -1 +1,2 @@ /// + diff --git a/frontend/download-center/vite.config.ts b/frontend/download-center/vite.config.ts index a0cd71c..c74b0ea 100644 --- a/frontend/download-center/vite.config.ts +++ b/frontend/download-center/vite.config.ts @@ -6,3 +6,4 @@ export default defineConfig({ port: 3000 } }); + diff --git a/infrastructure/docker/Dockerfile.admin-panel b/infrastructure/docker/Dockerfile.admin-panel index c507820..708783d 100644 --- a/infrastructure/docker/Dockerfile.admin-panel +++ b/infrastructure/docker/Dockerfile.admin-panel @@ -23,3 +23,4 @@ HEALTHCHECK --interval=10s --timeout=3s --retries=3 \ CMD wget -qO- http://localhost:80/ || exit 1 CMD ["nginx", "-g", "daemon off;"] + diff --git a/infrastructure/docker/Dockerfile.backend b/infrastructure/docker/Dockerfile.backend index 99d05bc..bff450b 100644 --- a/infrastructure/docker/Dockerfile.backend +++ b/infrastructure/docker/Dockerfile.backend @@ -37,3 +37,4 @@ HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ CMD wget -qO- http://localhost:5000/health || exit 1 ENTRYPOINT ["dotnet", "NativeUpdate.Api.dll"] + diff --git a/infrastructure/docker/Dockerfile.download-center b/infrastructure/docker/Dockerfile.download-center index ed152e9..fb86e5c 100644 --- a/infrastructure/docker/Dockerfile.download-center +++ b/infrastructure/docker/Dockerfile.download-center @@ -23,3 +23,4 @@ HEALTHCHECK --interval=10s --timeout=3s --retries=3 \ CMD wget -qO- http://localhost:80/ || exit 1 CMD ["nginx", "-g", "daemon off;"] + diff --git a/infrastructure/docker/docker-compose.forgejo.yml b/infrastructure/docker/docker-compose.forgejo.yml index 11fa53a..660e7f1 100644 --- a/infrastructure/docker/docker-compose.forgejo.yml +++ b/infrastructure/docker/docker-compose.forgejo.yml @@ -34,3 +34,4 @@ services: interval: 15s timeout: 5s retries: 3 + diff --git a/infrastructure/docker/docker-compose.prod.yml b/infrastructure/docker/docker-compose.prod.yml new file mode 100644 index 0000000..f7c4a9d --- /dev/null +++ b/infrastructure/docker/docker-compose.prod.yml @@ -0,0 +1,90 @@ +version: '3.8' + +networks: + proxy: + external: true + thecr3w_internal: + driver: bridge + internal: true + +volumes: + thecr3w_postgres_data: + driver: local + +services: + thecr3w-postgres: + image: postgres:16-alpine + container_name: thecr3w-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-thecr3w_db} + POSTGRES_USER: ${POSTGRES_USER:-thecr3w_user} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Brak hasla bazy w .env} + volumes: + - thecr3w_postgres_data:/var/lib/postgresql/data + - ./infrastructure/postgres/init:/docker-entrypoint-initdb.d:ro + networks: + - thecr3w_internal + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-thecr3w_user} -d ${POSTGRES_DB:-thecr3w_db}"] + interval: 10s + timeout: 5s + retries: 5 + + thecr3w-backend: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.backend + container_name: thecr3w-backend + restart: unless-stopped + depends_on: + thecr3w-postgres: + condition: service_healthy + environment: + ASPNETCORE_ENVIRONMENT: Production + ConnectionStrings__DefaultConnection: Host=thecr3w-postgres;Port=5432;Database=${POSTGRES_DB:-thecr3w_db};Username=${POSTGRES_USER:-thecr3w_user};Password=${POSTGRES_PASSWORD} + Keycloak__Authority: ${KEYCLOAK_AUTHORITY:-https://login.thecr3wnet.com/realms/master} + Keycloak__Audience: ${KEYCLOAK_AUDIENCE:-thecr3w-backend} + R2__Endpoint: ${R2_ENDPOINT:-https://bf705752e38169fedb83efd5b747390e.r2.cloudflarestorage.com} + R2__Bucket: ${R2_BUCKET:-thecr3w-app} + R2__Region: ${R2_REGION:-auto} + R2__AccessKeyId: ${R2_ACCESS_KEY_ID:-} + R2__SecretAccessKey: ${R2_SECRET_ACCESS_KEY:-} + Signing__Ed25519PrivateKeyHex: ${ED25519_PRIVATE_KEY_HEX:-} + Signing__Ed25519PublicKeyHex: ${ED25519_PUBLIC_KEY_HEX:-} + networks: + - proxy + - thecr3w_internal + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:5000/health || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + + thecr3w-download-center: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.download-center + container_name: thecr3w-download-center + restart: unless-stopped + networks: + - proxy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:80/ || exit 1"] + interval: 10s + timeout: 3s + retries: 3 + + thecr3w-admin-panel: + build: + context: . + dockerfile: infrastructure/docker/Dockerfile.admin-panel + container_name: thecr3w-admin-panel + restart: unless-stopped + networks: + - proxy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:80/ || exit 1"] + interval: 10s + timeout: 3s + retries: 3 diff --git a/infrastructure/postgres/init/01-init-schema.sql b/infrastructure/postgres/init/01-init-schema.sql index 35ed3b9..c26e299 100644 --- a/infrastructure/postgres/init/01-init-schema.sql +++ b/infrastructure/postgres/init/01-init-schema.sql @@ -255,3 +255,4 @@ CREATE TABLE upload_sessions ( ); CREATE INDEX idx_upload_sessions_status ON upload_sessions (status, expires_at); + diff --git a/scripts/update_caddy.sh b/scripts/update_caddy.sh new file mode 100644 index 0000000..baf768d --- /dev/null +++ b/scripts/update_caddy.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# ============================================================================== +# TheCr3w-dev — Bezpieczny skrypt aktualizacji Caddyfile +# Lokalizacja Caddyfile: /opt/proxy/Caddyfile +# Kontener: caddy +# ============================================================================== + +set -eo pipefail + +CADDYFILE="/opt/proxy/Caddyfile" +CADDY_CONTAINER="caddy" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="/opt/proxy/Caddyfile.bak_${TIMESTAMP}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +echo -e "${BLUE}==============================================================================${NC}" +echo -e "${BLUE} THECR3W-DEV — BEZPIECZNA AKTUALIZACJA REVERSE PROXY CADDY ${NC}" +echo -e "${BLUE}==============================================================================${NC}" + +# 1. Sprawdzenie uprawnień +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}[BŁĄD] Skrypt wymaga uprawnień roota (sudo).${NC}" + exit 1 +fi + +# 2. Sprawdzenie obecności pliku +if [ ! -f "$CADDYFILE" ]; then + echo -e "${RED}[BŁĄD] Plik $CADDYFILE nie istnieje!${NC}" + exit 1 +fi + +# 3. Kopia zapasowa +echo -e "\n${YELLOW}[1/4] Tworzenie kopii zapasowej Caddyfile...${NC}" +cp "$CADDYFILE" "$BACKUP_FILE" +echo -e " ${GREEN}✓ Kopia zapasowa zapisana w: $BACKUP_FILE${NC}" + +# 4. Sprawdzenie czy domeny są już skonfigurowane +echo -e "\n${YELLOW}[2/4] Weryfikacja istniejących wpisów domenowych...${NC}" +if grep -q "app.thecr3wnet.com" "$CADDYFILE"; then + echo -e " ${YELLOW}Uwaga: app.thecr3wnet.com jest już zdefiniowana w Caddyfile.${NC}" +else + echo -e " Dopisywanie reguł dla app.thecr3wnet.com i download.thecr3wnet.com..." + cat << 'EOF' >> "$CADDYFILE" + +# ------------------------------------------------------------------------------ +# TheCr3w-dev Ecosystem Services +# ------------------------------------------------------------------------------ +app.thecr3wnet.com { + reverse_proxy thecr3w-backend:5000 { + header_up X-Forwarded-Proto {scheme} + header_up X-Real-IP {remote_host} + } +} + +download.thecr3wnet.com { + reverse_proxy thecr3w-download-center:80 { + header_up X-Forwarded-Proto {scheme} + header_up X-Real-IP {remote_host} + } +} +EOF + echo -e " ${GREEN}✓ Dopisek dodany pomyślnie.${NC}" +fi + +# 5. Walidacja składni w kontenerze Caddy +echo -e "\n${YELLOW}[3/4] Walidacja składni wewnątrz kontenera Caddy...${NC}" +if docker exec "$CADDY_CONTAINER" caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile; then + echo -e " ${GREEN}✓ Składnia Caddyfile jest w 100% poprawna!${NC}" +else + echo -e "${RED}[BŁĄD] Błąd walidacji składni! Przywracanie kopii zapasowej...${NC}" + cp "$BACKUP_FILE" "$CADDYFILE" + echo -e " Przywrócono oryginalny plik z kopii: $BACKUP_FILE" + exit 1 +fi + +# 6. Przeładowanie Caddy bez przestojów +echo -e "\n${YELLOW}[4/4] Przeładowanie Caddy (Zero-Downtime Reload)...${NC}" +docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile +echo -e " ${GREEN}✓ Caddy pomyślnie przeładowany bez przestojów!${NC}" + +echo -e "\n${GREEN}==============================================================================${NC}" +echo -e "${GREEN} KONFIGURACJA DOMEN ZAKOŃCZONA SUKCESEM! ${NC}" +echo -e "${GREEN} Domeny aktywne: ${NC}" +echo -e "${GREEN} - https://app.thecr3wnet.com (Native Update API) ${NC}" +echo -e "${GREEN} - https://download.thecr3wnet.com (The Cr3w Download Center) ${NC}" +echo -e "${GREEN}==============================================================================${NC}" diff --git a/sdk/android/thecr3w-update-sdk/src/main/kotlin/com/thecr3w/update/TheCr3wUpdater.kt b/sdk/android/thecr3w-update-sdk/src/main/kotlin/com/thecr3w/update/TheCr3wUpdater.kt index 76160a0..adb1b85 100644 --- a/sdk/android/thecr3w-update-sdk/src/main/kotlin/com/thecr3w/update/TheCr3wUpdater.kt +++ b/sdk/android/thecr3w-update-sdk/src/main/kotlin/com/thecr3w/update/TheCr3wUpdater.kt @@ -174,3 +174,4 @@ class TheCr3wUpdater( return digest.digest().joinToString("") { "%02x".format(it) } } } + diff --git a/sdk/windows/TheCr3w.WindowsUpdate.Sdk/TheCr3wUpdater.cs b/sdk/windows/TheCr3w.WindowsUpdate.Sdk/TheCr3wUpdater.cs index 4911a2f..b1bac32 100644 --- a/sdk/windows/TheCr3w.WindowsUpdate.Sdk/TheCr3wUpdater.cs +++ b/sdk/windows/TheCr3w.WindowsUpdate.Sdk/TheCr3wUpdater.cs @@ -130,3 +130,4 @@ public class TheCr3wUpdater Environment.Exit(0); } } + diff --git a/tests/TheCr3w.Tests/ExternalDeploymentTests.cs b/tests/TheCr3w.Tests/ExternalDeploymentTests.cs index 387ebe3..5e6dbc0 100644 --- a/tests/TheCr3w.Tests/ExternalDeploymentTests.cs +++ b/tests/TheCr3w.Tests/ExternalDeploymentTests.cs @@ -41,3 +41,4 @@ public class ExternalDeploymentTests Assert.True(_service.ValidateHostKey("SHA256:abc123actual", "SHA256:abc123actual")); } } +