Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions .github/drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
# 流程:
# 1. 代码克隆 (获取完整历史和 tags)
# 2. 构建 Docker 镜像
# 3. 本地部署测试 (直接利用本地 docker socket 和 docker-compose.ci.yml)
# 4. 本地升级测试 (测试旧版到新版的迁移)
# 5. 打包传输到服务器 (可选)
# 3. 空数据库迁移测试
# 4. 本地部署测试 (直接利用本地 docker socket 和 docker-compose.ci.yml)
# 5. 本地升级测试 (测试旧版到新版的迁移)
# 6. 打包传输到服务器 (可选)
# ============================================================
kind: pipeline
type: docker
Expand Down Expand Up @@ -150,7 +151,20 @@ steps:
echo "镜像构建完成 source=$PREVIOUS_RELEASE_TAG target=$DRONE_COMMIT"

# --------------------------------------------------------
# Step 3: 本地部署测试 (Deploy Test)
# Step 3: 空数据库迁移测试 (Migration Test)
# --------------------------------------------------------
- name: fresh-database-migration-test
image: docker:24.0.6
privileged: true
volumes:
- name: docker-socket
path: /var/run/docker.sock
commands:
- chmod +x .github/scripts/ci_migration_test.sh
- .github/scripts/ci_migration_test.sh

# --------------------------------------------------------
# Step 4: 本地部署测试 (Deploy Test)
# --------------------------------------------------------
- name: local-deploy-test
image: docker:24.0.6
Expand All @@ -163,7 +177,7 @@ steps:
- .github/scripts/ci_deploy_test.sh

# --------------------------------------------------------
# Step 4: 本地升级测试 (Upgrade Test)
# Step 5: 本地升级测试 (Upgrade Test)
# --------------------------------------------------------
- name: local-upgrade-test
image: docker:24.0.6
Expand All @@ -176,7 +190,7 @@ steps:
- .github/scripts/ci_upgrade_test.sh

# --------------------------------------------------------
# Step 5: (可选) 导出镜像并传输到私有服务器
# Step 6: (可选) 导出镜像并传输到私有服务器
# 只有在部署测试通过后才会执行到这一步
# --------------------------------------------------------
- name: save-images
Expand Down
72 changes: 72 additions & 0 deletions .github/scripts/ci_migration_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/sh
set -eu

COMMIT_SHORT="$(printf '%.8s' "$DRONE_COMMIT")"
PROJECT="clawith-ci-$DRONE_BUILD_NUMBER-migration"
NETWORK="$PROJECT-network"
export COMPOSE_PROJECT_NAME="$PROJECT"
export CLAWITH_DOCKER_NETWORK="$NETWORK"
export IMAGE_TAG="ci-$DRONE_BUILD_NUMBER-$COMMIT_SHORT"

compose() {
docker compose -p "$PROJECT" -f docker-compose.ci.yml "$@"
}

cleanup() {
STATUS=$?
trap - EXIT
if [ "$STATUS" -ne 0 ]; then
echo "空数据库迁移测试失败,保留诊断输出"
compose logs --no-color --tail=300 postgres 2>/dev/null || true
fi
compose down -v --remove-orphans >/dev/null 2>&1 || true
exit "$STATUS"
}

wait_healthy() {
CONTAINER_ID="$1"
ATTEMPT=0
while [ "$(docker inspect --format '{{.State.Health.Status}}' "$CONTAINER_ID" 2>/dev/null || true)" != "healthy" ]; do
ATTEMPT=$((ATTEMPT + 1))
if [ "$ATTEMPT" -ge 60 ]; then
return 1
fi
sleep 2
done
}

trap cleanup EXIT

compose down -v --remove-orphans >/dev/null 2>&1 || true
compose up -d postgres
wait_healthy "$(compose ps -q postgres)"

echo "从空 PostgreSQL 数据库执行 alembic upgrade head"
compose run --rm --no-deps --entrypoint /bin/bash backend \
-lc 'alembic upgrade head && alembic current --check-heads'

DELETED_AT_COLUMN_COUNT="$(
compose exec -T postgres psql -U clawith -d clawith -Atc "
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name IN ('agents', 'llm_models')
AND column_name = 'deleted_at';
" | tr -d '\r'
)"
[ "$DELETED_AT_COLUMN_COUNT" = "2" ]

ACTIVE_INDEX_COUNT="$(
compose exec -T postgres psql -U clawith -d clawith -Atc "
SELECT COUNT(*)
FROM pg_indexes
WHERE schemaname = 'public'
AND indexname IN (
'ix_agents_active_tenant_created_at',
'ix_llm_models_active_tenant_created_at'
);
" | tr -d '\r'
)"
[ "$ACTIVE_INDEX_COUNT" = "2" ]

echo "空数据库迁移测试通过 columns=$DELETED_AT_COLUMN_COUNT indexes=$ACTIVE_INDEX_COUNT"
22 changes: 22 additions & 0 deletions .github/scripts/ci_upgrade_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ wait_healthy() {
done
}

run_schema_command() {
IMAGE="$1"
COMMAND="$2"
docker run --rm \
--network "$NETWORK" \
--entrypoint /bin/bash \
-e DATABASE_URL=postgresql+asyncpg://clawith:clawith@postgres:5432/clawith \
-e REDIS_URL=redis://redis:6379/0 \
-e SECRET_KEY=ci-test-secret \
-e JWT_SECRET_KEY=ci-test-jwt-secret \
"$IMAGE" -lc "$COMMAND"
}

trap cleanup EXIT

compose down -v --remove-orphans >/dev/null 2>&1 || true
Expand All @@ -63,6 +76,15 @@ docker image inspect "$OLD_IMAGE" --format '{{ index .Config.Labels "org.opencon
OLD_VERSION=$(cat /tmp/old_version | tr -d '\r')
echo "启动升级源 version=$OLD_VERSION revision=$OLD_REVISION"

if [ "$OLD_VERSION" = "v1.11.2" ]; then
echo "v1.11.2 无法从空库执行最终 migration,先建立其父 revision"
run_schema_command "$OLD_IMAGE" \
"alembic upgrade add_experience_revision_drafts"
echo "使用目标镜像执行修复后的最终 migration"
run_schema_command "$NEW_IMAGE" \
"alembic upgrade head && python -m app.scripts.setup_langgraph_checkpoints"
fi

docker run -d \
--name "$OLD_CONTAINER" \
--network "$NETWORK" \
Expand Down
89 changes: 63 additions & 26 deletions backend/alembic/versions/202607221500_add_agent_model_deleted_at.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,70 @@
depends_on: str | Sequence[str] | None = None


AGENTS_TABLE = "agents"
LLM_MODELS_TABLE = "llm_models"
DELETED_AT_COLUMN = "deleted_at"
AGENTS_ACTIVE_INDEX = "ix_agents_active_tenant_created_at"
LLM_MODELS_ACTIVE_INDEX = "ix_llm_models_active_tenant_created_at"


def _inspector():
return sa.inspect(op.get_bind())


def _column_exists(table_name: str, column_name: str) -> bool:
return column_name in {
column["name"] for column in _inspector().get_columns(table_name)
}


def _index_exists(table_name: str, index_name: str) -> bool:
return index_name in {
index["name"] for index in _inspector().get_indexes(table_name)
}


def upgrade() -> None:
op.add_column(
"agents",
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
)
op.add_column(
"llm_models",
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
)
op.create_index(
"ix_agents_active_tenant_created_at",
"agents",
["tenant_id", "created_at"],
unique=False,
postgresql_where=sa.text("deleted_at IS NULL"),
)
op.create_index(
"ix_llm_models_active_tenant_created_at",
"llm_models",
["tenant_id", "created_at"],
unique=False,
postgresql_where=sa.text("deleted_at IS NULL"),
)
if not _column_exists(AGENTS_TABLE, DELETED_AT_COLUMN):
op.add_column(
AGENTS_TABLE,
sa.Column(DELETED_AT_COLUMN, sa.DateTime(timezone=True), nullable=True),
)

if not _column_exists(LLM_MODELS_TABLE, DELETED_AT_COLUMN):
op.add_column(
LLM_MODELS_TABLE,
sa.Column(DELETED_AT_COLUMN, sa.DateTime(timezone=True), nullable=True),
)

if not _index_exists(AGENTS_TABLE, AGENTS_ACTIVE_INDEX):
op.create_index(
AGENTS_ACTIVE_INDEX,
AGENTS_TABLE,
["tenant_id", "created_at"],
unique=False,
postgresql_where=sa.text("deleted_at IS NULL"),
)

if not _index_exists(LLM_MODELS_TABLE, LLM_MODELS_ACTIVE_INDEX):
op.create_index(
LLM_MODELS_ACTIVE_INDEX,
LLM_MODELS_TABLE,
["tenant_id", "created_at"],
unique=False,
postgresql_where=sa.text("deleted_at IS NULL"),
)


def downgrade() -> None:
op.drop_index("ix_llm_models_active_tenant_created_at", table_name="llm_models")
op.drop_index("ix_agents_active_tenant_created_at", table_name="agents")
op.drop_column("llm_models", "deleted_at")
op.drop_column("agents", "deleted_at")
if _index_exists(LLM_MODELS_TABLE, LLM_MODELS_ACTIVE_INDEX):
op.drop_index(LLM_MODELS_ACTIVE_INDEX, table_name=LLM_MODELS_TABLE)

if _index_exists(AGENTS_TABLE, AGENTS_ACTIVE_INDEX):
op.drop_index(AGENTS_ACTIVE_INDEX, table_name=AGENTS_TABLE)

if _column_exists(LLM_MODELS_TABLE, DELETED_AT_COLUMN):
op.drop_column(LLM_MODELS_TABLE, DELETED_AT_COLUMN)

if _column_exists(AGENTS_TABLE, DELETED_AT_COLUMN):
op.drop_column(AGENTS_TABLE, DELETED_AT_COLUMN)
Loading