Skip to main content

Cài đặt Jenkins Agent trên Proxmox VE

Step 1: Khởi tạo Container

  • Tải template: Trên PVE tải template Ubuntu 24.04image.png
  • Tại CT với template Ubuntu vừa tải về
    • CPU: 2 - 4 core
    • RAM: 2048 MB (Swap 512 MB)
    • Disk: 16GB (có thể mở rộng thêm nếu cần)
    • Network: sử dụng Static IP dạng 192.168.1.100/24 (nhớ điền Gateway 192.168.1.1)
  • Ok, Create and Start container vừa tạo.

Step 2: Thiết lập môi trường cho Agent

Đăng nhập vào Agent với quyền root và mật khẩu đã tạo.

  • Cài đặt các tools cần thiết:
    • apt update && apt upgrade -y // Update các package đã có trong hệ thống.
    • apt install openjdk-17-jdk git curl -y // Cài đặt JDK, Git, curl
  • Tạo user riêng cho jenkins
    • adduser jenkins // Tạo user tên là jenkins
  • Tạo thư mục làm việc và cấp quyền cho jenkins
    • mkdir -p /var/jenkins_home // Tạo thư mục jenkins_home
    • chown -R jenkins:jenkins /var/jenkins_home // Cấp quyền cho user jenkins
  • Chuyển SSH từ Socket sang Service (Mặc định SSH sẽ ở socket, chỉ hoạt động khi có yêu cầu. Để tránh gặp lỗi trong quá trình tự động thì nên chuyển sang Service)
    • systemctl stop ssh.socket
      systemctl disable ssh.socket
      systemctl enable ssh.service
      systemctl start ssh.service

Step 3: Thiết lập SSH Key

Tạo SSH Key để Jenkins Master có thể ra lệnh cho Agent mà không cần nhập mật khẩu mỗi khi cần build.

# Chuyển sang user jenkins
su - jenkins
# Tạo key (không đặt passphrase)
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa

# Tự cấp quyền cho chính mình (Authorized Keys)
cd ~/.ssh
cat id_rsa.pub >> authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

# Hiển thị Private Key để copy vào Jenkins Master
cat id_rsa 

Step 4: Tạo Node mới trên Jenkins Master

  • Tạo Credentials mới loại SSH Username with private key với Private Key vừa lấy ở bước trên.
    • Chú ý domain là global
  • Tạo Node. Jenkins Master > Manage Jenkins > Node > New Node
    • Remote root directory: /var/jenkins_home (thư mục làm việc đã tạo ở trên)
    • Label: label bất kỳ để dùng trong jenkinsfile.
    • Host: IP của Agent đã tạo ở Bước 1.
    • Host Key Verification Strategy: Chọn "Non-verifying Verification Strategy".
  • Ok tạo Node. Nếu có respond như hình thì tức là đã tạo và kết nối thành công. Nếu không thì kiểm tra log để biết vấn đề.image.png

Step 5: Update Jenkinsfile

  • Thay đổi chính so với Agent Windows:
    • bat > sh
    • del /F /Q > rm -f
def packageVersionFromFile = ""
def packageName = ""
def verdaccioPackageUrl = ""

pipeline {
    // Agent with Node.js & Git required
    agent { label "verdaccio-publisher" }

    // Ensure Node.js tool is available (configure in Jenkins Global Tool Config)
    tools {
        nodejs "NodeJS22"
    }

    environment {
        // Path to the directory containing package.json
        PROJECT_PATH = "Assets/Packages/DeviceDebugger"

        // Verdaccio registry URL
        VERDACCIO_REGISTRY_URL = "https://upm.thanhdv.com"
        // Jenkins Credential ID for Verdaccio auth (e.g., Secret Text or User/Pass with token)
        VERDACCIO_CREDENTIAL_ID = "THANHDV_VERDACCIO_AUTH"

        // Jenkins Credential ID for Git auth (MUST be "Username with password" type)
        // Username: your git username
        // Password: your git access token
        GIT_CREDENTIAL_ID = "THANHDV_GITHUB_JENKINS_CREDENTIAL"

        // Discord webhook create on Discord and add to Jenkins credential
        DISCORD_WEBHOOK = "DISCORD_VERDACCIO_WEBHOOK"

        // Release if commit has this key
        RELEASE_KEYWORD = "Release v"
    }
    // === End Configuration ===

    // Pipeline options
    options {
        timestamps()
        buildDiscarder(logRotator(numToKeepStr: "10"))
        timeout(time: 30, unit: "MINUTES")
        disableConcurrentBuilds()
    }

    stages {
        stage("Checkout") {
            steps {
                // Get source code
                checkout scm
                // git lfs pull // Uncomment if using Git LFS
            }
        }
        
        // stage("Validate Commit Message") {
        //     steps {
        //         dir(env.PROJECT_PATH) {
        //             script {
        //                 def isManualTrigger = false
        //                 currentBuild.getBuildCauses().each{ cause -> 
        //                     if (cause instanceof hudson.model.Cause$UserIdCause || cause.shortDescription.contains("Started by user ")) {
        //                         isManualTrigger = true
        //                     }
        //                 }

        //                 if (isManualTrigger) {
        //                     echo "Build triggered by User! Ignore Validate Commit Message."
        //                 } else {
        //                     def commitMessage = sh(script: 'git log -1 --pretty=%%B', returnStdout: true).trim()
        //                     echo "Checking commit message: ${commitMessage}"
        //                     if (!commitMessage.contains(RELEASE_KEYWORD)) {
        //                         echo "Build condition not met! Aborting pipeline..."
        //                         currentBuild.result = 'ABORTED'
        //                         return
        //                     }
        //                 }

        //                 echo "Commit message is valid for release."
        //             }
        //         }
        //     }
        // }

        stage("Prepare") {
            when {
                expression { return currentBuild.result != "ABORTED" }
            }

            steps {
                // Operate within the project directory
                dir(env.PROJECT_PATH) {
                    script {
                        // Read version directly from package.json
                        def pkg = readJSON file: "package.json" // Assumes package.json is at PROJECT_PATH root
                        if (!pkg || !pkg.version || !pkg.name) {
                             error "Could not read version from package.json"
                        }
                        
                        // Store the version in the script-level variable
                        packageVersionFromFile = pkg.version
                        echo "Package version: ${packageVersionFromFile}"

                        packageName = pkg.name
                        echo "Package name: ${packageName}"

                        verdaccioPackageUrl = "${env.VERDACCIO_REGISTRY_URL}/-/web/detail/${packageName}"
                        echo "Package URL: ${verdaccioPackageUrl}"

                        if (!packageVersionFromFile || !packageName || !verdaccioPackageUrl) {
                             error "Failed to set variable."
                        }

                        withCredentials([usernamePassword(credentialsId: env.VERDACCIO_CREDENTIAL_ID, usernameVariable: "NPM_USER", passwordVariable: "NPM_PASS")]) {
                            echo "Configuring npm for Verdaccio using Username/Password..."
                            // Encrypt username:password to Base64
                            def userPass = "${NPM_USER}:${NPM_PASS}"
                            def encodedAuth = java.util.Base64.getEncoder().encodeToString(userPass.getBytes("UTF-8"))

                            def registryUri = new URI(env.VERDACCIO_REGISTRY_URL)
                            def registryAuthority = registryUri.getAuthority()
                            def registryHostPath = "//${registryAuthority}/"

                            // Configure .npmrc for Verdaccio registry & auth token
                            sh "echo ${registryHostPath}:_auth=\"${encodedAuth}\" > .npmrc"
                            sh "echo registry=${env.VERDACCIO_REGISTRY_URL} >> .npmrc"
                            echo ".npmrc configured."
                        }

                        // Install dependencies (might run prepublish scripts)
                        echo "Running npm install..."
                        sh "npm install"
                    }
                }
            }
        }

        stage("Publish") {
            when {
                expression { return currentBuild.result != "ABORTED" }
            }

            steps {
                // Operate within the project directory
                dir(env.PROJECT_PATH) {
                    script {                        
                        // Use the version read from package.json
                        echo "Publishing package version ${packageVersionFromFile} to ${env.VERDACCIO_REGISTRY_URL}"
                        try {
                            // Publish using npm (reads package.json for name/version, uses .npmrc for auth/registry)
                            sh "npm publish --registry ${env.VERDACCIO_REGISTRY_URL}"
                            echo "Package version ${packageVersionFromFile} published successfully!"
                        } catch (err) {
                            echo "ERROR: Failed to publish package!"
                            error "Publish failed: ${err.getMessage()}"
                        }
                    }
                }
            }
        }

        // // (Optional) Tag commit with the existing version from package.json
        // stage("Tag Existing Version") {
        //     // Only run on overall success so far
        //     when { expression { currentBuild.result == null || currentBuild.result == "SUCCESS" } }
        //     steps {
        //         // Operate within the project directory
        //         dir(env.PROJECT_PATH) {
        //             script {
        //                  echo "Tagging commit with existing version v${packageVersionFromFile}..."

        //                  // Inject Git credentials (Username = Git user, Password = Access Token)
        //                 withCredentials([usernamePassword(credentialsId: env.GIT_CREDENTIAL_ID, usernameVariable: "GIT_USERNAME", passwordVariable: "GIT_ACCESS_TOKEN")]) {

        //                     // Configure Git user (may not be needed if just tagging)
        //                     sh "git config user.email \"vanthanh1998@gmail.com\"" // EDIT Or use a specific user
        //                     sh "git config user.name \"ThanhDVs Jenkins\""

        //                     // NO commit needed here as we didn"t change package.json version via npm version

        //                     // Create annotated tag using the version from package.json
        //                     sh "git tag -a v${packageVersionFromFile} -m \"Release v${packageVersionFromFile}\"" // Use the read version

        //                     // Push tag using HTTPS URL with embedded token
        //                     def repoUrl = scm.userRemoteConfigs[0].url
        //                     if (!repoUrl || !repoUrl.startsWith("https://")) {
        //                         error "Could not determine HTTPS repository URL from SCM configuration."
        //                     }
        //                     def repoUrlClean = repoUrl.replaceAll(/https?:\/\/[^\/]+@/, "https://")
        //                     def pushUrl = repoUrlClean.replaceFirst("https://", "https://${GIT_USERNAME}:${GIT_ACCESS_TOKEN}@")

        //                     // Push only the tag
        //                     sh "git push ${pushUrl} refs/tags/v${packageVersionFromFile}:refs/tags/v${packageVersionFromFile}"

        //                     echo "Version tag v${packageVersionFromFile} pushed successfully."
        //                 }
        //             }
        //         }
        //     }
        // }
    }

    // Post-build actions
    post {
        // Always run cleanup
        always {
            echo "Build finished. Cleaning up..."
            // Clean up sensitive .npmrc file
            dir(env.PROJECT_PATH) {
                script {
                    try {
                        sh "rm -f .npmrc"
                    } catch (err) {
                        echo "Could not delete .npmrc (maybe it doesn't exist): ${err.getMessage()}"
                    }
                }
            }
        }
        
        // On success
        success {
            echo "Pipeline successful!"

            // Save artifacts if need
            // archiveArtifacts artifacts: '**/*.tgz', allowEmptyArchive: true
            
            echo "Cleaning up workspace..."
            deleteDir()

            echo "Sending success notification to Discord..."
            script {
                // Send
                withCredentials([string(credentialsId: "${env.DISCORD_WEBHOOK}", variable: 'DISCORD_WEBHOOK_URL_SECRET')]) {
                    discordSend(
                        webhookURL: DISCORD_WEBHOOK_URL_SECRET,
                        title: "✅ Success: ${env.JOB_NAME}", 
                        description: "Job `${env.JOB_NAME}` build #${env.BUILD_NUMBER} published package `${packageName}@${packageVersionFromFile}` successfully.\nBuild Log: ${env.BUILD_URL}.\nPackage URL: ${verdaccioPackageUrl}",
                        result: "SUCCESS",
                        link: env.BUILD_URL,
                        footer: "Jenkins Build Notification"
                    )
                }
            }
        }
        
        // On failure
        failure {
            echo "Pipeline failed!"

            echo "Sending failure notification to Discord..."
            script {    
                // Send
                withCredentials([string(credentialsId: "${env.DISCORD_WEBHOOK}", variable: 'DISCORD_WEBHOOK_URL_SECRET')]) {
                    discordSend(
                        webhookURL: DISCORD_WEBHOOK_URL_SECRET,
                        title: "❌ Failure: ${env.JOB_NAME}",
                        description: "Job `${env.JOB_NAME}` build #${env.BUILD_NUMBER} failed to publish package `${packageName}`.\nBuild Log: ${env.BUILD_URL}",
                        result: "FAILURE",
                        link: env.BUILD_URL,
                        footer: "Jenkins Build Notification"
                    )
                }
            }
        }

        aborted {
            echo "Pipeline aborted!"
            echo "Sending abort notification to Discord..."
            script {    
                // Send
                withCredentials([string(credentialsId: "${env.DISCORD_WEBHOOK}", variable: 'DISCORD_WEBHOOK_URL_SECRET')]) {
                    discordSend(
                        webhookURL: DISCORD_WEBHOOK_URL_SECRET,
                        title: "ℹ️ Aborted: ${env.JOB_NAME}",
                        description: "Build #${env.BUILD_NUMBER} for job `${env.JOB_NAME}` was aborted.",
                        result: "ABORTED",
                        link: env.BUILD_URL,
                        footer: "Jenkins Build Notification"
                    )
                }
            }
        }

        unstable {
            echo "Pipeline unstable!"
            
            echo "Sending unstable notification to Discord..."
            script {    
                withCredentials([string(credentialsId: "${env.DISCORD_WEBHOOK}", variable: 'DISCORD_WEBHOOK_URL_SECRET')]) {
                    discordSend(
                        webhookURL: DISCORD_WEBHOOK_URL_SECRET,
                        title: "⚠️ Unstable: ${env.JOB_NAME}",
                        description: "Job `${env.JOB_NAME}` build #${env.BUILD_NUMBER} finished with unstable status during processing of package `${packageName}`.\nBuild Log: ${env.BUILD_URL}",
                        result: "UNSTABLE",
                        link: env.BUILD_URL,
                        footer: "Jenkins Build Notification"
                    )
                }
            }
        }
    }
}