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:
co-authored by
Claude Sonnet 4.6
parent
2360e788d3
commit
87cd4b3975
+1
-1
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 || \
|
||||
|
||||
Executable
+54
@@ -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 "[完成] 自动发布配置已生效。"
|
||||
Executable
+42
@@ -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 "[完成] 检查结束。"
|
||||
Reference in New Issue
Block a user