0

Hướng dẫn Setup Jenkins cho hệ thống Monolith - Bài 17: Quản lý versioning tự động cho bản build dựa trên Git Commit Hash hoặc Semantic Versioning

Ở các bài học trước (từ Bài 10 đến Bài 16), chúng ta đã tạm đặt tên cho phiên bản gói cài đặt là BUILD_VERSION = "v1.0.${env.BUILD_NUMBER}". Cách đặt tên này dùng để học tập thì rất gọn, nhưng khi bước vào vận hành hệ thống Monolith thực chiến trên Production, nó lập tức bộc lộ điểm yếu chí mạng: Nhìn vào con số v1.0.45, không ai biết bản build này đang chứa đoạn code nào trên Git, được tạo ra từ nhánh nào, hay nếu bạn xóa Job Jenkins đi tạo lại khiến BUILD_NUMBER reset về 1 thì toàn bộ lịch sử phiên bản sẽ bị trùng lặp hỗn loạn!

Bài học hôm nay sẽ hướng dẫn bạn thiết lập hệ thống đánh số phiên bản (Automated Versioning) chuẩn chỉnh trong Jenkins: kết hợp giữa Semantic Versioning (SemVer), Git Commit Hash và kỹ thuật Nạp thông tin phiên bản (Build Metadata Injection) thẳng vào bên trong ứng dụng Monolith.

1. Tại sao hệ thống Monolith cần một chuẩn Versioning định danh duy nhất?

Trong kiến trúc Monolith, hàng chục lập trình viên cùng đẩy code vào một kho lưu trữ chung mỗi ngày. Việc chuẩn hóa Versioning giải quyết 3 bài toán sống còn:

  1. Truy vết tức thì khi xảy ra sự cố (Instant Traceability): Khi hệ thống trên Production báo lỗi lúc 2 giờ sáng, nhìn vào chuỗi version v2.4.1-84-ga1b2c3d, bạn chỉ mất đúng 3 giây để gõ git checkout a1b2c3d và tái hiện lại chính xác 100% trạng thái mã nguồn đang chạy trên server lúc đó.

  2. Tránh xung đột bộ nhớ đệm (Cache Invalidation & Immutable Artifacts): Không bao giờ dùng các nhãn chung chung như monolith-latest.tar.gz hay Docker tag :latest. Mỗi bản build phải là một thực thể bất biến (Immutable) với tên gọi duy nhất để hệ thống Rollback (Bài 27) không bao giờ lấy nhầm bản cũ trong cache.

  3. Phân biệt rõ môi trường phát triển: Nhìn vào hậu tố của version (ví dụ -dev.102, -rc.1 hay bản chính thức v2.4.1), đội ngũ QA/QC và DevOps biết ngay gói Artifact này được phép đi tới môi trường nào.

2. Ba chiến lược Versioning phổ biến nhất trên Jenkins Pipeline

Tùy thuộc vào quy trình Git (GitFlow hay Trunk-Based Development) của đội ngũ phát triển Monolith, bạn có thể áp dụng một trong ba công thức sau:

Chiến lược phiên bản Định dạng mẫu (Pattern) Khi nào nên sử dụng? Cách lấy giá trị trong Git / Jenkins
1. Git Short Hash + Build Number v2026.10.29-45-a1b2c3d Dùng cho các chu trình triển khai liên tục hàng ngày lên môi trường Dev hoặc Staging. Kết hợp ngày tháng hiện tại (date +%Y.%m.%d), biến môi trường ${env.BUILD_NUMBER} trong Jenkins và 7 ký tự đầu của mã băm Git (git rev-parse --short HEAD).
2. Semantic Versioning (SemVer chuẩn) v2.1.0 (MAJOR.MINOR.PATCH) Dùng cho các bản phát hành chính thức (Release) đẩy lên môi trường Production. Đọc trực tiếp từ Git Tag chính thức được gắn trên nhánh main / master (git describe --tags --abbrev=0).
3. Git Describe (Kết hợp thông minh) v2.1.0-14-ga1b2c3d Khuyên dùng cho Monolith / Microservices: Vừa giữ được mốc SemVer gốc, vừa tự động đếm số commit phát sinh thêm. Sử dụng lệnh git describe --tags --always (Cách đọc: Đang đứng ở mốc tag v2.1.0, tiến thêm 14 commits, mã commit hiện tại là a1b2c3d).

3. Kỹ thuật tạo chuỗi Version tự động theo nhánh Git trong Jenkinsfile

Thay vì gán cứng một công thức ở khối environment tĩnh, chúng ta sẽ viết một hàm Groovy động ở đầu Pipeline để Jenkins tự động tính toán chuỗi BUILD_VERSION dựa trên nhánh Git hiện tại:

  • Nếu build từ nhánh Production (main / master): Lấy theo chuẩn git describe --tags hoặc SemVer sạch (ví dụ: v1.4.2-a1b2c3d).

  • Nếu build từ nhánh Staging / Develop: Tự động gắn thêm hậu tố -staging hoặc -dev kèm số thứ tự build của Jenkins (ví dụ: v1.4.2-dev.58-a1b2c3d).

Groovy

// Đoạn script tính toán Version động trong Jenkinsfile
script {
    // 1. Lấy 7 ký tự đầu của Git Commit Hash hiện tại
    def gitShortHash = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
    
    // 2. Lấy Tag gần nhất theo chuẩn SemVer (Nếu repo chưa có tag nào thì mặc định là v0.1.0)
    def latestTag = sh(script: 'git describe --tags --abbrev=0 2>/dev/null || echo "v0.1.0"', returnStdout: true).trim()
    
    // 3. Lấy tên nhánh hiện tại và làm sạch ký tự đặc biệt (ví dụ: feature/login -> feature-login)
    def branchName = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim().replaceAll('[^a-zA-Z0-9.-]', '-')

    // 4. Quyết định định dạng BUILD_VERSION theo nhánh
    if (branchName == 'main' || branchName == 'master') {
        env.BUILD_VERSION = "${latestTag}-${gitShortHash}"
    } else {
        env.BUILD_VERSION = "${latestTag}-${branchName}.${env.BUILD_NUMBER}-${gitShortHash}"
    }
    
    // 5. Đổi tên hiển thị của bản Build trên giao diện Jenkins để dễ nhìn lịch sử
    currentBuild.displayName = "#${env.BUILD_NUMBER} - ${env.BUILD_VERSION}"
    currentBuild.description = "Branch: ${branchName} | Commit: ${gitShortHash}"
}

Lưu ý quan trọng khi dùng git describe trên Jenkins: Mặc định, khi Jenkins kéo code ở bước checkout scm, đôi khi nó cấu hình chế độ Shallow Clone hoặc Không kéo Git Tags để tiết kiệm băng thông, khiến lệnh git describe --tags không tìm thấy tag. Để khắc phục triệt để, bạn hãy chạy lệnh git fetch --tags --force ngay trước khi tính toán version.

4. Kỹ thuật thực chiến: "Bơm" (Inject) thông tin Version vào bên trong mã nguồn Monolith

Đặt tên cho file nén .tar.gz là chưa đủ! Khi ứng dụng Monolith đã được giải nén và chạy trên Production, làm sao để lập trình viên gọi một API /api/health (hoặc /version) là biết ngay server đang chạy bản build nào?

Chúng ta cần kỹ thuật Build Metadata Injection ngay trong lúc Jenkins đang biên dịch:

A. Đối với PHP / Laravel hoặc Node.js Monolith (Tạo file version.json / .env)

Ngay trước bước đóng gói Artifact, Jenkins tự động sinh ra một tệp version.json đặt ở thư mục gốc (hoặc ghi vào file config). API /health của Laravel/Node.js chỉ việc đọc tệp này trả về cho client:

Bash

cat <<EOF > version.json
{
  "app_name": "${APP_NAME}",
  "version": "${BUILD_VERSION}",
  "git_commit": "${GIT_COMMIT_HASH}",
  "build_number": "${BUILD_NUMBER}",
  "built_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
}
EOF

B. Đối với Golang Monolith (Sử dụng cờ -ldflags lúc biên dịch)

Trong Golang, bạn không cần tạo file phụ bên ngoài mà có thể truyền thẳng chuỗi version từ Jenkins vào biến toàn cục bên trong file thực thi (binary) thông qua cờ -ldflags "-X":

Bash

# Giả sử trong package main của Go có khai báo biến: var AppVersion = "dev"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
  -ldflags="-s -w -X 'main.AppVersion=${BUILD_VERSION}' -X 'main.GitCommit=${GIT_COMMIT_HASH}'" \
  -o bin/monolith-app ./cmd/main.go

C. Đối với Java Spring Boot Monolith (Maven Versions Plugin)

Sử dụng versions-maven-plugin để cập nhật trực tiếp thẻ <version> trong file pom.xml trước khi đóng gói ra file .jar:

Bash

mvn versions:set -DnewVersion=${BUILD_VERSION} -DgenerateBackupPoms=false
mvn clean package -DskipTests

5. Hoàn thiện Jenkinsfile với hệ thống Quản lý Versioning & Git Tagging tự động

Dưới đây là tệp Jenkinsfile hoàn chỉnh kết hợp cả việc sinh Version thông minh, hiển thị trực quan lên cột Build History của Jenkins, nhúng version.json vào mã nguồn và tự động đánh Git Tag đẩy ngược lên GitHub/GitLab khi build thành công trên nhánh main:

Groovy

pipeline {
    agent any

    options {
        buildDiscarder(logRotator(numToKeepStr: '15', artifactNumToKeepStr: '5'))
        timeout(time: 30, unit: 'MINUTES')
        timestamps()
    }

    environment {
        APP_NAME = "monolith-core-system"
    }

    stages {
        stage('1. Checkout & Generate Version') {
            steps {
                checkout scm
                
                echo "=== Đang đồng bộ Git Tags và tính toán phiên bản bản dựng (Build Version) ==="
                script {
                    // Đảm bảo kéo đầy đủ toàn bộ Git Tags từ Remote về Agent
                    sh 'git fetch --tags --force'

                    env.GIT_SHORT_HASH = sh(script: 'git rev-parse --short=7 HEAD', returnStdout: true).trim()
                    env.LATEST_SEMVER_TAG = sh(script: 'git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0"', returnStdout: true).trim()
                    env.CLEAN_BRANCH = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim().replaceAll('[^a-zA-Z0-9.-]', '-')

                    if (env.CLEAN_BRANCH == 'main' || env.CLEAN_BRANCH == 'master') {
                        // Định dạng chuẩn cho Production: v1.2.0-b45-a1b2c3d
                        env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-b${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
                    } else {
                        // Định dạng cho nhánh Dev/Staging: v1.2.0-develop.45-a1b2c3d
                        env.BUILD_VERSION = "${env.LATEST_SEMVER_TAG}-${env.CLEAN_BRANCH}.${env.BUILD_NUMBER}-${env.GIT_SHORT_HASH}"
                    }

                    // Cập nhật tên hiển thị ngay trên cột Build History của giao diện Jenkins
                    currentBuild.displayName = "${env.BUILD_VERSION}"
                    currentBuild.description = "Commit: ${env.GIT_SHORT_HASH} | Branch: ${env.CLEAN_BRANCH}"

                    echo "✅ Phiên bản được ấn định cho bản build này: ${env.BUILD_VERSION}"
                }
            }
        }

        stage('2. Install Dependencies') {
            steps {
                sh 'composer install --no-interaction --prefer-dist'
            }
        }

        stage('3. Run Unit Tests & Coverage') {
            environment {
                APP_ENV = "testing"
                DB_CONNECTION = "sqlite"
                DB_DATABASE = ":memory:"
                XDEBUG_MODE = "coverage"
            }
            steps {
                sh '''
                    mkdir -p reports
                    ./vendor/bin/phpunit \
                        --log-junit reports/junit.xml \
                        --coverage-clover reports/coverage-clover.xml
                '''
            }
            post {
                always {
                    junit testResults: 'reports/junit.xml', allowEmptyResults: false
                }
            }
        }

        stage('4. Security & SonarQube Gate') {
            steps {
                script {
                    sh 'trivy fs --scanners vuln,secret --severity CRITICAL --exit-code 1 .'
                    
                    def scannerHome = tool 'SonarScanner-CLI'
                    withSonarQubeEnv('SonarQube-Main-Server') {
                        // Truyền đúng BUILD_VERSION động vào SonarQube để theo dõi chất lượng theo từng phiên bản
                        sh "${scannerHome}/bin/sonar-scanner -Dsonar.projectVersion=${env.BUILD_VERSION}"
                    }
                }
                timeout(time: 5, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('5. Inject Metadata & Package Artifact') {
            steps {
                echo "=== Nhúng Metadata phiên bản và đóng gói Artifact ${env.BUILD_VERSION} ==="
                sh '''
                    # 1. Dọn dẹp thư viện dev
                    composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev

                    # 2. Tạo file version.json đi kèm vào trong gói code Monolith
                    cat <<EOF > version.json
                    {
                      "service": "${APP_NAME}",
                      "version": "${BUILD_VERSION}",
                      "commit": "${GIT_SHORT_HASH}",
                      "branch": "${CLEAN_BRANCH}",
                      "build_time": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
                    }
                    EOF

                    # 3. Đóng gói với tên file mang định danh Version duy nhất
                    tar --exclude='.git' --exclude='tests' --exclude='reports' -czf ${APP_NAME}-${BUILD_VERSION}.tar.gz .
                '''
            }
        }

        // STAGE TÙY CHỌN: Tự động đánh dấu Git Tag đẩy ngược lên kho chứa khi nhánh main build thành công
        stage('6. Push Git Tag (Release Branch Only)') {
            when {
                anyOf {
                    branch 'main'
                    branch 'master'
                }
            }
            steps {
                echo "=== Đánh dấu mốc phát hành (Git Tag) lên Git Repository ==="
                // Sử dụng SSH Agent với khóa đã cấu hình quyền Write ở Bài 5
                sshagent(credentials: ['jenkins-git-ssh-key']) {
                    sh '''
                        git config user.name "Jenkins CI Bot"
                        git config user.email "jenkins-bot@yourcompany.com"
                        
                        # Tạo tag có chú thích và đẩy lên Remote
                        git tag -a "release-${BUILD_VERSION}" -m "Automated release build ${BUILD_VERSION} by Jenkins"
                        git push origin "release-${BUILD_VERSION}"
                    '''
                }
            }
        }
    }

    post {
        success {
            archiveArtifacts artifacts: "${APP_NAME}-${env.BUILD_VERSION}.tar.gz, version.json", fingerprint: true
        }
        always {
            sh 'rm -rf reports/ version.json .env'
        }
    }
}

6. Trải nghiệm sự khác biệt trên giao diện Jenkins

Ngay sau khi áp dụng Jenkinsfile của Bài 17, bạn nhìn vào cột Build History ở góc trái màn hình Jenkins sẽ thấy một sự lột xác hoàn toàn:

  • Thay vì những con số vô hồn #14, #15, #16 như trước, giờ đây danh sách hiển thị rõ ràng:

    • v1.2.0-b16-8f9a1c2 (Commit: 8f9a1c2 | Branch: main)

    • v1.2.0-develop.15-3d4e5f6 (Commit: 3d4e5f6 | Branch: develop)

  • Trên giao diện SonarQube, biểu đồ lịch sử chất lượng code cũng tự động phân tách theo từng mốc version v1.2.0-b16-8f9a1c2, giúp bạn biết chính xác bản phát hành nào đã làm tăng hay giảm nợ kỹ thuật của dự án.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.