diff --git a/README.md b/README.md index 4ea3f5b..0056b24 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ pnpm prisma:migrate:dev --name init pnpm prisma:seed ``` +## 数据库迁移与 Jenkins 部署 + +- 迁移历史从 `prisma/migrations/20260611000000_baseline_current_schema` 开始,空库可以直接执行 `pnpm prisma migrate deploy` 初始化当前 schema。 +- 生产库在启用 Prisma 迁移历史前已经存在表结构;`deploy/deploy-devops-platform-api-from-jenkins.sh` 会检测完整表结构并自动将基线迁移标记为已应用,再执行 `pnpm prisma migrate deploy`。 +- Jenkins 服务器使用 `/usr/local/bin/deploy-devops-platform-api-from-jenkins`,该脚本应与仓库 `deploy/deploy-devops-platform-api-from-jenkins.sh` 保持一致。 +- 如需紧急跳过迁移,可在服务器环境中设置 `PRISMA_MIGRATE_DEPLOY_ENABLED=false`,但不能在存在 schema 变更的版本长期使用该开关。 +- 新增表或字段时必须提交新的 `prisma/migrations/_/migration.sql`,不要再用手工 SQL 直接改生产库。 + ## 内置账号 - 超级管理员账号:`zhanxi` diff --git a/RTK.md b/RTK.md index 5557fb7..ed4c520 100644 --- a/RTK.md +++ b/RTK.md @@ -50,3 +50,9 @@ pnpm lint pnpm test pnpm build ``` + +## 部署迁移 + +- Jenkins API 部署脚本的仓库版本在 `deploy/deploy-devops-platform-api-from-jenkins.sh`。 +- 脚本会在切换 `/srv/www/devops-platform-api` 前运行 `pnpm prisma migrate deploy`,迁移失败时不会替换旧服务目录。 +- 生产库早期通过手工 SQL 初始化,首次启用迁移时脚本会在确认 12 张核心表都存在后把基线迁移标记为 applied。 diff --git a/deploy/deploy-devops-platform-api-from-jenkins.sh b/deploy/deploy-devops-platform-api-from-jenkins.sh new file mode 100755 index 0000000..12aa0f8 --- /dev/null +++ b/deploy/deploy-devops-platform-api-from-jenkins.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +set -euo pipefail + +SRC="${1:-}" +APP_DIR="/srv/www/devops-platform-api" +BACKUP_DIR="/srv/www/devops-platform-api-backups" +ENV_FILE="/etc/devops-platform-api.env" +PM2_NAME="devops-platform-api" +HEALTH_URL="http://127.0.0.1:4300/health" +TS="$(date +%Y%m%d%H%M%S)" + +if [ -z "$SRC" ] || [ ! -d "$SRC" ]; then + echo "Usage: deploy-devops-platform-api-from-jenkins " + exit 2 +fi + +for required in "$SRC/dist/main.js" "$SRC/package.json" "$SRC/pnpm-lock.yaml" "$SRC/prisma" "$ENV_FILE"; do + if [ ! -e "$required" ]; then + echo "Missing $required" + exit 3 + fi +done + +run_prisma_migrations() { + if [ "${PRISMA_MIGRATE_DEPLOY_ENABLED:-true}" != "true" ]; then + echo "Skipping Prisma migrations because PRISMA_MIGRATE_DEPLOY_ENABLED is not true" + return + fi + + set -a + . ./.env.production + set +a + + local migration_state + migration_state="$(node <<'NODE' +const { PrismaClient } = require('@prisma/client'); + +const requiredTables = [ + 'agent_invocations', + 'audit_logs', + 'deploy_jobs', + 'deploy_runs', + 'environments', + 'outbox_messages', + 'platform_messages', + 'project_member_permissions', + 'projects', + 'run_steps', + 'secrets', + 'users', +]; +const baselineMigrations = [ + '20260611000000_baseline_current_schema', + '20260611120000_auth_members_messages', +]; + +const prisma = new PrismaClient(); + +async function main() { + const tables = await prisma.$queryRaw` + select table_name + from information_schema.tables + where table_schema = database() + `; + const existingTables = new Set(tables.map((row) => row.TABLE_NAME || row.table_name)); + + if (existingTables.size === 0) { + console.log('fresh'); + return; + } + + const missingTables = requiredTables.filter((name) => !existingTables.has(name)); + if (missingTables.length > 0) { + console.error(`Existing database schema is incomplete; missing tables: ${missingTables.join(', ')}`); + process.exit(12); + } + + const hasMigrationTable = existingTables.has('_prisma_migrations'); + const appliedMigrations = hasMigrationTable + ? await prisma.$queryRaw`select migration_name from _prisma_migrations` + : []; + const appliedNames = new Set( + appliedMigrations.map((row) => row.MIGRATION_NAME || row.migration_name), + ); + const missingMigrations = baselineMigrations.filter((name) => !appliedNames.has(name)); + + if (missingMigrations.length > 0) { + console.log(`bootstrap:${missingMigrations.join(',')}`); + return; + } + + console.log('ready'); +} + +main() + .finally(() => prisma.$disconnect()); +NODE +)" + + if [[ "$migration_state" == bootstrap:* ]]; then + local migrations_csv="${migration_state#bootstrap:}" + IFS=',' read -r -a missing_migrations <<< "$migrations_csv" + for migration in "${missing_migrations[@]}"; do + echo "Marking existing production schema migration as applied: $migration" + pnpm prisma migrate resolve --schema prisma/schema.prisma --applied "$migration" + done + else + echo "Prisma migration bootstrap state: $migration_state" + fi + + echo "Running Prisma migrate deploy" + pnpm prisma migrate deploy --schema prisma/schema.prisma +} + +mkdir -p "$BACKUP_DIR" +TMP="$(mktemp -d /srv/www/devops-platform-api.deploy.XXXXXX)" +OLD="$APP_DIR.old.$TS" + +cp -a "$SRC/dist" "$TMP/dist" +cp -a "$SRC/prisma" "$TMP/prisma" +cp -a "$SRC/package.json" "$TMP/package.json" +cp -a "$SRC/pnpm-lock.yaml" "$TMP/pnpm-lock.yaml" +cp -a "$ENV_FILE" "$TMP/.env.production" + +cd "$TMP" +corepack enable +corepack prepare pnpm@9.15.4 --activate +pnpm install --frozen-lockfile +pnpm prisma:generate +run_prisma_migrations + +if [ -d "$APP_DIR" ]; then + tar -C "$APP_DIR" -czf "$BACKUP_DIR/$TS.tgz" . + mv "$APP_DIR" "$OLD" +fi + +mv "$TMP" "$APP_DIR" +cd "$APP_DIR" +set -a +. ./.env.production +set +a + +if ! pm2 restart "$PM2_NAME" --update-env; then + pm2 start dist/main.js --name "$PM2_NAME" --update-env +fi + +sleep 8 +if ! curl -fsS "$HEALTH_URL" >/dev/null; then + echo "Health check failed; rolling back $PM2_NAME" + pm2 stop "$PM2_NAME" || true + rm -rf "$APP_DIR.failed.$TS" + mv "$APP_DIR" "$APP_DIR.failed.$TS" + if [ -d "$OLD" ]; then + mv "$OLD" "$APP_DIR" + cd "$APP_DIR" + set -a + . ./.env.production + set +a + pm2 restart "$PM2_NAME" --update-env || pm2 start dist/main.js --name "$PM2_NAME" --update-env + fi + exit 5 +fi + +rm -rf "$OLD" +pm2 save >/dev/null +echo "Deployed devops-platform-api from $SRC to $APP_DIR" diff --git a/prisma/manual-migrations/20260611120000_auth_members_messages.sql b/prisma/manual-migrations/20260611120000_auth_members_messages.sql new file mode 100644 index 0000000..d0b9bcc --- /dev/null +++ b/prisma/manual-migrations/20260611120000_auth_members_messages.sql @@ -0,0 +1,72 @@ +-- Historical SQL executed manually on production before `_prisma_migrations` +-- was bootstrapped. Kept for audit/reference only; Prisma migrate ignores this +-- directory. + +CREATE TABLE `users` ( + `id` VARCHAR(191) NOT NULL, + `account` VARCHAR(191) NOT NULL, + `displayName` VARCHAR(191) NOT NULL, + `role` ENUM('SUPER_ADMIN', 'MEMBER') NOT NULL DEFAULT 'MEMBER', + `status` ENUM('ACTIVE', 'DISABLED') NOT NULL DEFAULT 'ACTIVE', + `passwordHash` TEXT NOT NULL, + `mustChangePassword` BOOLEAN NOT NULL DEFAULT false, + `passwordVersion` INTEGER NOT NULL DEFAULT 1, + `lastLoginAt` DATETIME(3) NULL, + `createdById` VARCHAR(191) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `users_account_key`(`account`), + INDEX `users_role_status_idx`(`role`, `status`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE `project_member_permissions` ( + `id` VARCHAR(191) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `projectId` VARCHAR(191) NOT NULL, + `level` ENUM('READ', 'BUILD') NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `project_member_permissions_userId_projectId_key`(`userId`, `projectId`), + INDEX `project_member_permissions_projectId_idx`(`projectId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +CREATE TABLE `platform_messages` ( + `id` VARCHAR(191) NOT NULL, + `type` ENUM('PASSWORD_RESET_REQUEST') NOT NULL, + `status` ENUM('PENDING', 'HANDLED') NOT NULL DEFAULT 'PENDING', + `title` VARCHAR(191) NOT NULL, + `content` TEXT NOT NULL, + `requesterId` VARCHAR(191) NULL, + `requesterAccount` VARCHAR(191) NOT NULL, + `targetResourceType` VARCHAR(191) NULL, + `targetResourceId` VARCHAR(191) NULL, + `metadata` JSON NULL, + `handledById` VARCHAR(191) NULL, + `handledAt` DATETIME(3) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `platform_messages_type_status_createdAt_idx`(`type`, `status`, `createdAt`), + INDEX `platform_messages_requesterId_status_idx`(`requesterId`, `status`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +ALTER TABLE `project_member_permissions` + ADD CONSTRAINT `project_member_permissions_userId_fkey` + FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE `project_member_permissions` + ADD CONSTRAINT `project_member_permissions_projectId_fkey` + FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE `platform_messages` + ADD CONSTRAINT `platform_messages_requesterId_fkey` + FOREIGN KEY (`requesterId`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE `platform_messages` + ADD CONSTRAINT `platform_messages_handledById_fkey` + FOREIGN KEY (`handledById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260611000000_baseline_current_schema/migration.sql b/prisma/migrations/20260611000000_baseline_current_schema/migration.sql new file mode 100644 index 0000000..45e91c9 --- /dev/null +++ b/prisma/migrations/20260611000000_baseline_current_schema/migration.sql @@ -0,0 +1,275 @@ +-- Baseline migration for the current DevOps platform schema. +-- Existing production tables are marked as applied by the Jenkins deploy script +-- before running `prisma migrate deploy`; fresh databases execute this file. + +-- CreateTable +CREATE TABLE `projects` ( + `id` VARCHAR(191) NOT NULL, + `key` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL, + `repositoryUrl` VARCHAR(512) NULL, + `defaultBranch` VARCHAR(191) NOT NULL DEFAULT 'develop', + `status` ENUM('ACTIVE', 'ARCHIVED') NOT NULL DEFAULT 'ACTIVE', + `metadata` JSON NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `projects_key_key`(`key`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `environments` ( + `id` VARCHAR(191) NOT NULL, + `projectId` VARCHAR(191) NOT NULL, + `name` ENUM('TEST', 'PRODUCTION') NOT NULL, + `displayName` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `environments_projectId_idx`(`projectId`), + UNIQUE INDEX `environments_projectId_name_key`(`projectId`, `name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `deploy_jobs` ( + `id` VARCHAR(191) NOT NULL, + `projectId` VARCHAR(191) NOT NULL, + `environmentId` VARCHAR(191) NOT NULL, + `jenkinsJobPath` VARCHAR(191) NOT NULL, + `jenkinsParameters` JSON NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `deploy_jobs_environmentId_idx`(`environmentId`), + UNIQUE INDEX `deploy_jobs_projectId_environmentId_key`(`projectId`, `environmentId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `deploy_runs` ( + `id` VARCHAR(191) NOT NULL, + `projectId` VARCHAR(191) NOT NULL, + `environmentId` VARCHAR(191) NOT NULL, + `jobId` VARCHAR(191) NULL, + `status` ENUM('PENDING', 'QUEUED', 'RUNNING', 'SUCCESS', 'FAILED', 'CANCELED') NOT NULL DEFAULT 'PENDING', + `trigger` ENUM('MANUAL', 'GITEA_WEBHOOK', 'RETRY') NOT NULL DEFAULT 'MANUAL', + `ref` VARCHAR(191) NOT NULL, + `operator` VARCHAR(191) NOT NULL, + `idempotencyKey` VARCHAR(191) NOT NULL, + `jenkinsQueueId` VARCHAR(191) NULL, + `jenkinsBuildNumber` INTEGER NULL, + `failureSummary` TEXT NULL, + `metadata` JSON NULL, + `version` INTEGER NOT NULL DEFAULT 1, + `startedAt` DATETIME(3) NULL, + `finishedAt` DATETIME(3) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `deploy_runs_idempotencyKey_key`(`idempotencyKey`), + INDEX `deploy_runs_projectId_environmentId_createdAt_idx`(`projectId`, `environmentId`, `createdAt`), + INDEX `deploy_runs_status_createdAt_idx`(`status`, `createdAt`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `run_steps` ( + `id` VARCHAR(191) NOT NULL, + `deployRunId` VARCHAR(191) NOT NULL, + `bpmnNodeId` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL, + `status` ENUM('PENDING', 'RUNNING', 'SUCCESS', 'FAILED', 'SKIPPED', 'CANCELED') NOT NULL DEFAULT 'PENDING', + `order` INTEGER NOT NULL DEFAULT 0, + `errorSummary` TEXT NULL, + `metadata` JSON NULL, + `startedAt` DATETIME(3) NULL, + `finishedAt` DATETIME(3) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `run_steps_deployRunId_order_idx`(`deployRunId`, `order`), + UNIQUE INDEX `run_steps_deployRunId_bpmnNodeId_key`(`deployRunId`, `bpmnNodeId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `audit_logs` ( + `id` VARCHAR(191) NOT NULL, + `deployRunId` VARCHAR(191) NULL, + `actorId` VARCHAR(191) NULL, + `actorName` VARCHAR(191) NULL, + `action` VARCHAR(191) NOT NULL, + `resourceType` VARCHAR(191) NOT NULL, + `resourceId` VARCHAR(191) NULL, + `requestId` VARCHAR(191) NULL, + `sourceIp` VARCHAR(191) NULL, + `userAgent` VARCHAR(191) NULL, + `before` JSON NULL, + `after` JSON NULL, + `parameterDigest` JSON NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `audit_logs_resourceType_resourceId_createdAt_idx`(`resourceType`, `resourceId`, `createdAt`), + INDEX `audit_logs_actorId_createdAt_idx`(`actorId`, `createdAt`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `users` ( + `id` VARCHAR(191) NOT NULL, + `account` VARCHAR(191) NOT NULL, + `displayName` VARCHAR(191) NOT NULL, + `role` ENUM('SUPER_ADMIN', 'MEMBER') NOT NULL DEFAULT 'MEMBER', + `status` ENUM('ACTIVE', 'DISABLED') NOT NULL DEFAULT 'ACTIVE', + `passwordHash` TEXT NOT NULL, + `mustChangePassword` BOOLEAN NOT NULL DEFAULT false, + `passwordVersion` INTEGER NOT NULL DEFAULT 1, + `lastLoginAt` DATETIME(3) NULL, + `createdById` VARCHAR(191) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `users_account_key`(`account`), + INDEX `users_role_status_idx`(`role`, `status`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `project_member_permissions` ( + `id` VARCHAR(191) NOT NULL, + `userId` VARCHAR(191) NOT NULL, + `projectId` VARCHAR(191) NOT NULL, + `level` ENUM('READ', 'BUILD') NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `project_member_permissions_projectId_idx`(`projectId`), + UNIQUE INDEX `project_member_permissions_userId_projectId_key`(`userId`, `projectId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `platform_messages` ( + `id` VARCHAR(191) NOT NULL, + `type` ENUM('PASSWORD_RESET_REQUEST') NOT NULL, + `status` ENUM('PENDING', 'HANDLED') NOT NULL DEFAULT 'PENDING', + `title` VARCHAR(191) NOT NULL, + `content` TEXT NOT NULL, + `requesterId` VARCHAR(191) NULL, + `requesterAccount` VARCHAR(191) NOT NULL, + `targetResourceType` VARCHAR(191) NULL, + `targetResourceId` VARCHAR(191) NULL, + `metadata` JSON NULL, + `handledById` VARCHAR(191) NULL, + `handledAt` DATETIME(3) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `platform_messages_type_status_createdAt_idx`(`type`, `status`, `createdAt`), + INDEX `platform_messages_requesterId_status_idx`(`requesterId`, `status`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `secrets` ( + `id` VARCHAR(191) NOT NULL, + `provider` ENUM('JENKINS', 'GITEA', 'WECOM', 'LLM', 'INTERNAL') NOT NULL, + `scopeType` VARCHAR(191) NOT NULL, + `scopeId` VARCHAR(191) NOT NULL, + `name` VARCHAR(191) NOT NULL, + `encryptedValue` TEXT NOT NULL, + `version` INTEGER NOT NULL DEFAULT 1, + `rotatedAt` DATETIME(3) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + INDEX `secrets_scopeType_scopeId_idx`(`scopeType`, `scopeId`), + UNIQUE INDEX `secrets_provider_scopeType_scopeId_name_key`(`provider`, `scopeType`, `scopeId`, `name`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `agent_invocations` ( + `id` VARCHAR(191) NOT NULL, + `deployRunId` VARCHAR(191) NULL, + `purpose` ENUM('RISK_SUMMARY', 'FAILURE_DIAGNOSIS', 'RUNBOOK_QA', 'RELEASE_NOTES', 'INCIDENT_REVIEW') NOT NULL, + `actorId` VARCHAR(191) NULL, + `actorName` VARCHAR(191) NULL, + `model` VARCHAR(191) NULL, + `promptVersion` VARCHAR(191) NOT NULL, + `inputDigest` JSON NOT NULL, + `outputDigest` JSON NULL, + `tokenEstimate` INTEGER NULL, + `status` VARCHAR(191) NOT NULL, + `errorSummary` TEXT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `completedAt` DATETIME(3) NULL, + + INDEX `agent_invocations_purpose_createdAt_idx`(`purpose`, `createdAt`), + INDEX `agent_invocations_deployRunId_createdAt_idx`(`deployRunId`, `createdAt`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `outbox_messages` ( + `id` VARCHAR(191) NOT NULL, + `deployRunId` VARCHAR(191) NULL, + `channel` VARCHAR(191) NOT NULL, + `template` VARCHAR(191) NOT NULL, + `payload` JSON NOT NULL, + `status` ENUM('PENDING', 'SENT', 'FAILED', 'DEAD') NOT NULL DEFAULT 'PENDING', + `attemptCount` INTEGER NOT NULL DEFAULT 0, + `nextAttemptAt` DATETIME(3) NULL, + `lastError` TEXT NULL, + `idempotencyKey` VARCHAR(191) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + UNIQUE INDEX `outbox_messages_idempotencyKey_key`(`idempotencyKey`), + INDEX `outbox_messages_status_nextAttemptAt_idx`(`status`, `nextAttemptAt`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `environments` ADD CONSTRAINT `environments_projectId_fkey` FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `deploy_jobs` ADD CONSTRAINT `deploy_jobs_projectId_fkey` FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `deploy_jobs` ADD CONSTRAINT `deploy_jobs_environmentId_fkey` FOREIGN KEY (`environmentId`) REFERENCES `environments`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `deploy_runs` ADD CONSTRAINT `deploy_runs_projectId_fkey` FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `deploy_runs` ADD CONSTRAINT `deploy_runs_environmentId_fkey` FOREIGN KEY (`environmentId`) REFERENCES `environments`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `deploy_runs` ADD CONSTRAINT `deploy_runs_jobId_fkey` FOREIGN KEY (`jobId`) REFERENCES `deploy_jobs`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `run_steps` ADD CONSTRAINT `run_steps_deployRunId_fkey` FOREIGN KEY (`deployRunId`) REFERENCES `deploy_runs`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `audit_logs` ADD CONSTRAINT `audit_logs_deployRunId_fkey` FOREIGN KEY (`deployRunId`) REFERENCES `deploy_runs`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `project_member_permissions` ADD CONSTRAINT `project_member_permissions_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `project_member_permissions` ADD CONSTRAINT `project_member_permissions_projectId_fkey` FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `platform_messages` ADD CONSTRAINT `platform_messages_requesterId_fkey` FOREIGN KEY (`requesterId`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `platform_messages` ADD CONSTRAINT `platform_messages_handledById_fkey` FOREIGN KEY (`handledById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `agent_invocations` ADD CONSTRAINT `agent_invocations_deployRunId_fkey` FOREIGN KEY (`deployRunId`) REFERENCES `deploy_runs`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `outbox_messages` ADD CONSTRAINT `outbox_messages_deployRunId_fkey` FOREIGN KEY (`deployRunId`) REFERENCES `deploy_runs`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260611120000_auth_members_messages/migration.sql b/prisma/migrations/20260611120000_auth_members_messages/migration.sql index 316e8a0..0928c1d 100644 --- a/prisma/migrations/20260611120000_auth_members_messages/migration.sql +++ b/prisma/migrations/20260611120000_auth_members_messages/migration.sql @@ -1,68 +1,3 @@ -CREATE TABLE `users` ( - `id` VARCHAR(191) NOT NULL, - `account` VARCHAR(191) NOT NULL, - `displayName` VARCHAR(191) NOT NULL, - `role` ENUM('SUPER_ADMIN', 'MEMBER') NOT NULL DEFAULT 'MEMBER', - `status` ENUM('ACTIVE', 'DISABLED') NOT NULL DEFAULT 'ACTIVE', - `passwordHash` TEXT NOT NULL, - `mustChangePassword` BOOLEAN NOT NULL DEFAULT false, - `passwordVersion` INTEGER NOT NULL DEFAULT 1, - `lastLoginAt` DATETIME(3) NULL, - `createdById` VARCHAR(191) NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `users_account_key`(`account`), - INDEX `users_role_status_idx`(`role`, `status`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -CREATE TABLE `project_member_permissions` ( - `id` VARCHAR(191) NOT NULL, - `userId` VARCHAR(191) NOT NULL, - `projectId` VARCHAR(191) NOT NULL, - `level` ENUM('READ', 'BUILD') NOT NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `project_member_permissions_userId_projectId_key`(`userId`, `projectId`), - INDEX `project_member_permissions_projectId_idx`(`projectId`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -CREATE TABLE `platform_messages` ( - `id` VARCHAR(191) NOT NULL, - `type` ENUM('PASSWORD_RESET_REQUEST') NOT NULL, - `status` ENUM('PENDING', 'HANDLED') NOT NULL DEFAULT 'PENDING', - `title` VARCHAR(191) NOT NULL, - `content` TEXT NOT NULL, - `requesterId` VARCHAR(191) NULL, - `requesterAccount` VARCHAR(191) NOT NULL, - `targetResourceType` VARCHAR(191) NULL, - `targetResourceId` VARCHAR(191) NULL, - `metadata` JSON NULL, - `handledById` VARCHAR(191) NULL, - `handledAt` DATETIME(3) NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - INDEX `platform_messages_type_status_createdAt_idx`(`type`, `status`, `createdAt`), - INDEX `platform_messages_requesterId_status_idx`(`requesterId`, `status`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -ALTER TABLE `project_member_permissions` - ADD CONSTRAINT `project_member_permissions_userId_fkey` - FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE `project_member_permissions` - ADD CONSTRAINT `project_member_permissions_projectId_fkey` - FOREIGN KEY (`projectId`) REFERENCES `projects`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE `platform_messages` - ADD CONSTRAINT `platform_messages_requesterId_fkey` - FOREIGN KEY (`requesterId`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; - -ALTER TABLE `platform_messages` - ADD CONSTRAINT `platform_messages_handledById_fkey` - FOREIGN KEY (`handledById`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE; +-- This migration was executed manually before Prisma migration history was +-- enabled in production. The current baseline migration already contains these +-- tables, so this directory is intentionally kept as a no-op history marker. diff --git a/prisma/migrations/README.md b/prisma/migrations/README.md index 2d96c5c..9ddb68a 100644 --- a/prisma/migrations/README.md +++ b/prisma/migrations/README.md @@ -9,4 +9,8 @@ - 部署前先执行迁移检查,再启动 API 服务。 - 原生 SQL 只允许作为例外使用,必须放在 Repository 方法中并说明原因。 +`20260611000000_baseline_current_schema` 是当前线上 schema 的基线迁移。生产库已经存在这些表时,Jenkins 部署脚本会先执行 `prisma migrate resolve --applied` 标记基线,再运行 `prisma migrate deploy`。 + +`20260611120000_auth_members_messages` 是生产手动执行过的历史迁移,现在保留为 no-op 历史标记;原始 SQL 留存在 `prisma/manual-migrations/20260611120000_auth_members_messages.sql`。 + 首阶段 schema 定义在 `prisma/schema.prisma`。没有明确数据库连接前,不要求本地自动执行迁移。 diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..9f6c6d1 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system. +provider = "mysql"