{"id":39,"date":"2026-04-09T10:00:00","date_gmt":"2026-04-09T02:00:00","guid":{"rendered":"http:\/\/localhost:8888\/?p=39"},"modified":"2026-07-09T16:17:45","modified_gmt":"2026-07-09T08:17:45","slug":"jenkins%e6%b5%8b%e8%af%95%e6%b5%81%e6%b0%b4%e7%ba%bf%e6%90%ad%e5%bb%ba","status":"publish","type":"post","link":"https:\/\/blog.runnergo.cn\/?p=39","title":{"rendered":"Jenkins\u6d4b\u8bd5\u6d41\u6c34\u7ebf\u642d\u5efa &#8211; CI\/CD\u4e2d\u7684\u81ea\u52a8\u5316\u6d4b\u8bd5\u96c6\u6210\u5b9e\u6218"},"content":{"rendered":"<h1>Jenkins\u6d4b\u8bd5\u6d41\u6c34\u7ebf\u642d\u5efa - CI\/CD\u4e2d\u7684\u81ea\u52a8\u5316\u6d4b\u8bd5\u96c6\u6210\u5b9e\u6218<\/h1>\n\n&gt; \u4f5c\u8005\uff1aDevOps\u8001\u5175\n---\n\n<h2>\u524d\u8a00<\/h2>\n\n\u5927\u5bb6\u597d\uff0c\u6211\u662fDevOps\u8001\u5175\u3002\u505aCI\/CD\u5b9e\u8df510\u5e74\uff0c\u6d4b\u8bd5\u96c6\u6210\u662f\u6700\u5bb9\u6613\u88ab\u5ffd\u89c6\u7684\u73af\u8282\u3002\u4eca\u5929\u5206\u4eabJenkins\u6d4b\u8bd5\u6d41\u6c34\u7ebf\u7684\u642d\u5efa\u7ecf\u9a8c\u3002\n\n---\n\n<h2>\u4e00\u3001\u6d4b\u8bd5\u5728CI\/CD\u4e2d\u7684\u4f4d\u7f6e<\/h2>\n\n<h3>\u4f20\u7edfCI\u6d41\u7a0b<\/h3>\n\n<code>`<\/code>\n\u4ee3\u7801\u63d0\u4ea4 -&gt; \u7f16\u8bd1 -&gt; \u90e8\u7f72 -&gt; \u6d4b\u8bd5\uff08\u624b\u52a8\uff09\n<code>`<\/code>\n\n\u6d4b\u8bd5\u5f80\u5f80\u5728\u90e8\u7f72\u540e\u624b\u52a8\u6267\u884c\uff0c\u53cd\u9988\u6162\u3001\u6548\u7387\u4f4e\u3002\n\n<h3>\u7406\u60f3CI\/CD\u6d41\u7a0b<\/h3>\n\n<code>`<\/code>\n\u4ee3\u7801\u63d0\u4ea4 -&gt; \u5355\u5143\u6d4b\u8bd5 -&gt; \u6784\u5efa -&gt; \u96c6\u6210\u6d4b\u8bd5 -&gt; \u90e8\u7f72 -&gt; \u81ea\u52a8\u5316\u6d4b\u8bd5 -&gt; \u76d1\u63a7\n<code>`<\/code>\n\n\u6d4b\u8bd5\u5e94\u8be5\uff1a\n<ul><li><strong>\u5de6\u79fb<\/strong>\uff1a\u5c3d\u65e9\u6267\u884c\uff0c\u5feb\u901f\u53cd\u9988<\/li>\n<li><strong>\u81ea\u52a8\u5316<\/strong>\uff1a\u51cf\u5c11\u4eba\u5de5\u5e72\u9884<\/li>\n<li><strong>\u96c6\u6210<\/strong>\uff1a\u5d4c\u5165\u6d41\u6c34\u7ebf\u5404\u73af\u8282<\/li>\n<\/ul>\n---\n\n<h2>\u4e8c\u3001Jenkins Pipeline\u57fa\u7840<\/h2>\n\n<h3>Pipeline\u6982\u5ff5<\/h3>\n\nJenkins Pipeline\u662f\u4e00\u7cfb\u5217\u4efb\u52a1\u7684\u7ec4\u5408\uff1a\n<ul><li>Stage\uff1a\u9636\u6bb5\u5212\u5206<\/li>\n<li>Step\uff1a\u5177\u4f53\u4efb\u52a1<\/li>\n<li>Parallel\uff1a\u5e76\u884c\u6267\u884c<\/li>\n<\/ul>\n<h3>Pipeline\u8bed\u6cd5<\/h3>\n\n<code>`<\/code>groovy\npipeline {\n    agent any\n\n    stages {\n        stage('\u6784\u5efa') {\n            steps {\n                sh 'mvn clean package'\n            }\n        }\n\n        stage('\u6d4b\u8bd5') {\n            steps {\n                sh 'mvn test'\n            }\n        }\n\n        stage('\u90e8\u7f72') {\n            steps {\n                sh 'deploy.sh'\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n---\n\n<h2>\u4e09\u3001\u6d4b\u8bd5\u6d41\u6c34\u7ebf\u642d\u5efa<\/h2>\n\n<h3>\u5355\u5143\u6d4b\u8bd5\u96c6\u6210<\/h3>\n\n<code>`<\/code>groovy\npipeline {\n    agent any\n\n    stages {\n        stage('\u5355\u5143\u6d4b\u8bd5') {\n            steps {\n                sh '''\n                    # Python\u5355\u5143\u6d4b\u8bd5\n                    pytest tests\/unit -v --cov=src --cov-report=xml\n                '''\n            }\n            post {\n                always {\n                    \/\/ \u53d1\u5e03\u6d4b\u8bd5\u62a5\u544a\n                    publishHTML([\n                        reportDir: 'reports',\n                        reportFiles: 'coverage.html',\n                        reportName: '\u5355\u5143\u6d4b\u8bd5\u8986\u76d6\u7387'\n                    ])\n                }\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n<h3>\u96c6\u6210\u6d4b\u8bd5\u96c6\u6210<\/h3>\n\n<code>`<\/code>groovy\nstage('\u96c6\u6210\u6d4b\u8bd5') {\n    steps {\n        sh '''\n            # \u542f\u52a8\u6d4b\u8bd5\u73af\u5883\n            docker-compose -f docker-compose.test.yml up -d\n\n            # \u7b49\u5f85\u670d\u52a1\u5c31\u7eea\n            sleep 30\n\n            # \u6267\u884c\u96c6\u6210\u6d4b\u8bd5\n            pytest tests\/integration -v\n\n            # \u6e05\u7406\u73af\u5883\n            docker-compose -f docker-compose.test.yml down\n        '''\n    }\n}\n<code>`<\/code>\n\n<h3>API\u6d4b\u8bd5\u96c6\u6210<\/h3>\n\n<code>`<\/code>groovy\nstage('API\u6d4b\u8bd5') {\n    steps {\n        \/\/ \u4f7f\u7528Newman\u8fd0\u884cPostman\u6d4b\u8bd5\n        sh '''\n            npm install -g newman\n            newman run tests\/collection.json \n                -e tests\/environment.json \n                -r cli,html \n                --reporter-html-export reports\/api-report.html\n        '''\n    }\n    post {\n        always {\n            publishHTML([\n                reportDir: 'reports',\n                reportFiles: 'api-report.html',\n                reportName: 'API\u6d4b\u8bd5\u62a5\u544a'\n            ])\n        }\n    }\n}\n<code>`<\/code>\n\n<h3>RunnerGo\u96c6\u6210<\/h3>\n\nRunnerGo\u53ef\u4ee5\u901a\u8fc7API\u89e6\u53d1\u6d4b\u8bd5\uff1a\n\n<code>`<\/code>groovy\nstage('RunnerGo\u81ea\u52a8\u5316\u6d4b\u8bd5') {\n    steps {\n        sh '''\n            # \u8c03\u7528RunnerGo API\u89e6\u53d1\u6d4b\u8bd5\n            curl -X POST \n                -H \"Authorization: Bearer ${RUNNERGO_TOKEN}\" \n                -H \"Content-Type: application\/json\" \n                -d '{\"plan_id\": \"${PLAN_ID}\", \"scene_ids\": []}' \n                ${RUNNERGO_URL}\/api\/run\n        '''\n\n        \/\/ \u7b49\u5f85\u6d4b\u8bd5\u5b8c\u6210\u5e76\u83b7\u53d6\u62a5\u544a\n        script {\n            def reportId = ''\n            def status = 'running'\n\n            while (status == 'running') {\n                sleep(time: 10, unit: 'SECONDS')\n\n                def response = sh(\n                    script: 'curl -s ${RUNNERGO_URL}\/api\/status',\n                    returnStdout: true\n                ).trim()\n\n                def json = new groovy.json.JsonSlurper().parseText(response)\n                status = json.status\n                reportId = json.report_id\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n---\n\n<h2>\u56db\u3001\u6d4b\u8bd5\u73af\u5883\u7ba1\u7406<\/h2>\n\n<h3>Docker\u73af\u5883<\/h3>\n\n\u4f7f\u7528Docker\u521b\u5efa\u9694\u79bb\u6d4b\u8bd5\u73af\u5883\uff1a\n\n<code>`<\/code>yaml\n<h1>docker-compose.test.yml<\/h1>\nversion: '3'\nservices:\n  app:\n    build: .\n    ports:\n      - \"8080:8080\"\n    environment:\n      - DB_HOST=test-db\n      - DB_PORT=3306\n\n  test-db:\n    image: mysql:8\n    environment:\n      - MYSQL_ROOT_PASSWORD=test\n      - MYSQL_DATABASE=test_db\n\n  redis:\n    image: redis:alpine\n<code>`<\/code>\n\n<h3>\u73af\u5883\u53d8\u91cf\u7ba1\u7406<\/h3>\n\n<code>`<\/code>groovy\npipeline {\n    agent any\n\n    environment {\n        TEST_ENV = 'staging'\n        DB_HOST = 'test-db'\n        API_URL = 'http:\/\/test-api.example.com'\n    }\n\n    stages {\n        stage('\u6d4b\u8bd5') {\n            steps {\n                sh 'pytest tests'\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n---\n\n<h2>\u4e94\u3001\u6d4b\u8bd5\u62a5\u544a\u96c6\u6210<\/h2>\n\n<h3>JUnit\u62a5\u544a<\/h3>\n\n<code>`<\/code>groovy\nstage('\u6d4b\u8bd5') {\n    steps {\n        sh 'pytest --junitxml=reports\/junit.xml'\n    }\n    post {\n        always {\n            junit 'reports\/*.xml'\n        }\n    }\n}\n<code>`<\/code>\n\n<h3>Allure\u62a5\u544a<\/h3>\n\n<code>`<\/code>groovy\nstage('\u6d4b\u8bd5') {\n    steps {\n        sh 'pytest --alluredir=allure-results'\n    }\n    post {\n        always {\n            allure([\n                includeProperties: false,\n                jdk: '',\n                results: [[path: 'allure-results']],\n                reportBuildPolicy: 'ALWAYS',\n                report: 'allure-report'\n            ])\n        }\n    }\n}\n<code>`<\/code>\n\n---\n\n<h2>\u516d\u3001\u8d28\u91cf\u95e8\u7981<\/h2>\n\n<h3>\u6d4b\u8bd5\u5931\u8d25\u963b\u65ad<\/h3>\n\n<code>`<\/code>groovy\nstage('\u6d4b\u8bd5') {\n    steps {\n        script {\n            def result = sh(script: 'pytest', returnStatus: true)\n\n            if (result != 0) {\n                error('\u6d4b\u8bd5\u5931\u8d25\uff0c\u963b\u65ad\u90e8\u7f72')\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n<h3>\u8986\u76d6\u7387\u95e8\u7981<\/h3>\n\n<code>`<\/code>groovy\nstage('\u8986\u76d6\u7387\u68c0\u67e5') {\n    steps {\n        script {\n            def coverage = sh(\n                script: 'python get_coverage.py',\n                returnStdout: true\n            ).trim().toDouble()\n\n            if (coverage &lt; 80) {\n                error(&quot;\u8986\u76d6\u7387 ${coverage}% \u4e0d\u8fbe\u6807\uff08\u8981\u6c4280%\u4ee5\u4e0a\uff09&quot;)\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n---\n\n<h2>\u4e03\u3001\u5e76\u884c\u6d4b\u8bd5<\/h2>\n\n<h3>\u5e76\u884c\u6267\u884cStage<\/h3>\n\n<code>`<\/code>groovy\npipeline {\n    agent any\n\n    stages {\n        stage('\u5e76\u884c\u6d4b\u8bd5') {\n            parallel {\n                stage('\u5355\u5143\u6d4b\u8bd5') {\n                    steps {\n                        sh 'pytest tests\/unit'\n                    }\n                }\n                stage('API\u6d4b\u8bd5') {\n                    steps {\n                        sh 'newman run api-tests.json'\n                    }\n                }\n                stage('UI\u6d4b\u8bd5') {\n                    steps {\n                        sh 'pytest tests\/ui'\n                    }\n                }\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n---\n\n<h2>\u516b\u3001\u5b9a\u65f6\u6d4b\u8bd5\u4efb\u52a1<\/h2>\n\n<h3>\u5b9a\u65f6\u89e6\u53d1<\/h3>\n\n<code>`<\/code>groovy\npipeline {\n    agent any\n\n    triggers {\n        \/\/ \u6bcf\u5929\u65e9\u4e0a9\u70b9\u6267\u884c\n        cron('0 9 * * *')\n    }\n\n    stages {\n        stage('\u5b9a\u65f6\u6d4b\u8bd5') {\n            steps {\n                sh 'pytest tests\/regression'\n            }\n        }\n    }\n}\n<code>`<\/code>\n\n---\n\n<h2>\u603b\u7ed3<\/h2>\n\nJenkins\u6d4b\u8bd5\u6d41\u6c34\u7ebf\u8981\u70b9\uff1a\n\n| \u73af\u8282 | \u914d\u7f6e |\n|------|------|\n| \u5355\u5143\u6d4b\u8bd5 | pytest + \u8986\u76d6\u7387\u62a5\u544a |\n| \u96c6\u6210\u6d4b\u8bd5 | Docker\u73af\u5883\u9694\u79bb |\n| API\u6d4b\u8bd5 | Newman\/RunnerGo\u96c6\u6210 |\n| \u62a5\u544a | JUnit\/Allure\u53d1\u5e03 |\n| \u95e8\u7981 | \u8986\u76d6\u7387\u3001\u5931\u8d25\u963b\u65ad |\n| \u5e76\u884c | parallel stage |\n\n---\n\n<strong>\u4e0b\u671f\u9884\u544a\uff1aAppium 2.0\u65b0\u67b6\u6784\u89e3\u6790 - \u79fb\u52a8\u7aef\u81ea\u52a8\u5316\u6d4b\u8bd5\u8fdb\u9636\u6307\u5357<\/strong>\n\n&gt; \u4f5c\u8005\uff1aDevOps\u8001\u5175\uff0c10\u5e74CI\/CD\u5b9e\u8df5\u7ecf\u9a8c","protected":false},"excerpt":{"rendered":"<p>Jenkins\u6d4b\u8bd5\u6d41\u6c34\u7ebf\u642d\u5efa &#8211; CI\/CD\u4e2d\u7684\u81ea\u52a8\u5316\u6d4b\u8bd5\u96c6\u6210\u5b9e\u6218 &gt; \u4f5c\u8005\uff1aDevOps\u8001\u5175 &#8212; \u524d\u8a00 \u5927\u5bb6\u597d\uff0c\u6211\u662fDevOps\u8001\u5175\u3002\u505aCI\/CD\u5b9e\u8df510\u5e74\uff0c\u6d4b\u8bd5\u96c6\u6210\u662f\u6700\u5bb9\u6613\u88ab\u5ffd\u89c6\u7684\u73af\u8282\u3002&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-39","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=\/wp\/v2\/posts\/39","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=39"}],"version-history":[{"count":0,"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=\/wp\/v2\/posts\/39\/revisions"}],"wp:attachment":[{"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=39"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=39"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.runnergo.cn\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=39"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}