From 00e681f5d8466b34f45daf1b6522b10a8a589a81 Mon Sep 17 00:00:00 2001 From: the-khiem7 Date: Fri, 6 Feb 2026 01:15:53 +0700 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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/5] 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) } } }