Harness 教程 04:集成测试与制品管理:国内网络环境落地版

157 阅读8分钟

一:教程定位

在前 3 篇中,我们已经完成了 Harness 平台环境搭建、第一个 CI/CD 流水线,以及 Git 代码提交自动触发 Pipeline。

第 4 篇开始进入企业 CI/CD 的核心能力:测试质量控制与制品管理

很多初学者在做流水线时,只会把代码打包成镜像并部署到 Kubernetes,但企业真实落地不能只看"能不能部署"。更重要的是:

代码是否通过单元测试? 构建出的镜像是否可追溯? 镜像是否推送到了企业统一仓库? 失败测试是否能阻断发布? 代码质量和安全扫描是否通过? 制品版本是否能关联到 Git Commit 和流水线执行记录?

本篇目标是基于 Node.js 示例项目,在 Harness Pipeline 中增加:

npm install npm 单元测试 测试报告输出 Docker 镜像构建 镜像推送到 Harbor 镜像版本 Tag 管理 SonarQube 代码质量扫描 质量门检查 失败阻断后续部署


二:适合人群

本文适合:

DevOps 初学者 后端开发人员 前端开发人员 测试工程师 SRE / 运维工程师 正在把 GitLab CI / Jenkins 迁移到 Harness 的团队

建议已经完成:

Harness 教程 01:平台介绍与 Delegate 安装 Harness 教程 02:创建第一个 CI/CD 流水线 Harness 教程 03:代码仓库与触发器基础


三:学习目标

完成本文后,你应该能够:

在 Harness CI Stage 中添加 npm 单元测试步骤 理解测试失败如何阻断镜像构建和部署 把 Docker 镜像推送到企业 Harbor 使用 Harbor Robot Account 做流水线认证 为镜像设计可追溯 Tag 在 Harness 中保存和引用 Secret 通过 Shell Script / Run Step 集成 SonarQube 扫描 使用 SonarQube Quality Gate 阻断不合格代码 理解国内网络环境下 npm、镜像仓库、SonarQube 的常见问题


四:国内网络环境下的整体架构

本篇推荐架构如下:

开发人员
  ↓ git push
GitLab / Gitee
  ↓ webhook
Harness Pipeline
  ↓ 派发任务
Harness Delegate
  ↓ 拉取代码
GitLab / Gitee
  ↓ npm install / npm test
npmmirror / 企业 npm 私服
  ↓ SonarQube 扫描
内网 SonarQube
  ↓ docker build
CI 构建环境
  ↓ docker push
Harbor / ACR / TCR / SWR
  ↓ deploy
Kubernetes dev/test 环境

核心原则:

Git、Harbor、SonarQube、Kubernetes 尽量都在国内网络或企业内网 Harness SaaS 负责流水线编排 Harness Delegate 负责访问内网资源 npm 不直接走海外官方源 基础镜像提前同步到 Harbor 所有密码、Token 都使用 Harness Secret 管理


五:本篇最终流水线结构

本篇 Pipeline 结构如下:

Pipeline: nodejs-test-artifact-harbor

Stage 1: CI Test And Package Step 1: 打印构建上下文 Step 2: 安装依赖 npm install Step 3: 执行 npm 单元测试 Step 4: 生成测试报告 Step 5: SonarQube 代码扫描 Step 6: SonarQube Quality Gate 检查 Step 7: 构建 Docker 镜像 Step 8: 推送镜像到 Harbor

Stage 2: Approval For Deploy Step 1: 人工确认测试与扫描结果

Stage 3: Deploy Dev Step 1: 部署到 Kubernetes dev 环境 Step 2: 健康检查

本篇重点放在 Stage 1:测试与制品管理。部署阶段可以沿用第 2 篇的 Kubernetes 部署配置。


六:环境准备清单

1. Harness 资源

需要提前准备:

Org: devops-lab Project: harness-demo Delegate: cn-k8s-dev Git Connector: gitlab_company Docker Registry Connector: harbor_company Kubernetes Connector: k8s_dev_cluster Pipeline: nodejs-test-artifact-harbor

2. Git 仓库

示例:

gitlab.company.com/devops/harn…

推荐分支:

main dev feature/*

3. Harbor

示例地址:

harbor.company.com

示例项目:

devops library platform security

镜像地址:

harbor.company.com/devops/harness-demo-app

建议 Harbor 中准备:

library/node:20-alpine library/sonar-scanner-cli:latest library/alpine-curl-jq:3.20 devops/harness-demo-app

如果企业可以访问官方镜像,可以先在一台可出网机器上同步;如果完全离线,则由运维统一导入镜像。

4. SonarQube

示例地址:

sonarqube.company.com

需要准备:

SonarQube 项目:harness-demo-app SonarQube Token:用于扫描认证 Quality Gate:例如"无 Blocker / Critical 漏洞,新代码覆盖率达标"

如果企业暂时没有 SonarQube,本篇的 SonarQube 部分可以作为练习项,先完成 npm 测试与 Harbor 制品管理。

5. Kubernetes

需要准备命名空间:

kubectl create namespace harness-ci
kubectl create namespace dev

创建 Harbor 拉取密钥:

kubectl create secret docker-registry harbor-pull-secret \
  --docker-server=harbor.company.com \
  --docker-username='robot$devops+harness-ci' \
  --docker-password='替换为 Harbor Robot Token' \
  --docker-email=devops@company.com \
  -n dev

七:推荐仓库结构

本篇使用如下仓库结构:

harness-demo-app/
├── README.md
├── .npmrc
├── app/
│   ├── package.json
│   ├── server.js
│   ├── server.test.js
│   └── test-runner.js
├── coverage/
│   └── .gitkeep
├── docker/
│   └── Dockerfile
├── k8s/
│   ├── namespace.yaml
│   ├── deployment.yaml
│   └── service.yaml
├── sonar-project.properties
├── scripts/
│   ├── sync-ci-images.sh
│   ├── check-sonarqube-quality-gate.sh
│   ├── build-image-local.sh
│   └── push-image-local.sh
└── harness/
    └── pipelines/
        └── nodejs-test-artifact-harbor.yaml

八:Node.js 示例应用代码

1. .npmrc

registry=https://registry.npmmirror.com
fetch-retries=5
fetch-retry-mintimeout=20000
fetch-retry-maxtimeout=120000

说明:

国内网络环境建议使用 npmmirror 企业内网有 npm 私服时,可以替换为企业 npm 私服地址 例如:npm.company.com/repository/…


2. app/package.json

{
  "name": "harness-demo-app",
  "version": "1.0.0",
  "description": "Harness CI test and artifact demo for China network",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "test": "node server.test.js",
    "test:report": "node test-runner.js"
  },
  "dependencies": {
    "express": "^4.18.3"
  }
}

3. app/server.js

const express = require("express");

function createApp() {
  const app = express();

  app.get("/", (req, res) => {
    res.json({
      app: "harness-demo-app",
      env: process.env.APP_ENV || "dev",
      version: process.env.APP_VERSION || "v1",
      message: "Hello Harness Test And Artifact Management"
    });
  });

  app.get("/health", (req, res) => {
    res.status(200).json({ status: "UP" });
  });

  app.get("/api/users", (req, res) => {
    res.json([
      { id: 1, name: "Alice" },
      { id: 2, name: "Bob" }
    ]);
  });

  return app;
}

if (require.main === module) {
  const app = createApp();
  const port = process.env.PORT || 3000;

  app.listen(port, () => {
    console.log(`server started on port ${port}`);
  });
}

module.exports = createApp;

4. app/server.test.js

const createApp = require("./server");

function assert(condition, message) {
  if (!condition) {
    throw new Error(message);
  }
}

const app = createApp();

assert(app, "app should be created");
assert(typeof app.listen === "function", "app should be an express instance");

const routes = app._router.stack
  .filter(layer => layer.route)
  .map(layer => layer.route.path);

assert(routes.includes("/"), "root route should exist");
assert(routes.includes("/health"), "health route should exist");
assert(routes.includes("/api/users"), "users route should exist");

console.log("unit test passed");

5. app/test-runner.js

为了让初学者不依赖复杂测试框架,这里用 Node.js 生成一个简单测试报告文件。企业项目可以换成 Jest、Vitest、Mocha、JUnit XML 等。

const fs = require("fs");
const path = require("path");

const reportDir = path.join(__dirname, "..", "coverage");
const reportFile = path.join(reportDir, "unit-test-report.json");

if (!fs.existsSync(reportDir)) {
  fs.mkdirSync(reportDir, { recursive: true });
}

const result = {
  app: "harness-demo-app",
  status: "PASS",
  total: 3,
  passed: 3,
  failed: 0,
  timestamp: new Date().toISOString()
};

fs.writeFileSync(reportFile, JSON.stringify(result, null, 2));

console.log("unit test report generated:");
console.log(reportFile);

九:Dockerfile:构建业务镜像

docker/Dockerfile

FROM harbor.company.com/library/node:20-alpine

WORKDIR /app

COPY .npmrc ./
COPY app/package*.json ./

RUN npm install --omit=dev

COPY app/server.js ./

ENV PORT=3000
ENV APP_ENV=dev

EXPOSE 3000

CMD ["node", "server.js"]

变量说明:

harbor.company.com:替换为你的 Harbor 域名 library/node:20-alpine:需要提前同步到 Harbor APP_ENV:运行环境 PORT:服务端口

注意:

Dockerfile 中不要直接 FROM node:20-alpine 国内环境建议统一使用 Harbor 基础镜像 生产镜像不要把测试文件、源码无关文件全部复制进去


十:SonarQube 配置文件

sonar-project.properties

sonar.projectKey=harness-demo-app
sonar.projectName=Harness Demo App
sonar.projectVersion=1.0.0

sonar.sources=app
sonar.exclusions=**/node_modules/**,**/coverage/**

sonar.sourceEncoding=UTF-8

说明:

sonar.projectKey:SonarQube 中唯一项目标识 sonar.projectName:展示名称 sonar.sources:扫描源代码目录 sonar.exclusions:排除 node_modules、coverage 等目录

如果你是多服务仓库,可以按服务目录拆分:

sonar.projectKey=user-service
sonar.sources=services/user-service/src

十一:同步 CI 基础镜像到 Harbor

scripts/sync-ci-images.sh

#!/usr/bin/env bash
set -e

HARBOR_HOST="harbor.company.com"

docker login ${HARBOR_HOST}

echo "同步 Node.js 基础镜像"
docker pull node:20-alpine
docker tag node:20-alpine ${HARBOR_HOST}/library/node:20-alpine
docker push ${HARBOR_HOST}/library/node:20-alpine

echo "同步 SonarScanner CLI 镜像"
docker pull sonarsource/sonar-scanner-cli:latest
docker tag sonarsource/sonar-scanner-cli:latest ${HARBOR_HOST}/library/sonar-scanner-cli:latest
docker push ${HARBOR_HOST}/library/sonar-scanner-cli:latest

echo "同步 curl 工具镜像"
docker pull alpine:3.20
docker tag alpine:3.20 ${HARBOR_HOST}/library/alpine:3.20
docker push ${HARBOR_HOST}/library/alpine:3.20

echo "同步完成"

变量说明:

HARBOR_HOST:替换为企业 Harbor 地址 sonarsource/sonar-scanner-cli:如果不能直接访问 DockerHub,需要在可出网机器上同步

如果企业完全离线,推荐由平台团队统一制作:

harbor.company.com/platform/ci-node-sonar:20

这个镜像可以内置:

node npm curl jq sonar-scanner docker CLI kubectl

这样可以减少流水线中临时安装工具导致的网络问题。


十二:Harbor Robot Account 设计

1. 创建 Harbor 项目

进入 Harbor:

Projects → New Project

创建:

Project Name: devops Access Level: Private


2. 创建 Robot Account

进入:

Projects → devops → Robot Accounts → New Robot Account

推荐名称:

harness-ci

建议权限:

Pull Repository Push Repository List Repository Read Artifact

Harbor 中 Push Repository 权限通常需要同时具备 Pull Repository 权限,因此不要只勾选 Push。

生成后复制 Robot Token,注意:

Harbor Robot Token 创建后需要立即保存 不要提交到 Git 仓库 不要发到微信群或聊天工具 存入 Harness Secret


3. Harness Secret

在 Harness 中创建 Secret:

Project Settings → Secrets → New Secret → Text

填写:

Name: harbor-robot-token Identifier: harbor_robot_token Secret Value: Harbor Robot Token


4. Docker Registry Connector

创建 Docker Registry Connector:

Project Settings → Connectors → New Connector → Artifact Repositories → Docker Registry

填写:

Name: harbor-company Identifier: harbor_company Provider Type: Harbor 或 Other Docker Registry URL: harbor.company.com Authentication: Username and Password Username: robot$devops+harness-ci Password: <+secrets.getValue("harbor_robot_token")> Connectivity: Connect through Delegate Delegate Selector: cn-k8s-dev

YAML 参考:

connector:
  name: harbor-company
  identifier: harbor_company
  description: 企业 Harbor 镜像仓库
  orgIdentifier: devops_lab
  projectIdentifier: harness_demo
  type: DockerRegistry
  spec:
    dockerRegistryUrl: https://harbor.company.com
    providerType: Harbor
    auth:
      type: UsernamePassword
      spec:
        username: robot$devops+harness-ci
        passwordRef: harbor_robot_token
    executeOnDelegate: true
    delegateSelectors:
      - cn-k8s-dev

注意:

如果你的 Harness 版本中 providerType 没有 Harbor,可选择 Other Harbor 地址不要随意加 /v2,按 UI 测试结果和企业 Registry 地址为准 生产环境建议每个项目一个 Robot Account


十三:SonarQube Token 与 Secret

1. 创建 SonarQube Token

进入 SonarQube:

My Account → Security → Generate Tokens

创建:

Token Name: harness-demo-app-token Type: Project Analysis Token Project: harness-demo-app

复制 Token。


2. 创建 Harness Secret

Project Settings → Secrets → New Secret → Text

填写:

Name: sonar-token Identifier: sonar_token Secret Value: SonarQube Token

再创建 SonarQube 地址变量,也可以直接写在 Pipeline 变量中:

SONAR_HOST_URL=sonarqube.company.com

如果 SonarQube 使用自签证书,建议:

把企业 CA 导入 SonarScanner 镜像 或使用企业统一签发的受信任证书 不建议长期使用跳过证书校验


十四:SonarQube Quality Gate 检查脚本

scripts/check-sonarqube-quality-gate.sh

#!/usr/bin/env bash
set -e

SONAR_HOST_URL="${SONAR_HOST_URL}"
SONAR_TOKEN="${SONAR_TOKEN}"
REPORT_FILE=".scannerwork/report-task.txt"

if [ ! -f "${REPORT_FILE}" ]; then
  echo "未找到 ${REPORT_FILE},请确认 sonar-scanner 已成功执行"
  exit 1
fi

CE_TASK_ID=$(grep "ceTaskId=" "${REPORT_FILE}" | cut -d'=' -f2)

if [ -z "${CE_TASK_ID}" ]; then
  echo "无法从 report-task.txt 获取 ceTaskId"
  exit 1
fi

echo "SonarQube CE Task ID: ${CE_TASK_ID}"

for i in $(seq 1 30); do
  TASK_JSON=$(curl -s -u "${SONAR_TOKEN}:" \
    "${SONAR_HOST_URL}/api/ce/task?id=${CE_TASK_ID}")

  STATUS=$(echo "${TASK_JSON}" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')
  ANALYSIS_ID=$(echo "${TASK_JSON}" | sed -n 's/.*"analysisId":"\([^"]*\)".*/\1/p')

  echo "SonarQube task status: ${STATUS}"

  if [ "${STATUS}" = "SUCCESS" ]; then
    break
  fi

  if [ "${STATUS}" = "FAILED" ] || [ "${STATUS}" = "CANCELED" ]; then
    echo "SonarQube 后台任务失败或取消"
    echo "${TASK_JSON}"
    exit 1
  fi

  sleep 5
done

if [ -z "${ANALYSIS_ID}" ]; then
  echo "未获取到 analysisId,Quality Gate 无法检查"
  exit 1
fi

QG_JSON=$(curl -s -u "${SONAR_TOKEN}:" \
  "${SONAR_HOST_URL}/api/qualitygates/project_status?analysisId=${ANALYSIS_ID}")

QG_STATUS=$(echo "${QG_JSON}" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p')

echo "Quality Gate Status: ${QG_STATUS}"

if [ "${QG_STATUS}" != "OK" ]; then
  echo "SonarQube Quality Gate 未通过"
  echo "${QG_JSON}"
  exit 1
fi

echo "SonarQube Quality Gate 通过"

说明:

先读取 .scannerwork/report-task.txt 通过 ceTaskId 查询后台分析任务是否完成 再通过 analysisId 查询 Quality Gate 状态 状态不是 OK 时退出 1,阻断后续镜像构建和部署

注意:

这个脚本使用 sed 解析 JSON,适合入门和轻量场景 企业生产建议使用 jq 或 Python 解析 JSON 如果 CI 镜像没有 curl,需要使用内置 curl 的工具镜像


十五:在 Harness 中创建 CI 测试与制品流水线

进入 Harness:

Project → Pipelines → New Pipeline

填写:

Name: nodejs-test-artifact-harbor Identifier: nodejs_test_artifact_harbor Description: Node.js 集成测试与 Harbor 制品管理流水线


十六:创建 CI Stage

添加 Stage:

Add Stage → Build

配置:

Stage Name: CI Test And Package Stage Identifier: ci_test_and_package Infrastructure: Kubernetes Kubernetes Connector: k8s_dev_cluster Namespace: harness-ci OS: Linux

启用 Clone Codebase:

Codebase Connector: gitlab_company Repository: devops/harness-demo-app Branch: <+trigger.branch> 或 main Clone Directory: /harness


十七:Step 1 — 打印构建上下文

添加 Run Step:

Name: Print Build Context Identifier: print_build_context Image: harbor.company.com/library/node:20-alpine Shell: Sh

命令:

echo "===== Build Context ====="
echo "Pipeline: <+pipeline.name>"
echo "Execution ID: <+pipeline.executionId>"
echo "Sequence ID: <+pipeline.sequenceId>"
echo "Trigger Type: <+pipeline.triggerType>"
echo "Branch: <+codebase.branch>"
echo "Commit SHA: <+codebase.commitSha>"
echo "Git user: <+trigger.gitUser>"

作用:

方便排查本次构建对应哪个分支、哪个提交、哪个流水线执行


十八:Step 2 — 安装依赖

添加 Run Step:

Name: Install Dependencies Identifier: install_dependencies Image: harbor.company.com/library/node:20-alpine Shell: Sh

命令:

cd app

npm config set registry https://registry.npmmirror.com
npm install

echo "依赖安装完成"

企业内网 npm 私服写法:

npm config set registry https://npm.company.com/repository/npm-group/
npm install

建议:

优先使用 npm ci,而不是 npm install 前提是项目中存在 package-lock.json

可改为:

cd app
npm config set registry https://registry.npmmirror.com
npm ci

十九:Step 3 — 执行 npm 单元测试

添加 Run Step:

Name: Unit Test Identifier: unit_test Image: harbor.company.com/library/node:20-alpine Shell: Sh

命令:

cd app
npm test

预期输出:

unit test passed

如果测试失败,Harness 会标记该 Step 失败,后续 SonarQube、Docker Build、Deploy 都不应该继续执行。

企业建议:

单元测试失败必须阻断镜像构建 不要把测试失败的代码打包成镜像 不要把测试失败的镜像推送到 Harbor


二十:Step 4 — 生成测试报告

添加 Run Step:

Name: Generate Test Report Identifier: generate_test_report Image: harbor.company.com/library/node:20-alpine Shell: Sh

命令:

cd app
npm run test:report

cd ..
cat coverage/unit-test-report.json

说明:

本示例生成 JSON 测试报告 真实项目建议生成 JUnit XML、LCOV、HTML Coverage 等格式

后续可以将测试报告上传到对象存储、制品仓库或 Harness Artifact Registry。


二十一:Step 5 — SonarQube 扫描

1. 使用 Run Step 调用 SonarScanner

添加 Run Step:

Name: SonarQube Scan Identifier: sonarqube_scan Image: harbor.company.com/library/sonar-scanner-cli:latest Shell: Sh

环境变量:

SONAR_HOST_URL=sonarqube.company.com SONAR_TOKEN=<+secrets.getValue("sonar_token")>

命令:

sonar-scanner \
  -Dsonar.host.url=${SONAR_HOST_URL} \
  -Dsonar.token=${SONAR_TOKEN}

前提:

仓库根目录存在 sonar-project.properties SonarQube 中已创建 harness-demo-app 项目 SONAR_TOKEN 有该项目分析权限 Delegate / CI Pod 可以访问 SonarQube 地址

2. 如果使用内网自签证书

推荐做法:

制作企业版 sonar-scanner 镜像 将企业 CA 导入镜像 truststore 推送到 harbor.company.com/library/sonar-scanner-cli:company

示例 Dockerfile:

FROM sonarsource/sonar-scanner-cli:latest

USER root
COPY company-ca.crt /usr/local/share/ca-certificates/company-ca.crt
RUN update-ca-certificates || true

USER scanner-cli

构建并推送:

docker build -t harbor.company.com/library/sonar-scanner-cli:company .
docker push harbor.company.com/library/sonar-scanner-cli:company

Pipeline 中改用:

harbor.company.com/library/sonar-scanner-cli:company


二十二:Step 6 — SonarQube Quality Gate 检查

添加 Run Step:

Name: SonarQube Quality Gate Identifier: sonarqube_quality_gate Image: harbor.company.com/library/alpine:3.20 Shell: Sh

环境变量:

SONAR_HOST_URL=sonarqube.company.com SONAR_TOKEN=<+secrets.getValue("sonar_token")>

命令:

apk add --no-cache curl

chmod +x scripts/check-sonarqube-quality-gate.sh

SONAR_HOST_URL=${SONAR_HOST_URL} \
SONAR_TOKEN=${SONAR_TOKEN} \
scripts/check-sonarqube-quality-gate.sh

国内企业注意:

如果 CI Pod 不能访问公网 apk 源,不要在流水线中 apk add 应提前制作包含 curl/jq 的工具镜像 例如 harbor.company.com/platform/ci-tools:node20-curl-jq-sonar

更稳定的企业做法:

harbor.company.com/platform/ci-tools:1.0.0

内置:

node npm curl jq sonar-scanner kubectl docker CLI


二十三:Step 7 — 构建并推送 Docker 镜像到 Harbor

添加 Step:

Add Step → Build and Push to Docker Registry

填写:

Name: Build And Push Image Identifier: build_and_push_image Docker Registry Connector: harbor_company Dockerfile: docker/Dockerfile Context: . Repository: harbor.company.com/devops/harness-demo-app Tags: <+pipeline.sequenceId> <+codebase.commitSha>

说明:

<+pipeline.sequenceId>:流水线执行序号,适合开发环境快速识别 <+codebase.commitSha>:Git 提交 ID,适合追溯代码版本

不建议生产只使用:

latest

开发环境可以额外加:

latest

生产环境建议:

v1.0.0 release-20260615-001 <+codebase.commitSha>


二十四:镜像 Tag 策略

推荐设计:

开发环境: <+pipeline.sequenceId> latest

测试环境: <+codebase.commitSha>

准生产环境: release-<日期>-<流水号>

生产环境: Git Tag 语义化版本号 release-20260615-001

示例:

harbor.company.com/devops/harness-demo-app:105 harbor.company.com/devops/harness-demo-app:9f3a7c1 harbor.company.com/devops/harness-demo-app:release-20260615-001 harbor.company.com/devops/harness-demo-app:v1.3.0

企业要求:

镜像 Tag 必须能追溯到 Git Commit 镜像 Tag 必须能追溯到 Pipeline Execution 生产镜像不能被覆盖 Harbor 开启保留策略和漏洞扫描


二十五:完整 Pipeline YAML 参考

下面 YAML 作为参考,建议先使用 Harness Visual Editor 创建成功,再切换到 YAML 查看实际字段。不同 Harness 版本和模块启用情况可能会导致字段略有差异。

pipeline:
  name: nodejs-test-artifact-harbor
  identifier: nodejs_test_artifact_harbor
  projectIdentifier: harness_demo
  orgIdentifier: devops_lab
  tags:
    tutorial: harness-04
    network: china
    app: nodejs
  stages:
    - stage:
        name: CI Test And Package
        identifier: ci_test_and_package
        type: CI
        spec:
          cloneCodebase: true
          infrastructure:
            type: KubernetesDirect
            spec:
              connectorRef: k8s_dev_cluster
              namespace: harness-ci
              automountServiceAccountToken: true
              os: Linux
          execution:
            steps:
              - step:
                  name: Print Build Context
                  identifier: print_build_context
                  type: Run
                  spec:
                    connectorRef: harbor_company
                    image: harbor.company.com/library/node:20-alpine
                    shell: Sh
                    command: |
                      echo "Pipeline: <+pipeline.name>"
                      echo "Execution ID: <+pipeline.executionId>"
                      echo "Sequence ID: <+pipeline.sequenceId>"
                      echo "Trigger Type: <+pipeline.triggerType>"
                      echo "Branch: <+codebase.branch>"
                      echo "Commit SHA: <+codebase.commitSha>"

              - step:
                  name: Install Dependencies
                  identifier: install_dependencies
                  type: Run
                  spec:
                    connectorRef: harbor_company
                    image: harbor.company.com/library/node:20-alpine
                    shell: Sh
                    command: |
                      cd app
                      npm config set registry https://registry.npmmirror.com
                      npm install

              - step:
                  name: Unit Test
                  identifier: unit_test
                  type: Run
                  spec:
                    connectorRef: harbor_company
                    image: harbor.company.com/library/node:20-alpine
                    shell: Sh
                    command: |
                      cd app
                      npm test

              - step:
                  name: Generate Test Report
                  identifier: generate_test_report
                  type: Run
                  spec:
                    connectorRef: harbor_company
                    image: harbor.company.com/library/node:20-alpine
                    shell: Sh
                    command: |
                      cd app
                      npm run test:report
                      cd ..
                      cat coverage/unit-test-report.json

              - step:
                  name: SonarQube Scan
                  identifier: sonarqube_scan
                  type: Run
                  spec:
                    connectorRef: harbor_company
                    image: harbor.company.com/library/sonar-scanner-cli:latest
                    shell: Sh
                    envVariables:
                      SONAR_HOST_URL: https://sonarqube.company.com
                      SONAR_TOKEN: <+secrets.getValue("sonar_token")>
                    command: |
                      sonar-scanner \
                        -Dsonar.host.url=${SONAR_HOST_URL} \
                        -Dsonar.token=${SONAR_TOKEN}

              - step:
                  name: SonarQube Quality Gate
                  identifier: sonarqube_quality_gate
                  type: Run
                  spec:
                    connectorRef: harbor_company
                    image: harbor.company.com/platform/ci-tools:1.0.0
                    shell: Sh
                    envVariables:
                      SONAR_HOST_URL: https://sonarqube.company.com
                      SONAR_TOKEN: <+secrets.getValue("sonar_token")>
                    command: |
                      chmod +x scripts/check-sonarqube-quality-gate.sh
                      scripts/check-sonarqube-quality-gate.sh

              - step:
                  name: Build And Push Image
                  identifier: build_and_push_image
                  type: BuildAndPushDockerRegistry
                  spec:
                    connectorRef: harbor_company
                    repo: harbor.company.com/devops/harness-demo-app
                    dockerfile: docker/Dockerfile
                    context: .
                    tags:
                      - <+pipeline.sequenceId>
                      - <+codebase.commitSha>

说明:

SonarQube Quality Gate 失败时,Build And Push Image 不会执行 Unit Test 失败时,SonarQube 和 Docker Build 都不会执行 Docker Build 成功后,镜像会推送到 Harbor


二十六:部署阶段如何引用镜像

在 k8s/deployment.yaml 中:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: harness-demo-app
  namespace: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: harness-demo-app
  template:
    metadata:
      labels:
        app: harness-demo-app
    spec:
      imagePullSecrets:
        - name: harbor-pull-secret
      containers:
        - name: harness-demo-app
          image: harbor.company.com/devops/harness-demo-app:<+pipeline.sequenceId>
          ports:
            - containerPort: 3000
          env:
            - name: APP_VERSION
              value: "<+pipeline.sequenceId>"
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10

如果使用 commit sha 作为镜像 Tag:

image: harbor.company.com/devops/harness-demo-app:<+codebase.commitSha>

注意:

Build Step 的 tag 和 Deployment 中引用的 tag 必须一致 否则会出现构建成功但部署拉不到镜像


二十七:运行流水线

点击:

Save Run

选择:

Branch: main 或 dev

期望执行顺序:

Print Build Context Install Dependencies Unit Test Generate Test Report SonarQube Scan SonarQube Quality Gate Build And Push Image

如果前面任一步失败,后面步骤不会继续执行。


二十八:验证测试结果

在 Harness 执行日志中检查:

npm install 是否成功 npm test 是否输出 unit test passed coverage/unit-test-report.json 是否生成 SonarQube 扫描是否完成 Quality Gate 是否 OK Docker 镜像是否推送成功

查看测试报告内容:

{
  "app": "harness-demo-app",
  "status": "PASS",
  "total": 3,
  "passed": 3,
  "failed": 0,
  "timestamp": "2026-06-15T00:00:00.000Z"
}

二十九:验证 Harbor 镜像

在本地或运维机执行:

docker login harbor.company.com

docker pull harbor.company.com/devops/harness-demo-app:替换为流水线序号

查看 Harbor UI:

Projects → devops → Repositories → harness-demo-app → Artifacts

确认:

存在 <+pipeline.sequenceId> Tag 存在 <+codebase.commitSha> Tag 镜像上传时间与流水线执行时间一致 镜像 Digest 可查看


三十:验证 SonarQube

进入 SonarQube:

Projects → harness-demo-app

检查:

Bugs Vulnerabilities Code Smells Duplications Coverage Quality Gate

如果 Quality Gate 为 Failed,Harness 中 SonarQube Quality Gate Step 应该失败,后续 Docker Build 不应执行。


三十一:常见问题排查

1. npm install 很慢或失败

可能原因:

没有使用国内 npm 源 .npmrc 没有提交 企业网络阻断 registry.npmmirror.com CI Pod 无法访问企业 npm 私服

排查:

npm config get registry
curl -I https://registry.npmmirror.com

解决:

配置 .npmrc 使用企业 npm 私服 将 npm 依赖缓存到 Nexus / Verdaccio


2. 单元测试失败

可能原因:

测试代码有问题 依赖没装完整 Node 版本不一致 本地和 CI 环境不一致

排查:

node -v
npm -v
npm test

建议:

本地开发环境和 CI 镜像 Node 版本保持一致 测试失败必须阻断发布


3. SonarQube 扫描失败

可能原因:

SONAR_HOST_URL 错误 SONAR_TOKEN 错误 SonarQube 项目不存在 Delegate / CI Pod 访问不到 SonarQube 自签证书不被信任 sonar-project.properties 配置错误

排查:

curl -k -I https://sonarqube.company.com

解决:

确认 SonarQube Token 有项目分析权限 确认项目 key 与 sonar-project.properties 一致 导入企业 CA 证书到 scanner 镜像


4. Quality Gate 一直等待

可能原因:

SonarQube 后台任务慢 SonarQube Compute Engine 队列堆积 脚本没有读到 .scannerwork/report-task.txt

排查:

cat .scannerwork/report-task.txt

进入 SonarQube:

Administration → Projects → Background Tasks


5. Docker Build 失败

可能原因:

Dockerfile 路径错误 Context 配置错误 基础镜像不存在 基础镜像拉取失败 Harbor 自签证书问题

排查:

docker pull harbor.company.com/library/node:20-alpine
docker build -f docker/Dockerfile -t test .

6. Docker Push 失败

可能原因:

Harbor Connector 认证失败 Robot Account 没有 push 权限 Harbor 项目不存在 镜像名写错 Tag 不合法

排查:

docker login harbor.company.com
docker push harbor.company.com/devops/harness-demo-app:test

解决:

检查 Robot Account 权限 检查 Harness Secret 检查 Docker Registry Connector


7. Kubernetes 拉不到镜像

可能原因:

imagePullSecrets 不存在 Harbor 机器人账号没有 pull 权限 Deployment 中镜像 Tag 和构建 Tag 不一致 Kubernetes 节点不信任 Harbor 证书

排查:

kubectl describe pod -n dev
kubectl get secret harbor-pull-secret -n dev

三十二:企业制品管理最佳实践

1. Harbor 项目分层

建议:

library:基础镜像 platform:平台工具镜像 devops:业务应用镜像 security:安全扫描工具镜像 middleware:中间件镜像


2. Robot Account 分环境

建议:

dev 环境:robotdevops+devcitest环境:robotdevops+dev-ci test 环境:robotdevops+test-ci prod 环境:robot$devops+prod-release

不要所有环境共用一个机器人账号。


3. 镜像不可变

生产环境建议开启:

Tag 不可覆盖 镜像保留策略 镜像漏洞扫描 镜像签名


4. 测试门禁

推荐最小门禁:

单元测试必须通过 SonarQube Quality Gate 必须 OK Docker 镜像必须构建成功 镜像必须推送到 Harbor 部署前必须人工审批


5. 流水线失败策略

建议:

测试失败:直接终止 质量门失败:直接终止 镜像构建失败:直接终止 推送失败:直接终止 部署失败:触发回滚或人工介入


三十三:练习 1 — 让单元测试失败并观察流水线

修改 app/server.test.js

throw new Error("mock unit test failed");

提交代码:

git add app/server.test.js
git commit -m "test: mock unit test failure"
git push origin main

预期:

Unit Test Step 失败 Generate Test Report 不执行 SonarQube Scan 不执行 Build And Push Image 不执行 Deploy 不执行

目的:

理解测试失败必须阻断制品发布


三十四:练习 2 — 集成 SonarQube

完成以下任务:

  1. 在 SonarQube 创建 harness-demo-app 项目
  2. 创建 Project Analysis Token
  3. 在 Harness 创建 sonar_token Secret
  4. 在仓库添加 sonar-project.properties
  5. 在 Pipeline 中添加 SonarQube Scan Step
  6. 在 Pipeline 中添加 Quality Gate 检查 Step

验收:

SonarQube 项目中可以看到扫描结果 Quality Gate 为 OK 时继续构建镜像 Quality Gate 为 Failed 时阻断构建镜像


三十五:练习 3 — 优化镜像 Tag

将 Build And Push Image Step 的 Tag 改为:

<+codebase.commitSha>

Deployment 中也改为:

image: harbor.company.com/devops/harness-demo-app:<+codebase.commitSha>

验收:

Harbor 中出现 commit sha 对应镜像 Kubernetes 部署的镜像与 Git commit 一致


三十六:练习 4 — 增加 Harbor 制品保留策略

在 Harbor 中配置:

Project → devops → Policy → Retention

建议策略:

保留最近 30 个 release-* 镜像 保留最近 20 个 main 分支镜像 清理 30 天前无 tag 镜像

目的:

避免 Harbor 存储无限增长 保留可回滚版本


三十七:验收标准

完成本文后,应达到:

已在 Pipeline 中添加 npm install 已在 Pipeline 中添加 npm test 测试失败可以阻断后续步骤 已生成测试报告 已完成 SonarQube 扫描 已完成 Quality Gate 检查 Quality Gate 失败可以阻断镜像构建 已构建 Docker 镜像 已推送镜像到 Harbor 镜像 Tag 可以追溯到 Pipeline 或 Git Commit Harbor 中可以查看对应镜像制品


三十八:本篇总结

本篇完成了 Harness 中"测试 + 制品管理"的基础落地。

你需要重点记住:

CI/CD 不是只做构建和部署,测试门禁同样重要 npm 单元测试应该放在 Docker Build 前面 测试失败不能继续构建镜像 SonarQube 扫描适合放在测试后、镜像构建前 Quality Gate 失败必须阻断发布 Harbor Robot Account 不要使用个人账号 Harbor Token 必须放入 Harness Secret 镜像 Tag 必须能追溯到 Git Commit 和 Pipeline 执行记录 生产环境不要依赖 latest 国内环境要提前同步基础镜像和扫描工具镜像