Logo
热心市民王先生

实施指南

实施指南 Docker CI/CD Harness

Docker 沙盒配置、CLI 工具链集成、CI/CD 流水线实践

本章提供方案 A(纯静态分析工具链)的完整实施步骤,包括 Docker 配置、CLI 集成和 CI/CD 流水线。

1. Docker 沙盒配置

1.1 基础镜像构建

Dockerfile

# Dockerfile.lint-sandbox
FROM eclipse-temurin:17-jre-alpine

# 安装基础工具
RUN apk add --no-cache \
    curl \
    jq \
    bash \
    git

# 配置目录
ENV TOOLS_DIR=/opt/tools
ENV PATH="${TOOLS_DIR}:${PATH}"
RUN mkdir -p ${TOOLS_DIR}

# 安装 ktlint
ARG KTLINT_VERSION=1.8.0
RUN curl -sSL \
    "https://github.com/pinterest/ktlint/releases/download/${KTLINT_VERSION}/ktlint" \
    -o ${TOOLS_DIR}/ktlint && \
    chmod +x ${TOOLS_DIR}/ktlint

# 安装 Detekt
ARG DETEKT_VERSION=1.23.8
RUN curl -sSL \
    "https://github.com/detekt/detekt/releases/download/v${DETEKT_VERSION}/detekt-cli-${DETEKT_VERSION}.zip" \
    -o /tmp/detekt.zip && \
    unzip /tmp/detekt.zip -d ${TOOLS_DIR} && \
    mv ${TOOLS_DIR}/detekt-cli-${DETEKT_VERSION} ${TOOLS_DIR}/detekt && \
    ln -s ${TOOLS_DIR}/detekt/bin/detekt-cli ${TOOLS_DIR}/detekt && \
    rm /tmp/detekt.zip

# 复制配置文件
COPY config/detekt-sandbox.yml /opt/config/detekt.yml
COPY config/.editorconfig /opt/config/.editorconfig

# 创建分析脚本
COPY scripts/lint-analyzer.sh /opt/scripts/lint-analyzer.sh
RUN chmod +x /opt/scripts/lint-analyzer.sh

WORKDIR /workspace

# 默认入口
ENTRYPOINT ["/opt/scripts/lint-analyzer.sh"]
CMD ["--help"]

1.2 配置文件

detekt-sandbox.yml

build:
  analysisMode: light
  maxIssues: 0

config:
  validation: true
  warningsAsErrors: false

processors:
  active: true
  exclude:
    - 'DetektProgressListener'

console-reports:
  active: true
  exclude:
    - 'ProjectStatisticsReport'
    - 'ComplexityReport'
    - 'NotificationReport'
    - 'FindingsReport'
    - 'FileBasedFindingsReport'

# 输出格式
output-reports:
  active: true
  exclude:
    - 'TxtOutputReport'
    - 'XmlOutputReport'
    - 'HtmlOutputReport'
    - 'MdOutputReport'
  include:
    - 'JsonOutputReport'
    - 'SarifOutputReport'

# 规则配置
compose:
  active: true
  CompositionLocalAllowlist:
    active: true
  CompositionLocalNaming:
    active: true
  ContentEmitterReturningValues:
    active: true
  ContentSlotReused:
    active: true
  Material2:
    active: false
  ModifierClickableOrder:
    active: true
  ModifierComposed:
    active: true
  ModifierMissing:
    active: true
    # Compose 函数必须包含 Modifier 参数
  ModifierNaming:
    active: true
  ModifierNotUsedAtRoot:
    active: true
  ModifierReused:
    active: true
  MutableParams:
    active: true
  MutableStateAutoboxing:
    active: true
  MutableStateParam:
    active: true
  ParameterNaming:
    active: true
  PreviewAnnotationNaming:
    active: true
  PreviewPublic:
    active: true
  RememberMissing:
    active: true
  UnstableCollections:
    active: true
  ViewModelForwarding:
    active: true
  ViewModelInjection:
    active: true

style:
  active: true
  MagicNumber:
    active: true
    ignoreNumbers:
      - '-1'
      - '0'
      - '1'
      - '2'
      - '0.0'
      - '1.0'
      - '1.0f'
    ignoreAnnotated:
      - 'Composable'
  MaxLineLength:
    active: true
    maxLineLength: 120
    excludePackageStatements: true
    excludeImportStatements: true
  NewLineAtEndOfFile:
    active: true
  WildcardImport:
    active: true
    excludeImports:
      - 'java.util.*'

complexity:
  active: true
  LongMethod:
    active: true
    threshold: 60
    ignoreAnnotated:
      - 'Composable'
  LongParameterList:
    active: true
    functionThreshold: 8
    constructorThreshold: 8
    ignoreDefaultParameters: true
    ignoreAnnotated:
      - 'Composable'
  TooManyFunctions:
    active: true
    thresholdInFiles: 11
    thresholdInClasses: 11
    thresholdInInterfaces: 11

.editorconfig

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 120

[*.{kt,kts}]
# ktlint 规则
ktlint_standard = enabled

# Compose 特定配置
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_property_naming_ignore_when_annotated_with = Composable

# 禁用某些规则(沙盒环境)
ktlint_standard_filename = disabled
ktlint_standard_package-name = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled

# 导入规范
ktlint_standard_no-wildcard-imports = disabled
ij_kotlin_packages_to_use_import_on_demand = unset
ij_kotlin_name_count_to_use_star_import = 99
ij_kotlin_name_count_to_use_star_import_for_members = 99

1.3 分析脚本

lint-analyzer.sh

#!/bin/bash
set -euo pipefail

# 配置
KTLINT_CMD="ktlint"
DETEKT_CMD="detekt"
CONFIG_DIR="/opt/config"
OUTPUT_DIR="${OUTPUT_DIR:-/tmp/lint-results}"
REPORT_FORMAT="${REPORT_FORMAT:-json}"

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# 帮助信息
show_help() {
    cat << EOF
Jetpack Compose Lint Analyzer for Sandbox Environment

Usage: lint-analyzer.sh [OPTIONS] [FILES...]

Options:
    -h, --help          Show this help message
    -f, --format        Auto-fix issues where possible
    -o, --output DIR    Output directory for reports (default: /tmp/lint-results)
    -r, --reporter FMT  Reporter format: json, sarif, html (default: json)
    --only-ktlint       Run only ktlint
    --only-detekt       Run only Detekt
    --stdin             Read from stdin instead of files
    --fail-on-error     Exit with error code if issues found

Examples:
    # Check all files in current directory
    lint-analyzer.sh

    # Check specific files
    lint-analyzer.sh src/Main.kt src/ui/Components.kt

    # Auto-fix and output to specific directory
    lint-analyzer.sh -f -o ./reports src/

    # Read from stdin (for IDE integration)
    cat Main.kt | lint-analyzer.sh --stdin

    # CI mode: fail on any issue
    lint-analyzer.sh --fail-on-error src/
EOF
}

# 解析参数
AUTO_FIX=false
USE_STDIN=false
FAIL_ON_ERROR=false
ONLY_KTLINT=false
ONLY_DETEKT=false
FILES=()

while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            show_help
            exit 0
            ;;
        -f|--format)
            AUTO_FIX=true
            shift
            ;;
        -o|--output)
            OUTPUT_DIR="$2"
            shift 2
            ;;
        -r|--reporter)
            REPORT_FORMAT="$2"
            shift 2
            ;;
        --stdin)
            USE_STDIN=true
            shift
            ;;
        --fail-on-error)
            FAIL_ON_ERROR=true
            shift
            ;;
        --only-ktlint)
            ONLY_KTLINT=true
            shift
            ;;
        --only-detekt)
            ONLY_DETEKT=true
            shift
            ;;
        -*)
            echo -e "${RED}Error: Unknown option $1${NC}"
            show_help
            exit 1
            ;;
        *)
            FILES+=("$1")
            shift
            ;;
    esac
done

# 创建输出目录
mkdir -p "$OUTPUT_DIR"

# 存储结果的总问题数
TOTAL_ISSUES=0

# 运行 ktlint
run_ktlint() {
    if [[ "$ONLY_DETEKT" == true ]]; then
        return 0
    fi

    echo -e "${YELLOW}▶ Running ktlint...${NC}"
    
    local ktlint_args="--editorconfig=$CONFIG_DIR/.editorconfig"
    local output_file="$OUTPUT_DIR/ktlint-result.json"
    
    if [[ "$AUTO_FIX" == true ]]; then
        ktlint_args="$ktlint_args --format"
    fi
    
    if [[ "$REPORT_FORMAT" == "json" ]]; then
        ktlint_args="$ktlint_args --reporter=json,output=$output_file"
    else
        ktlint_args="$ktlint_args --reporter=plain"
    fi
    
    if [[ "$USE_STDIN" == true ]]; then
        ktlint_args="$ktlint_args --stdin"
        if ! $KTLINT_CMD $ktlint_args < /dev/stdin; then
            TOTAL_ISSUES=$((TOTAL_ISSUES + 1))
        fi
    elif [[ ${#FILES[@]} -gt 0 ]]; then
        for file in "${FILES[@]}"; do
            if ! $KTLINT_CMD $ktlint_args "$file"; then
                TOTAL_ISSUES=$((TOTAL_ISSUES + 1))
            fi
        done
    else
        if ! $KTLINT_CMD $ktlint_args; then
            TOTAL_ISSUES=$((TOTAL_ISSUES + 1))
        fi
    fi
    
    echo -e "${GREEN}✓ ktlint completed${NC}"
}

# 运行 Detekt
run_detekt() {
    if [[ "$ONLY_KTLINT" == true ]]; then
        return 0
    fi
    
    echo -e "${YELLOW}▶ Running Detekt...${NC}"
    
    local detekt_args="--config $CONFIG_DIR/detekt.yml --analysis-mode light"
    local output_file="$OUTPUT_DIR/detekt-result.json"
    
    if [[ "$AUTO_FIX" == true ]]; then
        detekt_args="$detekt_args --auto-correct"
    fi
    
    case "$REPORT_FORMAT" in
        json)
            detekt_args="$detekt_args --report json:$output_file"
            ;;
        sarif)
            detekt_args="$detekt_args --report sarif:$OUTPUT_DIR/detekt-result.sarif"
            ;;
        html)
            detekt_args="$detekt_args --report html:$OUTPUT_DIR/detekt-result.html"
            ;;
    esac
    
    if [[ "$USE_STDIN" == true ]]; then
        # Detekt 不直接支持 stdin,需要临时文件
        local temp_file=$(mktemp).kt
        cat > "$temp_file"
        detekt_args="$detekt_args --input $temp_file"
        if ! $DETEKT_CMD $detekt_args; then
            TOTAL_ISSUES=$((TOTAL_ISSUES + 1))
        fi
        rm -f "$temp_file"
    elif [[ ${#FILES[@]} -gt 0 ]]; then
        local input_paths=$(IFS=,; echo "${FILES[*]}")
        detekt_args="$detekt_args --input $input_paths"
        if ! $DETEKT_CMD $detekt_args 2>&1; then
            TOTAL_ISSUES=$((TOTAL_ISSUES + 1))
        fi
    else
        detekt_args="$detekt_args --input ."
        if ! $DETEKT_CMD $detekt_args 2>&1; then
            TOTAL_ISSUES=$((TOTAL_ISSUES + 1))
        fi
    fi
    
    echo -e "${GREEN}✓ Detekt completed${NC}"
}

# 合并报告
merge_reports() {
    echo -e "${YELLOW}▶ Merging reports...${NC}"
    
    local merged_file="$OUTPUT_DIR/merged-report.json"
    
    # 简单的报告合并(实际项目中可能需要更复杂的逻辑)
    cat > "$merged_file" << EOF
{
    "timestamp": "$(date -Iseconds)",
    "summary": {
        "totalIssues": $TOTAL_ISSUES
    },
    "reports": {
        "ktlint": "ktlint-result.json",
        "detekt": "detekt-result.json"
    }
}
EOF
    
    echo -e "${GREEN}✓ Reports saved to $OUTPUT_DIR${NC}"
}

# 主流程
main() {
    echo -e "${YELLOW}═══════════════════════════════════════${NC}"
    echo -e "${YELLOW}  Jetpack Compose Lint Analyzer${NC}"
    echo -e "${YELLOW}═══════════════════════════════════════${NC}"
    echo ""
    
    run_ktlint
    echo ""
    run_detekt
    echo ""
    merge_reports
    
    echo ""
    echo -e "${YELLOW}═══════════════════════════════════════${NC}"
    if [[ $TOTAL_ISSUES -eq 0 ]]; then
        echo -e "${GREEN}✓ All checks passed!${NC}"
    else
        echo -e "${RED}✗ Found $TOTAL_ISSUES issue(s)${NC}"
    fi
    echo -e "${YELLOW}═══════════════════════════════════════${NC}"
    
    if [[ "$FAIL_ON_ERROR" == true && $TOTAL_ISSUES -gt 0 ]]; then
        exit 1
    fi
    
    exit 0
}

main "$@"

1.4 构建与测试

# 构建镜像
docker build -f Dockerfile.lint-sandbox -t compose-lint-sandbox:latest .

# 测试运行
docker run --rm -v $(pwd):/workspace compose-lint-sandbox:latest --help

# 分析当前目录
docker run --rm -v $(pwd):/workspace compose-lint-sandbox:latest -o ./reports

# CI 模式(发现问题时退出码非零)
docker run --rm -v $(pwd):/workspace compose-lint-sandbox:latest --fail-on-error

2. Harness 集成

2.1 Harness Pipeline 配置

# harness-lint-pipeline.yml
pipeline:
  name: compose-lint-check
  identifier: composeLintCheck
  projectIdentifier: myProject
  orgIdentifier: myOrg
  tags: {}
  stages:
    - stage:
        name: Lint Check
        identifier: lintCheck
        description: Run ktlint and Detekt in sandbox
        type: CI
        spec:
          cloneCodebase: true
          infrastructure:
            type: KubernetesDirect
            spec:
              connectorRef: k8sConnector
              namespace: ci
              automountServiceAccountToken: true
              nodeSelector: {}
              os: Linux
          execution:
            steps:
              - step:
                  type: Run
                  name: Lint Analysis
                  identifier: lintAnalysis
                  spec:
                    connectorRef: dockerHub
                    image: compose-lint-sandbox:latest
                    shell: Sh
                    command: |
                      # 运行 lint 分析
                      lint-analyzer.sh \
                        --output /shared/reports \
                        --reporter sarif \
                        --fail-on-error \
                        src/
                    resources:
                      limits:
                        memory: 512Mi
                        cpu: 1000m
                    failureStrategies:
                      - onFailure:
                          errors:
                            - AllErrors
                          action:
                            type: MarkAsFailure
              - step:
                  type: Run
                  name: Upload Reports
                  identifier: uploadReports
                  spec:
                    connectorRef: dockerHub
                    image: alpine:latest
                    shell: Sh
                    command: |
                      # 上传报告到 Harness 或外部存储
                      echo "Uploading SARIF report..."
                      cat /shared/reports/detekt-result.sarif
                    resources:
                      limits:
                        memory: 128Mi
                        cpu: 100m
        delegateSelectors:
          - docker-delegate

2.2 GitHub Actions 集成

# .github/workflows/compose-lint.yml
name: Compose Lint Check

on:
  push:
    branches: [ main, develop ]
    paths:
      - '**.kt'
      - '**.kts'
  pull_request:
    branches: [ main ]
    paths:
      - '**.kt'
      - '**.kts'

jobs:
  lint:
    runs-on: ubuntu-latest
    container:
      image: compose-lint-sandbox:latest
      options: --memory=512m
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Run Lint Analysis
        run: |
          lint-analyzer.sh \
            --output ./reports \
            --reporter sarif \
            --fail-on-error \
            src/
      
      - name: Upload SARIF Report
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: ./reports/detekt-result.sarif
          category: compose-lint
      
      - name: Upload Artifacts
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: lint-reports
          path: ./reports/

3. IDE 集成

3.1 VS Code 配置

.vscode/settings.json

{
  "kotlin.formatting.enabled": false,
  "kotlin.linting.enabled": false,
  "editor.formatOnSave": false,
  
  // 自定义任务:调用沙盒 lint
  "tasks": {
    "version": "2.0.0",
    "tasks": [
      {
        "label": "Lint Current File (Sandbox)",
        "type": "shell",
        "command": "docker",
        "args": [
          "run",
          "--rm",
          "-v", "${workspaceFolder}:/workspace",
          "-w", "/workspace",
          "compose-lint-sandbox:latest",
          "--stdin"
        ],
        "group": "build",
        "presentation": {
          "reveal": "always",
          "panel": "new"
        },
        "problemMatcher": {
          "pattern": {
            "regexp": "^(.*):(\\d+):(\\d+): (.*)$",
            "file": 1,
            "line": 2,
            "column": 3,
            "message": 4
          }
        }
      }
    ]
  }
}

3.2 Git Hook 集成

.githooks/pre-commit

#!/bin/bash
# Git pre-commit hook for sandbox lint

# 获取暂存区的 Kotlin 文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(kt|kts)$' || true)

if [[ -z "$STAGED_FILES" ]]; then
    echo "No Kotlin files to check."
    exit 0
fi

echo "Running sandbox lint on staged files..."

# 使用 Docker 运行 lint
docker run --rm \
    -v "$(pwd):/workspace" \
    -w /workspace \
    compose-lint-sandbox:latest \
    --fail-on-error \
    $STAGED_FILES

if [[ $? -ne 0 ]]; then
    echo ""
    echo "Lint check failed. Please fix the issues before committing."
    echo "You can auto-fix some issues with: docker run --rm -v \$(pwd):/workspace compose-lint-sandbox:latest -f"
    exit 1
fi

echo "✓ Lint check passed!"
exit 0

启用 hook:

# 配置 Git 使用项目自定义 hooks
git config core.hooksPath .githooks

# 确保 hook 可执行
chmod +x .githooks/pre-commit

4. 性能优化

4.1 增量分析策略

#!/bin/bash
# incremental-lint.sh

# 获取变更文件列表
CHANGED_FILES=$(git diff --name-only HEAD~1 | grep -E '\.(kt|kts)$' || true)

if [[ -z "$CHANGED_FILES" ]]; then
    echo "No Kotlin files changed."
    exit 0
fi

# 仅分析变更文件
echo "Analyzing changed files: $CHANGED_FILES"
docker run --rm \
    -v "$(pwd):/workspace" \
    compose-lint-sandbox:latest \
    --fail-on-error \
    $CHANGED_FILES

4.2 并行分析

#!/bin/bash
# parallel-lint.sh

# 分割文件列表并行处理
find src -name "*.kt" -type f | xargs -P 4 -I {} \
    docker run --rm \
    -v "$(pwd):/workspace" \
    compose-lint-sandbox:latest \
    {}

5. 故障排查

5.1 常见问题

问题原因解决方案
OOM 错误内存限制过低增加容器内存至 512MB+
权限错误文件挂载权限使用 --user $(id -u):$(id -g)
配置未生效配置文件路径错误检查 /opt/config/detekt.yml
规则缺失analysis-mode 为 light使用 full 模式或禁用相关规则

5.2 调试模式

# 启用详细日志
docker run --rm \
    -v "$(pwd):/workspace" \
    -e DEBUG=1 \
    compose-lint-sandbox:latest \
    src/

# 进入容器手动调试
docker run --rm -it \
    -v "$(pwd):/workspace" \
    --entrypoint /bin/sh \
    compose-lint-sandbox:latest

本指南提供了从 Docker 配置到 CI/CD 集成的完整实施路径,可根据实际需求调整配置参数。