From 00e681f5d8466b34f45daf1b6522b10a8a589a81 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Fri, 6 Feb 2026 01:15:53 +0700 Subject: [PATCH 1/9] chore: remove deploy workflow and add VSCode settings for dotnet solution --- .github/workflows/deploy-docsify.yml | 44 ---------------------------- .vscode/settings.json | 3 ++ SnakeAid.Docs | 2 +- 3 files changed, 4 insertions(+), 45 deletions(-) delete mode 100644 .github/workflows/deploy-docsify.yml create mode 100644 .vscode/settings.json diff --git a/.github/workflows/deploy-docsify.yml b/.github/workflows/deploy-docsify.yml deleted file mode 100644 index 4ab10e1a..00000000 --- a/.github/workflows/deploy-docsify.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Deploy Docsify to GitHub Pages - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload Docs artifact - uses: actions/upload-pages-artifact@v3 - with: - path: docs - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: github-pages - url: ${{ steps.deploy.outputs.page_url }} - steps: - - name: Deploy to GitHub Pages - id: deploy - uses: actions/deploy-pages@v4 - - - name: Show deployed link - run: echo "Docs available at ${{ steps.deploy.outputs.page_url }}" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..fe46a187 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "SnakeAid.Api/SnakeAid.Api.sln" +} \ No newline at end of file diff --git a/SnakeAid.Docs b/SnakeAid.Docs index 7377ac31..1976c6f4 160000 --- a/SnakeAid.Docs +++ b/SnakeAid.Docs @@ -1 +1 @@ -Subproject commit 7377ac31de0541886c28e33772eaac8df30f1bc5 +Subproject commit 1976c6f4e921976550de5fba46f33197db52923c From 4603bfa8ad16a02f362824225bb1d123c642b43a Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Fri, 6 Feb 2026 02:36:04 +0700 Subject: [PATCH 2/9] feat: enhance Jenkins pipeline with improved Docker build and cleanup processes --- Jenkinsfile | 199 +++++++++++++++++++++++++++++----------------------- 1 file changed, 111 insertions(+), 88 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9d4d8749..5da8bafa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,6 +1,6 @@ // ---------- Helpers ---------- def utcCreated() { - return sh(script: "date -u +%Y-%m-%dT%H:%M:%SZ", returnStdout: true).trim() + sh(script: "date -u +%Y-%m-%dT%H:%M:%SZ", returnStdout: true).trim() } def normalizeRepoUrl(String rawRepo) { @@ -8,30 +8,108 @@ def normalizeRepoUrl(String rawRepo) { if (repoUrl.startsWith('git@github.com:')) { repoUrl = repoUrl.replace('git@github.com:', 'https://github.com/') } - return repoUrl.replaceAll(/\.git$/, '') + repoUrl.replaceAll(/\.git$/, '') } def commitUrl(String repoUrl, String commitSha) { - return (repoUrl && commitSha) ? "${repoUrl}/commit/${commitSha}" : (repoUrl ?: '') + (repoUrl && commitSha) ? "${repoUrl}/commit/${commitSha}" : (repoUrl ?: '') +} + +def guessRefName() { + env.CHANGE_ID ? "PR-${env.CHANGE_ID}" : (env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown') +} + +@NonCPS +Map resolveDeployContext(String branchName, String changeId, String changeTarget) { + final boolean isPr = (changeId != null) + + if (isPr) { + if (changeTarget == 'main') return [stage: 'staging', environment: 'preview'] + if (changeTarget == 'dev') return [stage: 'development', environment: 'pr'] + return [stage: 'development', environment: 'pr'] + } + + if (branchName == 'main') return [stage: 'production', environment: 'production'] + if (branchName == 'dev') return [stage: 'staging', environment: 'staging'] + return [stage: 'development', environment: 'development'] } def ociLabelArgs(String tag) { def created = utcCreated() def repoUrl = normalizeRepoUrl(env.GIT_URL ?: '') - def url = commitUrl(repoUrl, env.GIT_COMMIT ?: '') + def url = commitUrl(repoUrl, env.GIT_COMMIT ?: '') + def refName = guessRefName() + def ctx = resolveDeployContext(env.BRANCH_NAME, env.CHANGE_ID, env.CHANGE_TARGET) + + def baseImage = "mcr.microsoft.com/dotnet/aspnet:8.0" + def docsUrl = "https://snake-aid.github.io/SnakeAid.Docs" - // IMPORTANT: must be a SINGLE LINE to avoid Jenkins `sh` interpreting `--label` as a separate command def labels = [ "org.opencontainers.image.source=${repoUrl}", "org.opencontainers.image.revision=${env.GIT_COMMIT ?: ''}", "org.opencontainers.image.url=${url}", "org.opencontainers.image.created=${created}", - "org.opencontainers.image.version=${tag}" + "org.opencontainers.image.version=${tag}", + "org.opencontainers.image.ref.name=${refName}", + + "org.opencontainers.image.title=snakeaid-api", + "org.opencontainers.image.description=SnakeAid Backend API", + "org.opencontainers.image.vendor=SnakeAid", + "org.opencontainers.image.documentation=${docsUrl}", + "org.opencontainers.image.authors=thekhiem7", + + "org.opencontainers.image.base.name=${baseImage}", + "org.opencontainers.image.build.source=jenkins", + "org.opencontainers.image.build.version=${env.JENKINS_VERSION ?: 'unknown'}", + + "org.opencontainers.image.stage=${ctx.stage}", + "org.opencontainers.image.environment=${ctx.environment}", ].collect { "--label ${it}" }.join(' ') return "-f Dockerfile ${labels} ." } +def dockerBuildOnly(String tag) { + docker.build("${env.IMAGE}:${tag}", ociLabelArgs(tag)) +} + +def dockerBuildAndPush(String tag) { + def img = docker.build("${env.IMAGE}:${tag}", ociLabelArgs(tag)) + docker.withRegistry(env.REGISTRY_URL, env.REGISTRY_CREDENTIAL) { + img.push(tag) + } +} + +@NonCPS +int resolveCacheTtlHours(String branchName, String changeId) { + final int PR_TTL_HOURS = 24 + final int DEV_TTL_HOURS = 48 + final int MAIN_TTL_HOURS = 48 + final int DEFAULT_TTL_HOURS = 24 + + if (changeId != null) return PR_TTL_HOURS + if (branchName == 'main') return MAIN_TTL_HOURS + if (branchName == 'dev') return DEV_TTL_HOURS + return DEFAULT_TTL_HOURS +} + +void dockerCleanup(int cacheTtlHours) { + sh """ + set +e + + echo "[cleanup] docker image prune (dangling only)" + docker image prune -f + + echo "[cleanup] docker builder prune (until=${cacheTtlHours}h)" + docker builder prune -f --filter "until=${cacheTtlHours}h" + + echo "[cleanup] docker container prune (stopped)" + docker container prune -f + + exit 0 + """ +} + // ---------- Pipeline ---------- pipeline { agent any @@ -42,118 +120,63 @@ pipeline { } environment { - TIME_STAMP_FORMAT = "dd-MM-yyyy HH:mm:ss" - GITHUB_PR_URL = 'https://github.com/Snake-AID/SnakeAid.Backend/pull/' - IMAGE = 'thekhiem7/snakeaid-api' + IMAGE = 'thekhiem7/snakeaid-api' REGISTRY_CREDENTIAL = 'thekhiem7-dockerhub-credentials' - REGISTRY_URL = 'https://index.docker.io/v1/' + REGISTRY_URL = 'https://index.docker.io/v1/' } stages { stage('Checkout') { - steps { - checkout scm - } + steps { checkout scm } } - stage('Build Check (PR to dev)') { - when { - expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'dev' } - } - + stage('Build Check (PR -> dev)') { + when { expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'dev' } } steps { - script { - def tag = "pr-${env.CHANGE_ID}" - docker.build("${IMAGE}:${tag}", ociLabelArgs(tag)) - - // Cleanup: remove local image after build check - sh "docker rmi ${IMAGE}:${tag} --force || true" - } + script { dockerBuildOnly("pr-${env.CHANGE_ID}") } } } stage('Publish Dev (Merged to dev)') { - when { - allOf { - branch 'dev' - not { changeRequest() } - } - } - + when { allOf { branch 'dev'; not { changeRequest() } } } steps { - script { - def tag = "dev" - def img = docker.build("${IMAGE}:${tag}", ociLabelArgs(tag)) - - docker.withRegistry(REGISTRY_URL, REGISTRY_CREDENTIAL) { - img.push() - } - - // Cleanup - sh "docker rmi ${IMAGE}:${tag} --force || true" - } + script { dockerBuildAndPush('dev') } } } stage('Publish Preview (PR to main)') { - when { - expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'main' } - } - + when { expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'main' } } steps { - script { - def tag = "preview-${env.CHANGE_ID}" - def img = docker.build("${IMAGE}:${tag}", ociLabelArgs(tag)) - - docker.withRegistry(REGISTRY_URL, REGISTRY_CREDENTIAL) { - img.push() - } - - // Cleanup - sh "docker rmi ${IMAGE}:${tag} --force || true" - } + script { dockerBuildAndPush("preview-${env.CHANGE_ID}") } } } stage('Publish Latest (Merged to main)') { - when { - allOf { - branch 'main' - not { changeRequest() } - } - } - + when { allOf { branch 'main'; not { changeRequest() } } } steps { - script { - def tag = "latest" - def img = docker.build("${IMAGE}:${tag}", ociLabelArgs(tag)) - - docker.withRegistry(REGISTRY_URL, REGISTRY_CREDENTIAL) { - img.push() - } - - // Cleanup - sh "docker rmi ${IMAGE}:${tag} --force || true" - } + script { dockerBuildAndPush('latest') } } } stage('Deploy (Portainer Webhook)') { - when { - allOf { - branch 'main' - not { changeRequest() } + when { allOf { branch 'main'; not { changeRequest() } } } + steps { + withCredentials([string(credentialsId: 'portainer-snakeaid-webhook', variable: 'PORTAINER_WEBHOOK')]) { + sh 'curl -fsS -X POST "$PORTAINER_WEBHOOK"' } } + } + } - steps { - withCredentials([ - string(credentialsId: 'portainer-snakeaid-webhook', variable: 'PORTAINER_WEBHOOK') - ]) { - sh ''' - set -e - curl -fsS -X POST "$PORTAINER_WEBHOOK" - ''' + post { + always { + script { + stage('Docker Cleanup') { + catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { + def ttl = resolveCacheTtlHours(env.BRANCH_NAME, env.CHANGE_ID) + echo "[cleanup] ttl=${ttl}h" + dockerCleanup(ttl) + } } } } From 75ecb0944f8ebcb6a4388e053d92f75975cc472a Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Fri, 6 Feb 2026 03:15:41 +0700 Subject: [PATCH 3/9] feat: refactor Docker build and cleanup processes in Jenkins pipeline --- Jenkinsfile | 118 ++++++++++++++++++++++++++-------------------------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 5da8bafa..4edf9b34 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -66,48 +66,31 @@ def ociLabelArgs(String tag) { "org.opencontainers.image.environment=${ctx.environment}", ].collect { "--label ${it}" }.join(' ') - return "-f Dockerfile ${labels} ." + return "-f Dockerfile ${labels}" } -def dockerBuildOnly(String tag) { - docker.build("${env.IMAGE}:${tag}", ociLabelArgs(tag)) -} - -def dockerBuildAndPush(String tag) { - def img = docker.build("${env.IMAGE}:${tag}", ociLabelArgs(tag)) - docker.withRegistry(env.REGISTRY_URL, env.REGISTRY_CREDENTIAL) { - img.push(tag) +// ---------- Docker wrapper (AUTO CLEANUP) ---------- +def withDockerImage(String tag, Closure body) { + try { + body() + } finally { + sh "docker rmi ${env.IMAGE}:${tag} --force || true" } } -@NonCPS -int resolveCacheTtlHours(String branchName, String changeId) { - final int PR_TTL_HOURS = 24 - final int DEV_TTL_HOURS = 48 - final int MAIN_TTL_HOURS = 48 - final int DEFAULT_TTL_HOURS = 24 - - if (changeId != null) return PR_TTL_HOURS - if (branchName == 'main') return MAIN_TTL_HOURS - if (branchName == 'dev') return DEV_TTL_HOURS - return DEFAULT_TTL_HOURS +def dockerBuildOnly(String tag) { + withDockerImage(tag) { + docker.build("${env.IMAGE}:${tag}","${ociLabelArgs(tag)} .") + } } -void dockerCleanup(int cacheTtlHours) { - sh """ - set +e - - echo "[cleanup] docker image prune (dangling only)" - docker image prune -f - - echo "[cleanup] docker builder prune (until=${cacheTtlHours}h)" - docker builder prune -f --filter "until=${cacheTtlHours}h" - - echo "[cleanup] docker container prune (stopped)" - docker container prune -f - - exit 0 - """ +def dockerBuildAndPush(String tag) { + withDockerImage(tag) { + def img = docker.build("${env.IMAGE}:${tag}","${ociLabelArgs(tag)} .") + docker.withRegistry(env.REGISTRY_URL, env.REGISTRY_CREDENTIAL) { + img.push(tag) + } + } } // ---------- Pipeline ---------- @@ -131,54 +114,69 @@ pipeline { } stage('Build Check (PR -> dev)') { - when { expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'dev' } } + when { + expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'dev' } + } steps { - script { dockerBuildOnly("pr-${env.CHANGE_ID}") } + script { + dockerBuildOnly("pr-${env.CHANGE_ID}") + } } } stage('Publish Dev (Merged to dev)') { - when { allOf { branch 'dev'; not { changeRequest() } } } + when { + allOf { + branch 'dev' + not { changeRequest() } + } + } steps { - script { dockerBuildAndPush('dev') } + script { + dockerBuildAndPush('dev') + } } } stage('Publish Preview (PR to main)') { - when { expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'main' } } + when { + expression { env.CHANGE_ID != null && env.CHANGE_TARGET == 'main' } + } steps { - script { dockerBuildAndPush("preview-${env.CHANGE_ID}") } + script { + dockerBuildAndPush("preview-${env.CHANGE_ID}") + } } } stage('Publish Latest (Merged to main)') { - when { allOf { branch 'main'; not { changeRequest() } } } + when { + allOf { + branch 'main' + not { changeRequest() } + } + } steps { - script { dockerBuildAndPush('latest') } + script { + dockerBuildAndPush('latest') + } } } stage('Deploy (Portainer Webhook)') { - when { allOf { branch 'main'; not { changeRequest() } } } - steps { - withCredentials([string(credentialsId: 'portainer-snakeaid-webhook', variable: 'PORTAINER_WEBHOOK')]) { - sh 'curl -fsS -X POST "$PORTAINER_WEBHOOK"' + when { + allOf { + branch 'main' + not { changeRequest() } } } - } - } - - post { - always { - script { - stage('Docker Cleanup') { - catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') { - def ttl = resolveCacheTtlHours(env.BRANCH_NAME, env.CHANGE_ID) - echo "[cleanup] ttl=${ttl}h" - dockerCleanup(ttl) - } + steps { + withCredentials([ + string(credentialsId: 'portainer-snakeaid-webhook', variable: 'PORTAINER_WEBHOOK') + ]) { + sh 'curl -fsS -X POST "$PORTAINER_WEBHOOK"' } } } } -} +} \ No newline at end of file From 7cc33420f297c1e0be9f32f248d4785730938d06 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Fri, 6 Feb 2026 03:25:11 +0700 Subject: [PATCH 4/9] hotfix(Jenkins): Convert from docker plugin into sh command to avoud buildx problem --- Jenkinsfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4edf9b34..7af9f9ed 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,15 +80,14 @@ def withDockerImage(String tag, Closure body) { def dockerBuildOnly(String tag) { withDockerImage(tag) { - docker.build("${env.IMAGE}:${tag}","${ociLabelArgs(tag)} .") + sh "docker buildx build -t ${env.IMAGE}:${tag} ${ociLabelArgs(tag)} --load ." } } def dockerBuildAndPush(String tag) { withDockerImage(tag) { - def img = docker.build("${env.IMAGE}:${tag}","${ociLabelArgs(tag)} .") docker.withRegistry(env.REGISTRY_URL, env.REGISTRY_CREDENTIAL) { - img.push(tag) + sh "docker buildx build -t ${env.IMAGE}:${tag} ${ociLabelArgs(tag)} --push ." } } } From 95449822b4aeafa772655c9b6d668d8e2b6e1c7a Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Fri, 6 Feb 2026 03:53:43 +0700 Subject: [PATCH 5/9] fix: update Docker build commands to use docker.build for improved compatibility --- Jenkinsfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7af9f9ed..129b8d76 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -64,7 +64,7 @@ def ociLabelArgs(String tag) { "org.opencontainers.image.stage=${ctx.stage}", "org.opencontainers.image.environment=${ctx.environment}", - ].collect { "--label ${it}" }.join(' ') + ].collect { "--label \"${it}\"" }.join(' ') return "-f Dockerfile ${labels}" } @@ -80,14 +80,15 @@ def withDockerImage(String tag, Closure body) { def dockerBuildOnly(String tag) { withDockerImage(tag) { - sh "docker buildx build -t ${env.IMAGE}:${tag} ${ociLabelArgs(tag)} --load ." + docker.build("${env.IMAGE}:${tag}","${ociLabelArgs(tag)} .") } } def dockerBuildAndPush(String tag) { withDockerImage(tag) { + def img = docker.build("${env.IMAGE}:${tag}","${ociLabelArgs(tag)} .") docker.withRegistry(env.REGISTRY_URL, env.REGISTRY_CREDENTIAL) { - sh "docker buildx build -t ${env.IMAGE}:${tag} ${ociLabelArgs(tag)} --push ." + img.push(tag) } } } From 305d914a5b07d465821a5b54027ad329053c1b11 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Fri, 6 Feb 2026 20:54:47 +0700 Subject: [PATCH 6/9] chore: update subproject commit reference in SnakeAid.Docs --- SnakeAid.Docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SnakeAid.Docs b/SnakeAid.Docs index 1976c6f4..0cc11121 160000 --- a/SnakeAid.Docs +++ b/SnakeAid.Docs @@ -1 +1 @@ -Subproject commit 1976c6f4e921976550de5fba46f33197db52923c +Subproject commit 0cc11121118d6417e32a180f42ac45340607b915 From c07587ebf5bb28d59c855360ff7e778413bd8176 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Sat, 7 Feb 2026 15:14:44 +0700 Subject: [PATCH 7/9] feat: centralize Doppler configuration management and enhance app settings integration --- .vscode/settings.json | 40 ++++++- Directory.Packages.props | 1 + SnakeAid.Api/DI/DependencyInjection.cs | 111 ++++++++++++++++++++ SnakeAid.Api/Program.cs | 5 +- SnakeAid.Api/Properties/launchSettings.json | 21 ++-- SnakeAid.Api/SnakeAid.Api.csproj | 1 + SnakeAid.Core/Exceptions/ApiException.cs | 8 ++ SnakeAid.Core/Settings/AppSettings.cs | 9 +- 8 files changed, 187 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fe46a187..2937b7b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,41 @@ { - "dotnet.defaultSolution": "SnakeAid.Api/SnakeAid.Api.sln" + "dotnet.defaultSolution": "SnakeAid.Backend.sln", + "files.exclude": { + "**/bin": false, + "**/obj": false, + "**/Properties": false, + ".vscode": false, + "**/node_modules": false, + "**/packages": false, + "**/.vs": false, + "**/dist": false, + "**/coverage": false, + "**/artifacts": false, + "**/logs": false, + "**/*.dll": false, + "**/*.pdb": false, + "**/*.exe": false, + "**/*.nupkg": false, + "**/*.zip": false, + "**/*.tar.gz": false, + "**/*.so": false, + "**/*.dylib": false, + "**/*.pyc": false, + "**/*.db": false, + "**/*.sqlite": false + }, + "search.exclude": { + "**/bin": true, + "**/obj": true, + "**/node_modules": true, + "**/.vs": true, + "**/dist": true + }, + "files.watcherExclude": { + "**/bin/**": true, + "**/obj/**": true, + "**/.git/**": true, + "**/node_modules/**": true, + "**/.vs/**": true + } } \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 68dcef3a..6d8c3c25 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,7 @@ true + diff --git a/SnakeAid.Api/DI/DependencyInjection.cs b/SnakeAid.Api/DI/DependencyInjection.cs index 093f3551..ba3cc200 100644 --- a/SnakeAid.Api/DI/DependencyInjection.cs +++ b/SnakeAid.Api/DI/DependencyInjection.cs @@ -13,12 +13,121 @@ using System.Security.Claims; using System.Text; +using Serilog; +using Doppler.Extensions.Configuration; // Ensure this is available + namespace SnakeAid.Api.DI; public static class DependencyInjection { #region service he thong + #region AppSettings + public static WebApplicationBuilder AddAppsettings(this WebApplicationBuilder builder) + { + var dopplerAccessToken = LoadDopplerToken(); + + var configStrategy = new ConfigControl + { + UseDoppler = Environment.GetEnvironmentVariable("SNAKEAID_CONF_USE_DOPPLER") != "false", + UseAppSettings = Environment.GetEnvironmentVariable("SNAKEAID_CONF_USE_APPSETTINGS") != "false", + Priority = Environment.GetEnvironmentVariable("SNAKEAID_CONF_PRIORITY")?.ToUpper() ?? "DOPPLER" + }; + + ManageAppSettings(builder, configStrategy); + ManageDoppler(builder, configStrategy, dopplerAccessToken); + ValidateConfiguration(configStrategy); + + return builder; + } + + private static string? LoadDopplerToken() + { + var token = Environment.GetEnvironmentVariable("DOPPLER_TOKEN"); + if (!string.IsNullOrEmpty(token)) return token; + + token = Environment.GetEnvironmentVariable("DOPPLER_TOKEN", EnvironmentVariableTarget.User); + if (string.IsNullOrEmpty(token)) return null; + + Environment.SetEnvironmentVariable("DOPPLER_TOKEN", token, EnvironmentVariableTarget.Process); + + // Propagate other user env vars + foreach (System.Collections.DictionaryEntry userEnvVar in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)) + { + if (userEnvVar.Key is string key && userEnvVar.Value is string value && key != "DOPPLER_TOKEN") + { + Environment.SetEnvironmentVariable(key, value, EnvironmentVariableTarget.Process); + } + } + + return token; + } + + private static void ManageAppSettings(WebApplicationBuilder builder, ConfigControl strategy) + { + if (strategy.UseAppSettings) + { + Serilog.Log.Information("✅ Configuration Source: AppSettings (ENABLED)"); + return; + } + + var jsonSourcesToDiscard = builder.Configuration.Sources + .Where(source => source is Microsoft.Extensions.Configuration.Json.JsonConfigurationSource) + .ToList(); + + foreach (var source in jsonSourcesToDiscard) + { + builder.Configuration.Sources.Remove(source); + } + Serilog.Log.Information("đŸšĢ Configuration Source: AppSettings (DISABLED via Env Var)"); + } + + private static void ManageDoppler(WebApplicationBuilder builder, ConfigControl strategy, string? token) + { + if (!strategy.UseDoppler) + { + Serilog.Log.Information("đŸšĢ Configuration Source: Doppler (DISABLED via Env Var)"); + return; + } + + if (string.IsNullOrEmpty(token)) + { + Serilog.Log.Warning("âš ī¸ Doppler enabled but DOPPLER_TOKEN is missing."); + return; + } + + builder.Configuration.AddDoppler(token); + + if (strategy.Priority == "APPSETTINGS" && strategy.UseAppSettings) + { + var jsonSourcesToReorder = builder.Configuration.Sources + .Where(source => source is Microsoft.Extensions.Configuration.Json.JsonConfigurationSource) + .ToList(); + + foreach (var source in jsonSourcesToReorder) + { + builder.Configuration.Sources.Remove(source); + builder.Configuration.Sources.Add(source); + } + Serilog.Log.Information("✅ Configuration Source: Doppler (ENABLED, Priority: LOW)"); + } + else + { + Serilog.Log.Information("✅ Configuration Source: Doppler (ENABLED, Priority: HIGH)"); + } + } + + private static void ValidateConfiguration(ConfigControl strategy) + { + if (!strategy.UseAppSettings && !strategy.UseDoppler) + { + throw new Core.Exceptions.ConfigurationException("❌ Critical Error: All configuration sources (Doppler and AppSettings) are DISABLED."); + } + } + + #endregion + + #region Services public static IServiceCollection AddServices(this IServiceCollection services, IConfiguration configuration) { var cloudinarySection = configuration.GetSection("Cloudinary"); @@ -77,6 +186,8 @@ private static IAsyncPolicy GetCircuitBreakerPolicy() #endregion + #endregion + #region authen vs author public static IServiceCollection AddAuthenticateAuthor(this IServiceCollection services, diff --git a/SnakeAid.Api/Program.cs b/SnakeAid.Api/Program.cs index 914685d8..bf871b81 100644 --- a/SnakeAid.Api/Program.cs +++ b/SnakeAid.Api/Program.cs @@ -17,6 +17,7 @@ using SQLitePCL; using Swashbuckle.AspNetCore.SwaggerUI; using System.Text.Json.Serialization; +using Doppler.Extensions.Configuration; namespace SnakeAid.Api { @@ -33,6 +34,8 @@ public static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.AddAppsettings(); + Batteries_V2.Init(); var sqliteLogPath = Path.Combine(builder.Environment.ContentRootPath, "logs", "logs.db"); @@ -149,7 +152,7 @@ public static async Task Main(string[] args) }); builder.Services.AddControllers(); - + // Add Razor Pages for lightweight UI admin pages builder.Services.AddRazorPages(); diff --git a/SnakeAid.Api/Properties/launchSettings.json b/SnakeAid.Api/Properties/launchSettings.json index 88511877..b8acf112 100644 --- a/SnakeAid.Api/Properties/launchSettings.json +++ b/SnakeAid.Api/Properties/launchSettings.json @@ -13,28 +13,37 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "", "applicationUrl": "http://localhost:5009", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "SNAKEAID_CONF_USE_DOPPLER": "true", + "SNAKEAID_CONF_USE_APPSETTINGS": "true", + "SNAKEAID_CONF_PRIORITY": "DOPPLER" } }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "", "applicationUrl": "https://localhost:7026;http://localhost:5009", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "SNAKEAID_CONF_USE_DOPPLER": "true", + "SNAKEAID_CONF_USE_APPSETTINGS": "true", + "SNAKEAID_CONF_PRIORITY": "DOPPLER" } }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "swagger", + "launchUrl": "", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "SNAKEAID_CONF_USE_DOPPLER": "true", + "SNAKEAID_CONF_USE_APPSETTINGS": "true", + "SNAKEAID_CONF_PRIORITY": "DOPPLER" } } } diff --git a/SnakeAid.Api/SnakeAid.Api.csproj b/SnakeAid.Api/SnakeAid.Api.csproj index c6c4f4f6..63f99af7 100644 --- a/SnakeAid.Api/SnakeAid.Api.csproj +++ b/SnakeAid.Api/SnakeAid.Api.csproj @@ -9,6 +9,7 @@ + diff --git a/SnakeAid.Core/Exceptions/ApiException.cs b/SnakeAid.Core/Exceptions/ApiException.cs index 9c353ff5..9c9f8bea 100644 --- a/SnakeAid.Core/Exceptions/ApiException.cs +++ b/SnakeAid.Core/Exceptions/ApiException.cs @@ -111,4 +111,12 @@ public TooManyRequestsException(string reason, DateTime? retryAfter, int? limit Period = period; Endpoint = endpoint; } +} + +public class ConfigurationException : ApiException +{ + public ConfigurationException(string reason) + : base(reason, HttpStatusCode.InternalServerError) + { + } } \ No newline at end of file diff --git a/SnakeAid.Core/Settings/AppSettings.cs b/SnakeAid.Core/Settings/AppSettings.cs index 99b2df50..e0d78024 100644 --- a/SnakeAid.Core/Settings/AppSettings.cs +++ b/SnakeAid.Core/Settings/AppSettings.cs @@ -43,4 +43,11 @@ public class SnakeAISettings public bool SaveImage { get; set; } = false; public float Confidence { get; set; } = 0.25f; public int TimeoutSeconds { get; set; } = 30; -} \ No newline at end of file +} + +public class ConfigControl +{ + public bool UseDoppler { get; set; } = true; + public bool UseAppSettings { get; set; } = true; + public string Priority { get; set; } = "DOPPLER"; // "DOPPLER" or "APPSETTINGS" +} \ No newline at end of file From 73628791624ff00dfeeec43489415514aeb4050f Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Sun, 8 Feb 2026 20:16:07 +0700 Subject: [PATCH 8/9] refactor(config): simplify Doppler integration and remove complex configuration strategy --- SnakeAid.Api/DI/DependencyInjection.cs | 109 +++++-------------------- SnakeAid.Api/Program.cs | 2 +- SnakeAid.Core/Settings/AppSettings.cs | 6 -- 3 files changed, 22 insertions(+), 95 deletions(-) diff --git a/SnakeAid.Api/DI/DependencyInjection.cs b/SnakeAid.Api/DI/DependencyInjection.cs index ba3cc200..dd6045fa 100644 --- a/SnakeAid.Api/DI/DependencyInjection.cs +++ b/SnakeAid.Api/DI/DependencyInjection.cs @@ -22,109 +22,42 @@ public static class DependencyInjection { #region service he thong - #region AppSettings - public static WebApplicationBuilder AddAppsettings(this WebApplicationBuilder builder) - { - var dopplerAccessToken = LoadDopplerToken(); - - var configStrategy = new ConfigControl - { - UseDoppler = Environment.GetEnvironmentVariable("SNAKEAID_CONF_USE_DOPPLER") != "false", - UseAppSettings = Environment.GetEnvironmentVariable("SNAKEAID_CONF_USE_APPSETTINGS") != "false", - Priority = Environment.GetEnvironmentVariable("SNAKEAID_CONF_PRIORITY")?.ToUpper() ?? "DOPPLER" - }; - - ManageAppSettings(builder, configStrategy); - ManageDoppler(builder, configStrategy, dopplerAccessToken); - ValidateConfiguration(configStrategy); - - return builder; - } - - private static string? LoadDopplerToken() + #region Doppler + public static WebApplicationBuilder AddConfigurationFromDopplerCloud(this WebApplicationBuilder builder) { + // 1. Load Doppler Token (Process -> User) var token = Environment.GetEnvironmentVariable("DOPPLER_TOKEN"); - if (!string.IsNullOrEmpty(token)) return token; - - token = Environment.GetEnvironmentVariable("DOPPLER_TOKEN", EnvironmentVariableTarget.User); - if (string.IsNullOrEmpty(token)) return null; - Environment.SetEnvironmentVariable("DOPPLER_TOKEN", token, EnvironmentVariableTarget.Process); - - // Propagate other user env vars - foreach (System.Collections.DictionaryEntry userEnvVar in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)) + if (string.IsNullOrEmpty(token)) { - if (userEnvVar.Key is string key && userEnvVar.Value is string value && key != "DOPPLER_TOKEN") + token = Environment.GetEnvironmentVariable("DOPPLER_TOKEN", EnvironmentVariableTarget.User); + if (!string.IsNullOrEmpty(token)) { - Environment.SetEnvironmentVariable(key, value, EnvironmentVariableTarget.Process); + // Propagate User token and other User env vars to Process + Environment.SetEnvironmentVariable("DOPPLER_TOKEN", token, EnvironmentVariableTarget.Process); + foreach (System.Collections.DictionaryEntry userEnvVar in Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User)) + { + if (userEnvVar.Key is string key && userEnvVar.Value is string value && key != "DOPPLER_TOKEN") + { + Environment.SetEnvironmentVariable(key, value, EnvironmentVariableTarget.Process); + } + } } } - return token; - } - - private static void ManageAppSettings(WebApplicationBuilder builder, ConfigControl strategy) - { - if (strategy.UseAppSettings) - { - Serilog.Log.Information("✅ Configuration Source: AppSettings (ENABLED)"); - return; - } - - var jsonSourcesToDiscard = builder.Configuration.Sources - .Where(source => source is Microsoft.Extensions.Configuration.Json.JsonConfigurationSource) - .ToList(); - - foreach (var source in jsonSourcesToDiscard) - { - builder.Configuration.Sources.Remove(source); - } - Serilog.Log.Information("đŸšĢ Configuration Source: AppSettings (DISABLED via Env Var)"); - } - - private static void ManageDoppler(WebApplicationBuilder builder, ConfigControl strategy, string? token) - { - if (!strategy.UseDoppler) - { - Serilog.Log.Information("đŸšĢ Configuration Source: Doppler (DISABLED via Env Var)"); - return; - } - - if (string.IsNullOrEmpty(token)) - { - Serilog.Log.Warning("âš ī¸ Doppler enabled but DOPPLER_TOKEN is missing."); - return; - } - - builder.Configuration.AddDoppler(token); - - if (strategy.Priority == "APPSETTINGS" && strategy.UseAppSettings) + // 2. Register and Log results + if (!string.IsNullOrEmpty(token)) { - var jsonSourcesToReorder = builder.Configuration.Sources - .Where(source => source is Microsoft.Extensions.Configuration.Json.JsonConfigurationSource) - .ToList(); - - foreach (var source in jsonSourcesToReorder) - { - builder.Configuration.Sources.Remove(source); - builder.Configuration.Sources.Add(source); - } - Serilog.Log.Information("✅ Configuration Source: Doppler (ENABLED, Priority: LOW)"); + builder.Configuration.AddDoppler(token); + Serilog.Log.Information("🚀 Configuration: Doppler Cloud (ENABLED)"); } else { - Serilog.Log.Information("✅ Configuration Source: Doppler (ENABLED, Priority: HIGH)"); + Serilog.Log.Information("â„šī¸ Configuration: Local Settings (AppSettings.json & Env Vars)"); } - } - private static void ValidateConfiguration(ConfigControl strategy) - { - if (!strategy.UseAppSettings && !strategy.UseDoppler) - { - throw new Core.Exceptions.ConfigurationException("❌ Critical Error: All configuration sources (Doppler and AppSettings) are DISABLED."); - } + return builder; } - #endregion #region Services diff --git a/SnakeAid.Api/Program.cs b/SnakeAid.Api/Program.cs index bf871b81..d76b4fec 100644 --- a/SnakeAid.Api/Program.cs +++ b/SnakeAid.Api/Program.cs @@ -34,7 +34,7 @@ public static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); - builder.AddAppsettings(); + builder.AddConfigurationFromDopplerCloud(); Batteries_V2.Init(); diff --git a/SnakeAid.Core/Settings/AppSettings.cs b/SnakeAid.Core/Settings/AppSettings.cs index e0d78024..acb77e17 100644 --- a/SnakeAid.Core/Settings/AppSettings.cs +++ b/SnakeAid.Core/Settings/AppSettings.cs @@ -45,9 +45,3 @@ public class SnakeAISettings public int TimeoutSeconds { get; set; } = 30; } -public class ConfigControl -{ - public bool UseDoppler { get; set; } = true; - public bool UseAppSettings { get; set; } = true; - public string Priority { get; set; } = "DOPPLER"; // "DOPPLER" or "APPSETTINGS" -} \ No newline at end of file From ff92c4d9e42af30d22d95b772aaff175c966d361 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Sun, 8 Feb 2026 20:16:07 +0700 Subject: [PATCH 9/9] build: update launch configurations, environment variables, and docs subproject --- .vscode/launch.json | 8 ++++---- SnakeAid.Api/Properties/launchSettings.json | 17 +++-------------- SnakeAid.Docs | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9ad0e4a1..da579d08 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "cwd": "${workspaceFolder}/SnakeAid.Api", "stopAtEntry": false, "serverReadyAction": { - "pattern": "\\bNow listening on:\\s+(https?://\\S+)", + "pattern": "Now listening on:\\s+(https?://[^\\s]+)", "uriFormat": "%s", "action": "openExternally" }, @@ -33,7 +33,7 @@ "cwd": "${workspaceFolder}/SnakeAid.Api", "stopAtEntry": false, "serverReadyAction": { - "pattern": "\\bNow listening on:\\s+(https://.*:7026)", + "pattern": "Now listening on:\\s+(https://[^\\s]+:7026)", "uriFormat": "%s", "action": "openExternally" }, @@ -58,7 +58,7 @@ "cwd": "${workspaceFolder}/SnakeAid.Api", "stopAtEntry": false, "serverReadyAction": { - "pattern": "\\bNow listening on:\\s+(http://.*:5009)", + "pattern": "Now listening on:\\s+(http://[^\\s]+:5009)", "uriFormat": "%s", "action": "openExternally" }, @@ -83,7 +83,7 @@ "cwd": "${workspaceFolder}/SnakeAid.Api", "stopAtEntry": false, "serverReadyAction": { - "pattern": "\\bNow listening on:\\s+(https://.*:7026)", + "pattern": "Now listening on:\\s+(https://[^\\s]+:7026)", "uriFormat": "%s", "action": "openExternally" }, diff --git a/SnakeAid.Api/Properties/launchSettings.json b/SnakeAid.Api/Properties/launchSettings.json index b8acf112..97da9ec0 100644 --- a/SnakeAid.Api/Properties/launchSettings.json +++ b/SnakeAid.Api/Properties/launchSettings.json @@ -14,12 +14,8 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "", - "applicationUrl": "http://localhost:5009", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "SNAKEAID_CONF_USE_DOPPLER": "true", - "SNAKEAID_CONF_USE_APPSETTINGS": "true", - "SNAKEAID_CONF_PRIORITY": "DOPPLER" + "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { @@ -27,12 +23,8 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "", - "applicationUrl": "https://localhost:7026;http://localhost:5009", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "SNAKEAID_CONF_USE_DOPPLER": "true", - "SNAKEAID_CONF_USE_APPSETTINGS": "true", - "SNAKEAID_CONF_PRIORITY": "DOPPLER" + "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS Express": { @@ -40,10 +32,7 @@ "launchBrowser": true, "launchUrl": "", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "SNAKEAID_CONF_USE_DOPPLER": "true", - "SNAKEAID_CONF_USE_APPSETTINGS": "true", - "SNAKEAID_CONF_PRIORITY": "DOPPLER" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/SnakeAid.Docs b/SnakeAid.Docs index 0cc11121..7377ac31 160000 --- a/SnakeAid.Docs +++ b/SnakeAid.Docs @@ -1 +1 @@ -Subproject commit 0cc11121118d6417e32a180f42ac45340607b915 +Subproject commit 7377ac31de0541886c28e33772eaac8df30f1bc5