diff --git a/apps/server/test/modules.test.ts b/apps/server/test/modules.test.ts index a2394ffc..1cdbc130 100644 --- a/apps/server/test/modules.test.ts +++ b/apps/server/test/modules.test.ts @@ -1,6 +1,6 @@ import { createRoute, z } from "@hono/zod-openapi"; import { apiResponse, CAPABILITY_IDS, capabilitySpec } from "@umt/contracts"; -import { defineTask, TASK_NAMES } from "@umt/tasks"; +import { defineTask, TASK_NAMES, TaskRegistry } from "@umt/tasks"; import { describe, expect, it } from "vitest"; import { buildTaskRegistry, collectOutboxRoutes, DeferredTaskEngine } from "../src/context.ts"; import { createApp } from "../src/http/app.ts"; @@ -91,6 +91,16 @@ describe("ServerModule 骨架", () => { expect(() => buildTaskRegistry([pingModule, { ...pingModule, name: "dup" }], fakeCtx)).toThrow(/重复注册/); }); + it("生产装配:每个核心任务都由业务模块正式注册,不落到空壳兜底", () => { + const owned = new Set(); + for (const m of SERVER_MODULES) { + const own = new TaskRegistry(); + m.registerTasks(own, fakeCtx); + for (const name of own.names()) owned.add(name); + } + expect(TASK_NAMES.filter((n) => !owned.has(n))).toEqual([]); + }); + it("outbox 路由合并;同一事件被两个模块认领报错", () => { const merged = Object.keys(collectOutboxRoutes([...SERVER_MODULES, pingModule], fakeCtx)); expect(merged).toContain("test.ping"); diff --git a/scripts/release.sh b/scripts/release.sh index f55629fe..ba03cf7b 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,16 +1,24 @@ #!/usr/bin/env bash # 测试环境发布:从干净的提交构建并替换 app 容器。 -# 用法:bash scripts/release.sh 版本取根 deno.json -# bash scripts/release.sh --no-build 只用已有镜像重建容器 +# 用法:bash scripts/release.sh [--no-build] [--allow-inflight] +# --no-build 只用已有镜像重建容器 +# --allow-inflight 引擎里有未结束的任务也继续(缺省停下:新版本可能没有旧版本任务的处理器) # 前提:工作区干净、CHANGELOG.md 已写好本版本条目、所有成员包版本号一致。 -# 做的事:打 tag v<版本>(已存在则须指向 HEAD)→ 写 .env.deploy 的 APP_VERSION / SOURCE_REVISION +# 做的事:检查引擎里未结束的任务 → 打 tag v<版本>(已存在则须指向 HEAD)→ 写 .env.deploy 的 APP_VERSION / SOURCE_REVISION # → 构建镜像 → 只替换 app(启动时自动迁移)→ 等 healthy 并核对健康接口版本。 set -euo pipefail cd "$(dirname "$0")/.." ENV_FILE=.env.deploy BUILD=1 -[ "${1:-}" = "--no-build" ] && BUILD=0 +ALLOW_INFLIGHT=0 +for a in "$@"; do + case "$a" in + --no-build) BUILD=0 ;; + --allow-inflight) ALLOW_INFLIGHT=1 ;; + *) echo "未知参数 $a" >&2; exit 1 ;; + esac +done die() { echo "✗ $*" >&2; exit 1; } @@ -25,6 +33,20 @@ for f in apps/*/deno.json apps/web/package.json packages/*/deno.json; do done grep -q "^## \[$version\]" CHANGELOG.md || die "CHANGELOG.md 还没有 ## [$version] 条目" +compose() { docker compose --env-file "$ENV_FILE" "$@"; } + +# 升级前未结束的任务:新版本只认领自己注册的任务名 + 版本,旧版本的在途任务可能没人处理 +if [ -n "$(compose ps -q postgres 2>/dev/null)" ]; then + inflight=$(compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc " + select workflow_name || '"'"'@'"'"' || version || '"'"' '"'"' || status || '"'"' x'"'"' || count(*) + from openworkflow.workflow_runs where status not in ('"'"'completed'"'"', '"'"'failed'"'"', '"'"'canceled'"'"') + group by workflow_name, version, status order by 1"' 2>/dev/null || true) + if [ -n "$inflight" ]; then + echo "引擎里有未结束的任务:"; echo "$inflight" | sed 's/^/ /' + [ $ALLOW_INFLIGHT -eq 1 ] || die "等它们结束,或确认新版本仍有对应处理器后加 --allow-inflight" + fi +fi + head=$(git rev-parse --short HEAD) if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then [ "$(git rev-parse --short "$tag^{commit}")" = "$head" ] || die "$tag 已存在但不指向 HEAD($head)" @@ -39,7 +61,6 @@ set_env() { set_env APP_VERSION "$tag" set_env SOURCE_REVISION "$head" -compose() { docker compose --env-file "$ENV_FILE" "$@"; } if [ $BUILD -eq 1 ]; then echo "▶ 构建 uhd-media-tool:$tag" compose build app