build-flow.api/scripts/backend-optimization-sop.sh

411 lines
16 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
SOLUTION="$REPO_ROOT/BuildFlow.slnx"
COMPOSE_FILE="$SCRIPT_DIR/backend-optimization-compose.yml"
RUN_TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ)
RUN_ID="${RUN_TIMESTAMP}-$$"
ARTIFACT_ROOT=${BACKEND_SOP_ARTIFACT_ROOT:-"$REPO_ROOT/artifacts/backend-optimization-sop"}
ARTIFACT_DIR="$ARTIFACT_ROOT/$RUN_ID"
LOG_DIR="$ARTIFACT_DIR/logs"
TRX_DIR="$ARTIFACT_DIR/trx"
SUMMARY_FILE="$ARTIFACT_DIR/summary.md"
STAGE_FILE="$ARTIFACT_DIR/stages.tsv"
ENV_FILE="$ARTIFACT_DIR/infrastructure.env"
SUCCESS_FILE="$ARTIFACT_DIR/SUCCESS"
COMPOSE_PROJECT="buildflow-sop-$(date -u +%Y%m%d%H%M%S)-$$"
CURRENT_STAGE="not-started"
CURRENT_LOG=""
INFRA_STARTED=0
RUN_COMPLETED=0
mkdir -p "$LOG_DIR" "$TRX_DIR"
printf 'stage\tstarted_utc\tended_utc\tduration_seconds\texit_status\tlog\tcommand\n' > "$STAGE_FILE"
run_logged() {
printf '+ '
printf '%q ' "$@"
printf '\n'
"$@"
}
run_stage() {
local stage=$1
shift
local log="$LOG_DIR/$stage.log"
local started_at
local ended_at
local started_epoch
local ended_epoch
local status
CURRENT_STAGE=$stage
CURRENT_LOG=$log
started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
started_epoch=$(date +%s)
{
printf 'stage=%s\n' "$stage"
printf 'started_utc=%s\n' "$started_at"
printf 'command='
printf '%q ' "$@"
printf '\n'
} | tee "$log"
set +e
(
set -e
cd "$REPO_ROOT"
"$@"
) 2>&1 | tee -a "$log"
status=${PIPESTATUS[0]}
set -e
ended_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
ended_epoch=$(date +%s)
printf 'ended_utc=%s\nduration_seconds=%s\nexit_status=%s\n' \
"$ended_at" "$((ended_epoch - started_epoch))" "$status" | tee -a "$log"
printf '%s\t%s\t%s\t%s\t%s\t%s\t' \
"$stage" "$started_at" "$ended_at" "$((ended_epoch - started_epoch))" \
"$status" "$log" >> "$STAGE_FILE"
printf '%q ' "$@" >> "$STAGE_FILE"
printf '\n' >> "$STAGE_FILE"
return "$status"
}
require_command() {
local name=$1
command -v "$name" >/dev/null 2>&1 || {
printf 'Required command is missing: %s\n' "$name" >&2
return 1
}
}
assert_clean_repository() {
local status
status=$(git status --porcelain --untracked-files=normal)
if [[ -n "$status" ]]; then
printf 'Repository must be clean before running the closure SOP:\n%s\n' "$status" >&2
return 1
fi
}
assert_no_matches() {
local description=$1
local pattern=$2
shift 2
local status
printf 'scan=%s pattern=%s\n' "$description" "$pattern"
set +e
rg -n --pcre2 -U "$pattern" "$@"
status=$?
set -e
if [[ $status -eq 0 ]]; then
printf 'Forbidden source debt found: %s\n' "$description" >&2
return 1
fi
if [[ $status -ne 1 ]]; then
printf 'Static scan failed: %s (rg exit %s)\n' "$description" "$status" >&2
return "$status"
fi
}
extract_counter() {
local trx=$1
local counter=$2
local value
value=$(rg -o "${counter}=\"[0-9]+\"" "$trx" | head -n 1 | tr -dc '0-9' || true)
printf '%s' "${value:-0}"
}
collect_sop_metrics() {
rg -o --no-filename \
-g '*.log' \
-g '*.trx' \
'SOP_METRIC [A-Za-z0-9_]+=[0-9]+' \
"$LOG_DIR" "$TRX_DIR" 2>/dev/null | sort -u
}
assert_no_skipped_tests() {
local target=$1
local found=0
local skipped=0
local trx
while IFS= read -r trx; do
found=1
skipped=$((skipped + $(extract_counter "$trx" notExecuted)))
done < <(find "$target" -type f -name '*.trx' -print | sort)
if [[ $found -eq 0 ]]; then
printf 'No TRX result was produced under %s\n' "$target" >&2
return 1
fi
if [[ $skipped -gt 0 ]]; then
printf 'Skipped tests are forbidden: skipped=%s target=%s\n' "$skipped" "$target" >&2
return 1
fi
printf 'skipped=0 target=%s\n' "$target"
}
run_test() {
local label=$1
local project=$2
local filter=${3:-}
local args
args=(
dotnet test "$project"
--no-restore
--no-build
--logger "trx;LogFileName=${label}.trx"
--results-directory "$TRX_DIR"
--logger "console;verbosity=normal"
)
if [[ -n "$filter" ]]; then
args+=(--filter "$filter")
fi
run_logged "${args[@]}"
assert_no_skipped_tests "$TRX_DIR/${label}.trx"
}
load_infrastructure_env() {
if [[ ! -f "$ENV_FILE" ]]; then
printf 'Infrastructure environment file is missing: %s\n' "$ENV_FILE" >&2
return 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
}
compose_port() {
local service=$1
local container_port=$2
local endpoint
printf '+ docker compose -p %q -f %q port %q %q\n' \
"$COMPOSE_PROJECT" "$COMPOSE_FILE" "$service" "$container_port" >&2
endpoint=$(docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" port "$service" "$container_port")
printf 'endpoint=%s\n' "$endpoint" >&2
printf '%s' "${endpoint##*:}"
}
stage_preflight() {
local command
for command in dotnet docker git rg curl; do
require_command "$command"
done
run_logged dotnet --version
[[ $(dotnet --version) == 10.* ]] || {
printf '.NET SDK 10 is required.\n' >&2
return 1
}
run_logged docker --version
run_logged docker compose version
run_logged docker version --format '{{.Server.Version}}'
run_logged git --version
run_logged rg --version
run_logged curl --version
run_logged git rev-parse --show-toplevel
assert_clean_repository
}
stage_static_guards() {
local fixed_password_pattern='Password\s*=\s*(?:(?!REPLACE_IN_SECRETS|YOUR_[A-Z_]+)[A-Za-z0-9_-]{3,}(?:;|")|config\[[^]]+\]\s*\?\?\s*"[^"]+")'
local one_argument_exception_pattern='new\s+(?:[A-Za-z_][A-Za-z0-9_:]*\.)*(?:BusinessException|NotFoundException|UnauthorizedException)\s*\(\s*"'
local endpoint_dbcontext_pattern='class\s+[A-Za-z0-9_]+Endpoint\s*\([^)]*[A-Za-z0-9_]+DbContext\b'
# 通配排除 *Secrets*.json:本地机密文件已 gitignore、绝不提交;
# 本机部分 rg 构建不尊重 vcs ignore(连 --ignore-file 也不生效),需 glob 硬排除
assert_no_matches 'fixed password' "$fixed_password_pattern" src -g '*.cs' -g '*.json' -g '!*.Secrets.json'
assert_no_matches 'one-argument business exception' "$one_argument_exception_pattern" src -g '*.cs'
assert_no_matches 'Endpoint DbContext dependency' "$endpoint_dbcontext_pattern" src/BuildFlow.Host/Endpoints -g '*.cs'
assert_no_matches 'InProcess bridge' 'InProcess(UserInfoService|FileStorageClient|AvatarUrlResolver)' src -g '*.cs'
assert_no_matches 'local wall-clock usage' 'DateTime\.Now' src -g '*.cs'
assert_no_matches 'detached workflow work' 'Task\.Run' src/modules/workflow src/BuildFlow.Host -g '*.cs'
}
stage_restore_build() {
run_logged dotnet restore "$SOLUTION"
run_logged dotnet build "$SOLUTION" --no-restore -warnaserror
}
stage_unit_tests() {
local source_guard_filter='FullyQualifiedName~EndpointConstructorsShouldNotDependOnDbContexts|FullyQualifiedName~HostShouldNotContainInProcessBridges|FullyQualifiedName~BusinessExceptionsShouldSpecifySemanticErrorCodes|FullyQualifiedName~SourceFilesShouldContainAtMostOneTopLevelClassRecordOrEnum'
local workflow_filter='FullyQualifiedName!~BuildFlow.Workflow.Tests.Reliability&FullyQualifiedName!~OpenXmlExportBoundaryTests&FullyQualifiedName!~FormAggregateQueryCountTests&FullyQualifiedName!~ImportJobBatchingTests'
run_test 04-source-architecture tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj "$source_guard_filter"
run_test 04-host-architecture tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj 'FullyQualifiedName~HostArchitectureTests'
run_test 04-sop-contract tests/BuildFlow.Architecture.Tests/BuildFlow.Architecture.Tests.csproj 'FullyQualifiedName~BackendOptimizationSopTests'
run_test 04-host tests/BuildFlow.Host.Tests/BuildFlow.Host.Tests.csproj
run_test 04-platform tests/BuildFlow.Platform.Tests/BuildFlow.Platform.Tests.csproj
run_test 04-scripting tests/BuildFlow.Scripting.Tests/BuildFlow.Scripting.Tests.csproj
run_test 04-file tests/BuildFlow.File.Tests/BuildFlow.File.Tests.csproj 'Category!=External'
run_test 04-rag tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName!~RedisChatMessageCacheTests'
run_test 04-im tests/BuildFlow.IM.Tests/BuildFlow.IM.Tests.csproj 'FullyQualifiedName!~SendMessageCommandHandlerTests'
run_test 04-workflow tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj "$workflow_filter"
}
stage_start_infrastructure() {
local postgres_port
local redis_port
local mongo_port
local minio_port
run_logged docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" up -d --wait
postgres_port=$(compose_port postgres 5432)
redis_port=$(compose_port redis 6379)
mongo_port=$(compose_port mongo 27017)
minio_port=$(compose_port minio 9000)
{
printf 'export BUILDFLOW_WORKFLOW_TEST_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=postgres"
printf 'export BUILDFLOW_MONITORING_TEST_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=postgres"
printf 'export BUILDFLOW_RAG_TEST_POSTGRES_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=postgres"
printf 'export BUILDFLOW_RAG_REDIS_TEST_CONNECTION=%q\n' "127.0.0.1:$redis_port,abortConnect=false"
printf 'export BUILDFLOW_IM_TEST_POSTGRES_CONNECTION=%q\n' "Host=127.0.0.1;Port=$postgres_port;Username=buildflow;Password=buildflow;Database=im_system_test"
printf 'export BUILDFLOW_IM_TEST_MONGO_CONNECTION=%q\n' "mongodb://buildflow:buildflow@127.0.0.1:$mongo_port/im_system_test?authSource=admin"
printf 'export BUILDFLOW_IM_TEST_REDIS_CONNECTION=%q\n' "127.0.0.1:$redis_port,abortConnect=false"
printf 'export BUILDFLOW_FILE_S3_TEST_ENDPOINT=%q\n' "http://127.0.0.1:$minio_port"
printf 'export BUILDFLOW_FILE_S3_TEST_ACCESS_KEY=%q\n' buildflowminio
printf 'export BUILDFLOW_FILE_S3_TEST_SECRET_KEY=%q\n' buildflowminiosecret
} > "$ENV_FILE"
sed -E 's/(SECRET_KEY=).*/\1<redacted>/' "$ENV_FILE"
}
stage_database_migrations() {
load_infrastructure_env
run_test 06-workflow-migrations tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj 'FullyQualifiedName~WorkflowJobMigrationTests'
}
stage_integration_tests() {
load_infrastructure_env
run_test 07-monitoring-upsert tests/BuildFlow.Monitoring.Tests/BuildFlow.Monitoring.Tests.csproj
run_test 07-rag-redis tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~RedisChatMessageCacheTests'
BUILDFLOW_IM_TEST_MONGO_CONNECTION=${BUILDFLOW_IM_TEST_MONGO_CONNECTION/im_system_test/im_unit_test} \
run_test 07-im-mongo tests/BuildFlow.IM.Tests/BuildFlow.IM.Tests.csproj 'FullyQualifiedName~SendMessageCommandHandlerTests'
run_test 07-im-signalr tests/BuildFlow.IM.Integration.Tests/BuildFlow.IM.Integration.Tests.csproj
run_test 07-file-s3 tests/BuildFlow.File.Tests/BuildFlow.File.Tests.csproj 'FullyQualifiedName~S3StorageServiceIntegrationTests'
run_test 07-rag-stream tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~RagStreamQueryHandlerTests'
run_test 07-rag-pgvector tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~RagPgVectorIntegrationTests'
run_test 07-host-sse tests/BuildFlow.Host.Tests/BuildFlow.Host.Tests.csproj 'FullyQualifiedName~RagStreamEndpointTests'
}
stage_recovery_tests() {
load_infrastructure_env
run_test 08-workflow-recovery tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj 'FullyQualifiedName~WorkflowTimerProcessorTests|FullyQualifiedName~ImportJobClaimTests|FullyQualifiedName~ImportJobRecoveryTests'
}
stage_performance_tests() {
load_infrastructure_env
run_test 09-workflow-performance tests/BuildFlow.Workflow.Tests/BuildFlow.Workflow.Tests.csproj 'FullyQualifiedName~FormAggregateQueryCountTests|FullyQualifiedName~ImportJobBatchingTests|FullyQualifiedName~OpenXmlMemoryProfileTests|FullyQualifiedName~OpenXmlExportBoundaryTests'
run_test 09-rag-performance tests/BuildFlow.Rag.Tests/BuildFlow.Rag.Tests.csproj 'FullyQualifiedName~ChatContextWindowBuilderTests|FullyQualifiedName~RedisChatMessageCacheTests'
run_test 09-file-performance tests/BuildFlow.File.Tests/BuildFlow.File.Tests.csproj 'FullyQualifiedName~FileSizeVerificationProcessorTests'
}
stage_full_solution() {
load_infrastructure_env
run_logged dotnet test "$SOLUTION" \
--no-restore \
--no-build \
--logger 'trx;LogFilePrefix=10-full-solution' \
--results-directory "$TRX_DIR/10-full-solution" \
--logger 'console;verbosity=minimal'
assert_no_skipped_tests "$TRX_DIR/10-full-solution"
}
write_summary() {
local status=$1
local total=0
local passed=0
local failed=0
local skipped=0
local trx
local failure_log='none'
while IFS= read -r trx; do
total=$((total + $(extract_counter "$trx" total)))
passed=$((passed + $(extract_counter "$trx" passed)))
failed=$((failed + $(extract_counter "$trx" failed)))
skipped=$((skipped + $(extract_counter "$trx" notExecuted)))
done < <(find "$TRX_DIR" -type f -name '*.trx' -print 2>/dev/null | sort)
if [[ "$status" != success ]]; then
failure_log=${CURRENT_LOG:-none}
fi
{
printf '# Backend Optimization SOP Report\n\n'
printf -- '- Run: `%s`\n' "$RUN_ID"
printf -- '- Status: `%s`\n' "$status"
printf -- '- Compose project: `%s`\n' "$COMPOSE_PROJECT"
printf -- '- Tests: total `%s`, passed `%s`, failed `%s`, skipped `%s`\n' "$total" "$passed" "$failed" "$skipped"
printf -- '- Failure log: `%s`\n\n' "$failure_log"
printf '## Stages\n\n'
printf '| Stage | Started UTC | Ended UTC | Seconds | Exit | Log | Command |\n'
printf '|---|---|---|---:|---:|---|---|\n'
tail -n +2 "$STAGE_FILE" | while IFS=$'\t' read -r stage started ended duration exit_code log command; do
printf '| `%s` | `%s` | `%s` | %s | %s | `%s` | `%s` |\n' \
"$stage" "$started" "$ended" "$duration" "$exit_code" "$log" "$command"
done
printf '\n## Metrics\n\n'
if ! collect_sop_metrics; then
printf 'No SOP_METRIC lines were produced.\n'
fi
printf '\n## Evidence\n\n'
printf -- '- Stage ledger: `%s`\n' "$STAGE_FILE"
printf -- '- Logs: `%s`\n' "$LOG_DIR"
printf -- '- TRX: `%s`\n' "$TRX_DIR"
} > "$SUMMARY_FILE"
}
cleanup() {
local status=$?
local cleanup_status=0
trap - EXIT
set +e
if [[ $INFRA_STARTED -eq 1 ]]; then
docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE" down -v \
>> "$ARTIFACT_DIR/cleanup.log" 2>&1
cleanup_status=$?
fi
if [[ $status -eq 0 && $cleanup_status -ne 0 ]]; then
status=$cleanup_status
CURRENT_STAGE=cleanup
CURRENT_LOG="$ARTIFACT_DIR/cleanup.log"
fi
if [[ $status -ne 0 || $RUN_COMPLETED -ne 1 ]]; then
rm -f "$SUCCESS_FILE"
write_summary failure
printf 'Backend optimization SOP failed in stage %s. Report: %s\n' \
"$CURRENT_STAGE" "$SUMMARY_FILE" >&2
fi
exit "$status"
}
trap cleanup EXIT
run_stage '01-preflight' stage_preflight
run_stage '02-static-guards' stage_static_guards
run_stage '03-restore-build' stage_restore_build
run_stage '04-unit-tests' stage_unit_tests
INFRA_STARTED=1
run_stage '05-start-infrastructure' stage_start_infrastructure
run_stage '06-database-migrations' stage_database_migrations
run_stage '07-integration-tests' stage_integration_tests
run_stage '08-recovery-tests' stage_recovery_tests
run_stage '09-performance-tests' stage_performance_tests
run_stage '10-full-solution' stage_full_solution
assert_no_skipped_tests "$TRX_DIR"
write_summary success
printf 'success\n' > "$SUCCESS_FILE"
RUN_COMPLETED=1
printf 'Backend optimization SOP completed. Report: %s\n' "$SUMMARY_FILE"