实施指南
实施指南 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 集成的完整实施路径,可根据实际需求调整配置参数。