feat: add CI/CD workflows and ops scripts for GHCR → Harbor → k3s pipeline

- Gate validation: all 143 unit tests pass (80.38% coverage), 11 integration tests pass
- Fix ANTHROPIC_MODEL default: claude-sonnet-4-6-20260217 → claude-sonnet-4-6
- Fix docker-compose DATABASE_URL/REDIS_URL: remove nested var expansion
- health_check.sh: auto-source .env for local runs, capture claude inference log
- .github/workflows/ci.yml: lint (ruff), type-check (mypy), unit + integration tests
- .github/workflows/build-and-notify.yml: build Python image → GHCR, notify sync-service
- scripts/ops/apply-sync-autopr.sh: configure sync-service auto-PR to agent-deploy
- scripts/ops/check-sync-autopr.sh: verify sync-service + ArgoCD app status

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
BrunoGaoSZ
2026-02-28 17:35:02 +01:00
co-authored by Claude Sonnet 4.6
parent 2360e788d3
commit 87cd4b3975
8 changed files with 344 additions and 6 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# LLM
ANTHROPIC_AUTH_TOKEN=sk-ant-CHANGE_ME
ANTHROPIC_BASE_URL=https://api.anthropic.com
ANTHROPIC_MODEL=claude-sonnet-4-6-20260217
ANTHROPIC_MODEL=claude-sonnet-4-6
ANTHROPIC_TIMEOUT_CONNECT=10.0
ANTHROPIC_TIMEOUT_READ=60.0
LOCAL_MODEL_ENDPOINT=http://localhost:11434/v1
+150
View File
@@ -0,0 +1,150 @@
name: build-and-notify
on:
push:
branches:
- master
- main
workflow_dispatch:
permissions:
contents: read
packages: write
env:
GHCR_REGISTRY: ghcr.io
jobs:
build-api:
name: Build API to GHCR
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
image: ${{ steps.meta.outputs.image }}
digest: ${{ steps.push.outputs.digest }}
short_sha: ${{ steps.meta.outputs.short_sha }}
branch: ${{ steps.meta.outputs.branch }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve metadata
id: meta
shell: bash
run: |
set -euo pipefail
SHORT_SHA="${GITHUB_SHA::7}"
BRANCH_NAME="${GITHUB_REF_NAME}"
REPOSITORY_LOWER="$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')"
IMAGE="${GHCR_REGISTRY}/${REPOSITORY_LOWER}/agent-platform-api"
{
echo "short_sha=${SHORT_SHA}"
echo "branch=${BRANCH_NAME}"
echo "image=${IMAGE}"
echo "sha_tag=sha-${SHORT_SHA}"
echo "branch_tag=branch-${BRANCH_NAME}"
} >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push API image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.api
target: runtime
push: true
tags: |
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha_tag }}
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.branch_tag }}
cache-from: type=gha,scope=api-ghcr
cache-to: type=gha,scope=api-ghcr,mode=max
notify-sync-service:
name: Notify Sync Service
runs-on: ubuntu-latest
needs:
- build-api
timeout-minutes: 10
steps:
- name: Send webhook event
env:
SYNC_WEBHOOK_URL: ${{ secrets.SYNC_WEBHOOK_URL }}
SYNC_WEBHOOK_SECRET: ${{ secrets.SYNC_WEBHOOK_SECRET }}
SYNC_BEARER_TOKEN: ${{ secrets.SYNC_BEARER_TOKEN }}
API_IMAGE: ${{ needs.build-api.outputs.image }}
API_DIGEST: ${{ needs.build-api.outputs.digest }}
API_SHORT_SHA: ${{ needs.build-api.outputs.short_sha }}
API_BRANCH: ${{ needs.build-api.outputs.branch }}
shell: bash
run: |
set -euo pipefail
if [ -z "${SYNC_WEBHOOK_URL:-}" ] || [ -z "${SYNC_WEBHOOK_SECRET:-}" ] || [ -z "${SYNC_BEARER_TOKEN:-}" ]; then
echo "SYNC webhook secrets are required." >&2
exit 1
fi
send_event() {
local component="$1"
local image="$2"
local digest="$3"
local short_sha="$4"
local branch="$5"
local event_id="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${component}-${GITHUB_SHA}"
local triggered_at
triggered_at="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
local payload
payload="$(jq -nc \
--arg event_id "${event_id}" \
--arg repository "${GITHUB_REPOSITORY}" \
--arg image "${image}" \
--arg git_sha "${GITHUB_SHA}" \
--arg short_sha "${short_sha}" \
--arg branch "${branch}" \
--arg digest "${digest}" \
--arg triggered_at "${triggered_at}" \
--arg tag1 "sha-${short_sha}" \
--arg tag2 "branch-${branch}" \
--arg component "${component}" \
'{
event_id: $event_id,
repository: $repository,
image: $image,
git_sha: $git_sha,
short_sha: $short_sha,
branch: $branch,
digest: $digest,
tags: [$tag1, $tag2],
component: $component,
triggered_at: $triggered_at
}')"
local timestamp
timestamp="$(date +%s)"
local signature
signature="$(printf '%s.%s' "${timestamp}" "${payload}" \
| openssl dgst -sha256 -hmac "${SYNC_WEBHOOK_SECRET}" -binary \
| xxd -p -c 256)"
curl -fsS -X POST "${SYNC_WEBHOOK_URL}/sync" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${SYNC_BEARER_TOKEN}" \
-H "X-Sync-Timestamp: ${timestamp}" \
-H "X-Sync-Signature: sha256=${signature}" \
-d "${payload}"
}
send_event "api" "${API_IMAGE}" "${API_DIGEST}" "${API_SHORT_SHA}" "${API_BRANCH}"
+83
View File
@@ -0,0 +1,83 @@
name: CI
on:
pull_request:
branches: [master, main]
push:
branches: [master, main]
permissions:
contents: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
run: pip install --no-cache-dir uv
- name: Install dependencies
run: uv sync --frozen
- name: Ruff lint
run: uv run ruff check . --select E,F,I --quiet
- name: Mypy type check
run: uv run mypy core/ api/ --ignore-missing-imports --no-error-summary
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
run: pip install --no-cache-dir uv
- name: Install dependencies
run: uv sync --frozen
- name: Run unit tests
run: uv run pytest tests/unit/ -x -q --tb=short --cov=core --cov=api --cov-fail-under=80
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
run: pip install --no-cache-dir uv
- name: Install dependencies
run: uv sync --frozen
- name: Run integration tests
run: uv run pytest tests/integration/ -x -q --tb=short -m "not claude_api" --timeout=60
+2 -2
View File
@@ -144,8 +144,8 @@ services:
env_file:
- .env
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://agent:${POSTGRES_PASSWORD:-CHANGE_ME}@postgres:5432/agent_platform}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
DATABASE_URL: "postgresql+asyncpg://agent:${POSTGRES_PASSWORD:-CHANGE_ME}@postgres:5432/agent_platform"
REDIS_URL: "redis://redis:6379/0"
MILVUS_HOST: milvus
MILVUS_PORT: 19530
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
+2 -2
View File
@@ -14,8 +14,8 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"--model",
default=os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6-20260217"),
help="Model name to use (default: ANTHROPIC_MODEL env or claude-sonnet-4-6-20260217).",
default=os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6"),
help="Model name to use (default: ANTHROPIC_MODEL env or claude-sonnet-4-6).",
)
parser.add_argument(
"--base-url",
+10 -1
View File
@@ -1,6 +1,14 @@
#!/bin/bash
set -euo pipefail
# Load project env vars for local runs (e.g. `make health`) without manual `source .env`.
if [ -f ".env" ]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
PASS=0
TOTAL=6
@@ -38,10 +46,11 @@ else
print_fail "API /health: curl -sf http://localhost:8000/health"
fi
if uv run python scripts/check_claude_inference.py >/dev/null 2>&1; then
if uv run python scripts/check_claude_inference.py >/tmp/claude_inference_check.log 2>&1; then
print_ok "Claude API: messages.create inference check"
else
print_fail "Claude API: messages.create inference check"
sed -n '1,5p' /tmp/claude_inference_check.log
fi
if curl -sf "http://localhost:13133/" >/dev/null 2>&1 || \
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
# Required
: "${GITHUB_TOKEN:?请先设置 GITHUB_TOKEN}"
# Optional with defaults
NAMESPACE="${NAMESPACE:-infra}"
DEPLOYMENT_NAME="${DEPLOYMENT_NAME:-sync-service}"
SECRET_NAME="${SECRET_NAME:-sync-service-env}"
DEPLOY_AUTOPR_ENABLED="${DEPLOY_AUTOPR_ENABLED:-true}"
DEPLOY_AUTOPR_BRANCH_PREFIX="${DEPLOY_AUTOPR_BRANCH_PREFIX:-sync}"
DEPLOY_AUTOPR_CLOSE_SUPERSEDED_PRS="${DEPLOY_AUTOPR_CLOSE_SUPERSEDED_PRS:-true}"
DEPLOY_AUTOPR_AUTO_MERGE_ENABLED="${DEPLOY_AUTOPR_AUTO_MERGE_ENABLED:-false}"
DEPLOY_AUTOPR_AUTO_MERGE_METHOD="${DEPLOY_AUTOPR_AUTO_MERGE_METHOD:-squash}"
DEPLOY_REPO_OWNER="${DEPLOY_REPO_OWNER:-BrunoGao}"
DEPLOY_REPO_NAME="${DEPLOY_REPO_NAME:-agent-deploy}"
DEPLOY_REPO_BASE_BRANCH="${DEPLOY_REPO_BASE_BRANCH:-main}"
DEPLOY_REPO_FILE_PATH="${DEPLOY_REPO_FILE_PATH:-apps/agent-platform/overlays/prod/kustomization.yaml}"
echo "[信息] 写入 ${NAMESPACE}/${SECRET_NAME} 自动发布配置..."
kubectl patch secret "${SECRET_NAME}" -n "${NAMESPACE}" --type merge -p "$(
jq -nc \
--arg enabled "${DEPLOY_AUTOPR_ENABLED}" \
--arg token "${GITHUB_TOKEN}" \
--arg prefix "${DEPLOY_AUTOPR_BRANCH_PREFIX}" \
--arg close_old "${DEPLOY_AUTOPR_CLOSE_SUPERSEDED_PRS}" \
--arg merge_enabled "${DEPLOY_AUTOPR_AUTO_MERGE_ENABLED}" \
--arg merge_method "${DEPLOY_AUTOPR_AUTO_MERGE_METHOD}" \
--arg owner "${DEPLOY_REPO_OWNER}" \
--arg repo "${DEPLOY_REPO_NAME}" \
--arg base_branch "${DEPLOY_REPO_BASE_BRANCH}" \
--arg file_path "${DEPLOY_REPO_FILE_PATH}" \
'{
stringData: {
DEPLOY_AUTOPR_ENABLED: $enabled,
DEPLOY_AUTOPR_GITHUB_TOKEN: $token,
DEPLOY_AUTOPR_BRANCH_PREFIX: $prefix,
DEPLOY_AUTOPR_CLOSE_SUPERSEDED_PRS: $close_old,
DEPLOY_AUTOPR_AUTO_MERGE_ENABLED: $merge_enabled,
DEPLOY_AUTOPR_AUTO_MERGE_METHOD: $merge_method,
DEPLOY_REPO_OWNER: $owner,
DEPLOY_REPO_NAME: $repo,
DEPLOY_REPO_BASE_BRANCH: $base_branch,
DEPLOY_REPO_FILE_PATH: $file_path
}
}'
)"
echo "[信息] 重启 ${NAMESPACE}/${DEPLOYMENT_NAME}..."
kubectl rollout restart deploy/"${DEPLOYMENT_NAME}" -n "${NAMESPACE}"
kubectl rollout status deploy/"${DEPLOYMENT_NAME}" -n "${NAMESPACE}" --timeout=180s
echo "[完成] 自动发布配置已生效。"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
NAMESPACE="${NAMESPACE:-infra}"
DEPLOYMENT_NAME="${DEPLOYMENT_NAME:-sync-service}"
SECRET_NAME="${SECRET_NAME:-sync-service-env}"
APP_NAMESPACE="${APP_NAMESPACE:-argocd}"
APP_NAME="${APP_NAME:-agent-platform}"
echo "[检查] sync-service 部署状态"
kubectl get deploy "${DEPLOYMENT_NAME}" -n "${NAMESPACE}" -o wide
echo
echo "[检查] 自动发布关键配置"
for key in \
DEPLOY_AUTOPR_ENABLED \
DEPLOY_AUTOPR_BRANCH_PREFIX \
DEPLOY_AUTOPR_CLOSE_SUPERSEDED_PRS \
DEPLOY_AUTOPR_AUTO_MERGE_ENABLED \
DEPLOY_AUTOPR_AUTO_MERGE_METHOD \
DEPLOY_REPO_OWNER \
DEPLOY_REPO_NAME \
DEPLOY_REPO_BASE_BRANCH \
DEPLOY_REPO_FILE_PATH; do
value="$(kubectl get secret "${SECRET_NAME}" -n "${NAMESPACE}" -o "jsonpath={.data.${key}}" | base64 -d || true)"
if [ -z "${value}" ]; then
echo "[警告] ${key} 未设置"
else
echo "[信息] ${key}=${value}"
fi
done
echo
echo "[检查] ArgoCD 应用状态"
kubectl get app "${APP_NAME}" -n "${APP_NAMESPACE}" -o jsonpath='{.status.sync.status} {.status.health.status} {.status.sync.revision}{"\n"}'
echo
echo "[检查] 最近 5 分钟自动发布日志"
kubectl logs -n "${NAMESPACE}" deploy/"${DEPLOYMENT_NAME}" --since=5m | grep -E "自动 PR|自动关闭旧 PR|自动合并|task_id" || true
echo
echo "[完成] 检查结束。"