调度管线按路径过滤 Full Build 并回填升级证据
Project CI / AI game creator shell Rust shard 1/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled

- 调度管线同时判定 AGC 与 Full Build 两条下游的发布范围,并分别决定是否触发
- Full Build 采用与线上站点/后端无关的路径黑名单,改动落在黑名单外照常部署,避免漏发
- 两条同时跳过时只推进 revision 状态、不触发任何发布,构建描述写明实际触发的下游
- 新增 Full Build 跳过模式守卫用例,并补齐开发运维与技术方案说明
- 主规范回填真实更新闭环证据:0.1.47 客户端完成提示、下载、安装与重启
This commit is contained in:
kdletters
2026-09-17 19:31:19 +08:00
parent ba089980b1
commit 5ccc46ac1a
5 changed files with 91 additions and 37 deletions
@@ -364,3 +364,30 @@ test('scheduler path filter stays in sync with client release paths', () => {
);
}
});
test('scheduler skips the full build only for non-deploy paths', () => {
const jenkinsfile = readFileSync(
new URL(
'../../../jenkins/Jenkinsfile.scheduled-revision-trigger',
import.meta.url,
),
'utf8',
);
const skipLine = jenkinsfile
.split('\n')
.find((line) => line.includes('docs/*|.codex/*|jenkins/*'));
assert.ok(skipLine, '调度管线里应存在 Full Build 跳过模式');
for (const pattern of [
'docs/*',
'.codex/*',
'jenkins/*',
'apps/ai-game-creator-shell/*',
'apps/mobile-shell/*',
'apps/desktop-shell/*',
'apps/preview-deployer-web/*',
'tools/*',
'*.md',
]) {
assert.ok(skipLine.includes(pattern), `Full Build 跳过模式缺少 ${pattern}`);
}
});
@@ -102,24 +102,25 @@
已获得的证据:
| 条款 | 验收方式 | 证据 |
| -------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| 渠道与端点映射、渠道校验 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过(默认渠道、错配失败关闭、未知渠道失败关闭) |
| universal 包挂两个平台键 | 同上 + 本地发布烟测(伪造 bundle) | 通过(两键同 URL 同签名,不生成迁移清单) |
| 缺签名时失败关闭 | 同上 | 通过 |
| 开发态不检查更新 | `vitest run apps/ai-game-creator-shell/tests/appUpdate.test.ts` | 通过(开关关闭时不请求清单) |
| 旧自研链路整条删除 | 代码检索无残留命令、事件与白名单条目 | 通过(`download_agc_update` / 下载事件 / 清单常量均无残留) |
| 清单与对象布局符合渠道约定 | `Genarrative-Agc-Windows-Build` #682026-09-17SUCCESS | 通过:`agc/dev-win/latest.json` = 0.1.48 + `windows-x86_64``agc/dev-win/0.1.48/陶泥儿_0.1.48_x64-setup.exe` 与同名 `.sig` 公网可读 |
| 清单签名与签名对象一致 | 取回 `.sig` 对象与渠道清单 `signature` 比对 | 通过(逐字相同,420 字节) |
| 安装包与清单登记一致 | 下载安装包实算 SHA-256 与尺寸后与迁移桥清单比对 | 通过(size `104678031`、sha256 `1f67…4fd0` 一致) |
| 旧协议迁移桥 | 公网读取 `agc/latest.json` | 通过(0.1.48`downloadUrl` 指向同一对象,含 `sha256` / `size`) |
| 条款 | 验收方式 | 证据 |
| ---------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| 渠道与端点映射、渠道校验 | `node --test apps/ai-game-creator-shell/scripts/build-release.test.mjs` | 通过(默认渠道、错配失败关闭、未知渠道失败关闭) |
| universal 包挂两个平台键 | 同上 + 本地发布烟测(伪造 bundle) | 通过(两键同 URL 同签名,不生成迁移清单) |
| 缺签名时失败关闭 | 同上 | 通过 |
| 开发态不检查更新 | `vitest run apps/ai-game-creator-shell/tests/appUpdate.test.ts` | 通过(开关关闭时不请求清单) |
| 旧自研链路整条删除 | 代码检索无残留命令、事件与白名单条目 | 通过(`download_agc_update` / 下载事件 / 清单常量均无残留) |
| 清单与对象布局符合渠道约定 | `Genarrative-Agc-Windows-Build` #682026-09-17SUCCESS | 通过:`agc/dev-win/latest.json` = 0.1.48 + `windows-x86_64``agc/dev-win/0.1.48/陶泥儿_0.1.48_x64-setup.exe` 与同名 `.sig` 公网可读 |
| 清单签名与签名对象一致 | 取回 `.sig` 对象与渠道清单 `signature` 比对 | 通过(逐字相同,420 字节) |
| 安装包与清单登记一致 | 下载安装包实算 SHA-256 与尺寸后与迁移桥清单比对 | 通过(size `104678031`、sha256 `1f67…4fd0` 一致) |
| 旧协议迁移桥 | 公网读取 `agc/latest.json` | 通过(0.1.48`downloadUrl` 指向同一对象,含 `sha256` / `size`) |
| 真实更新闭环(含升级后重启) | 0.1.47 客户端按提示下载安装并重启 | 通过(2026-09-17 用户实测:提示 → 下载 → 安装 → 关于页显示新版本,再次检查为已是最新) |
待执行证据(首次渠道发布后回填):
| 条款 | 验收方式 | 证据 |
| ---------------------------- | --------------------------------------------------------------------- | ------ |
| 真实更新闭环(含升级后重启) | 0.1.47 客户端升级到新版本,再启动不再提示;`npm run agc` 仍无更新入口 | 待执行 |
| 签名校验失败拒绝安装 | 篡改渠道清单 `signature` 后观察客户端拒绝安装的表现 | 待执行 |
| 条款 | 验收方式 | 证据 |
| -------------------- | --------------------------------------------------- | ------ |
| 签名校验失败拒绝安装 | 篡改渠道清单 `signature` 后观察客户端拒绝安装的表现 | 待执行 |
| 签名校验失败拒绝安装 | 篡改渠道清单 `signature` 后观察客户端拒绝安装的表现 | 待执行 |
## 未决问题与决策
@@ -137,7 +137,7 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去
`Genarrative-Scheduled-Revision-Trigger` 是唯一的定时入口,每小时检查一次(`H * * * *`,分钟由 Jenkins 按 Job 名散列,不等同于整点)。它只用 `git ls-remote` 解析 `SOURCE_BRANCH`(默认 `master`)的远端 HEAD,不 checkout 工作区;解析出的完整 commit 与上一次触发过的 revision 相同则标记 `NOT_BUILT` 并结束,不触发任何下游。
revision 变化时,调度管线把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy``Genarrative-Agc-Windows-Build`,两条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。两条下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/``packages/``server-rs/crates/``plugins/agc-cocos-editor/``apps/desktop-shell/src-tauri/icons/``package.json``package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build`,纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交标题逐条写进 `notes`(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development``STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`
revision 变化时,调度管线把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy``Genarrative-Agc-Windows-Build`,两条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。两条下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/``packages/``server-rs/crates/``plugins/agc-cocos-editor/``apps/desktop-shell/src-tauri/icons/``package.json``package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build`,纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。两条下游各自判定:AGC Windows Build 采用「客户端相关路径白名单」,Full Build 采用「与线上站点 / 后端无关的路径黑名单」(`docs/``.codex/``jenkins/``apps/ai-game-creator-shell/``apps/mobile-shell/``apps/desktop-shell/``apps/preview-deployer-web/``tools/`、根级 `*.md`),改动只要落在黑名单之外就会照常部署,避免漏发线上站点或后端;两条同时被判为跳过时调度管线只推进 revision 状态、不触发任何发布。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交标题逐条写进 `notes`(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development``STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`
调度状态是调度 Job 工作区里的 `.jenkins-last-triggered-revision`,构建描述同时回显本次 revision 与结果。工作区被清理(例如 `Wipe Out Workspace`)或状态文件缺失时,下一次运行按“版本变化”处理并触发一次,之后恢复稳定;需要重建同一版本时勾选 `FORCE_TRIGGER`。Job 按仓库内 `jenkins/scheduled-revision-trigger-job-config.xml` 创建:`scriptPath=jenkins/Jenkinsfile.scheduled-revision-trigger`、Git 入口 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`、凭据 `genarrative-local-gitea-ssh``<triggers/>` 留空(定时器写在 Jenkinsfile 里)。推送后必须让三个 live Job 各自加载一次新 Jenkinsfile,并只读核对 `config.xml`Full 与 AGC 不再有 cron,定时只来自新调度 Job;只改 Jenkinsfile 而不确认 live 配置时,旧 cron 仍会继续触发。
+46 -20
View File
@@ -58,24 +58,25 @@ pipeline {
}
}
// 只有在「本轮到达的提交」里出现 AGC 相关路径时,Windows 客户端才发布新版本;
// 纯文档或流水线自身的提交仍然触发 Full Build,但不再推高客户端版本号。
stage('Resolve AGC Release Scope') {
// 「本轮到达的提交」判定两条下游各自是否需要跑:
// - AGC:只有出现客户端相关路径才发布 Windows 客户端(避免纯文档提交推高版本号
// - Full Build:只在改动全部落在「与线上站点/后端无关」的路径时跳过(fail-open 到部署)。
stage('Resolve Release Scope') {
when {
expression { return env.REVISION_CHANGED == 'true' }
}
steps {
withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GENARRATIVE_GIT_SSH_KEY')]) {
script {
// 判定失败一律按「需要发布」处理,避免这段逻辑影响其它下游管线
def scope = 'changed'
// 判定失败一律按「两条都要跑」处理,避免这段逻辑影响下游发布
def output = 'agc=changed\nfull=changed'
try {
scope = sh(script: '''#!/usr/bin/env bash
output = sh(script: '''#!/usr/bin/env bash
set -uo pipefail
export GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY:?缺少 Git SSH 凭据} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
previous="$(cat "${REVISION_STATE_FILE}" 2>/dev/null || true)"
if [[ -z "${previous}" ]]; then
echo changed
printf 'agc=changed\nfull=changed\n'
exit 0
fi
mkdir -p "${AGC_SCOPE_CACHE_DIR}"
@@ -85,31 +86,46 @@ pipeline {
fi
refspec="+refs/heads/${SOURCE_BRANCH}:refs/remotes/origin/${SOURCE_BRANCH}"
if ! git -C "${AGC_SCOPE_CACHE_DIR}" fetch --quiet --depth=200 --no-tags --filter=blob:none origin "${refspec}"; then
git -C "${AGC_SCOPE_CACHE_DIR}" fetch --quiet --depth=200 --no-tags origin "${refspec}" || { echo changed; exit 0; }
git -C "${AGC_SCOPE_CACHE_DIR}" fetch --quiet --depth=200 --no-tags origin "${refspec}" || { printf 'agc=changed\nfull=changed\n'; exit 0; }
fi
if ! git -C "${AGC_SCOPE_CACHE_DIR}" cat-file -e "${previous}^{commit}" 2>/dev/null; then
echo "浅取窗口内没有 ${previous},按需要发布处理" >&2
echo changed
printf 'agc=changed\nfull=changed\n'
exit 0
fi
changed_paths="$(git -C "${AGC_SCOPE_CACHE_DIR}" diff --name-only "${previous}" "${REMOTE_REVISION}" 2>/dev/null || true)"
agc_scope=unchanged
full_scope=unchanged
while IFS= read -r changed_path; do
[[ -z "${changed_path}" ]] && continue
case "${changed_path}" in
apps/ai-game-creator-shell/*|packages/*|server-rs/crates/*|plugins/agc-cocos-editor/*|apps/desktop-shell/src-tauri/icons/*|package.json|package-lock.json)
echo changed
exit 0
agc_scope=changed
;;
esac
case "${changed_path}" in
docs/*|.codex/*|jenkins/*|apps/ai-game-creator-shell/*|apps/mobile-shell/*|apps/desktop-shell/*|apps/preview-deployer-web/*|tools/*|*.md)
;;
*)
full_scope=changed
;;
esac
done <<< "${changed_paths}"
echo unchanged
printf 'agc=%s\nfull=%s\n' "${agc_scope}" "${full_scope}"
''', returnStdout: true).trim()
} catch (error) {
echo "AGC 发布范围判定失败,按需要发布处理:${error}"
scope = 'changed'
echo "发布范围判定失败,按需要发布处理:${error}"
output = 'agc=changed\nfull=changed'
}
env.AGC_RELEASE_SCOPE = (scope == 'unchanged') ? 'unchanged' : 'changed'
echo "AGC 发布范围:${env.AGC_RELEASE_SCOPE}(上一轮已触发 revision=${env.LAST_TRIGGERED_REVISION ?: '无'}"
def values = output.split('\n').collect { it.trim() }.findAll { it }
def readScope = { String key ->
def entry = values.find { it.startsWith(key + '=') }
def value = entry == null ? '' : entry.split('=')[1]
return (value == 'unchanged') ? 'unchanged' : 'changed'
}
env.AGC_RELEASE_SCOPE = readScope('agc')
env.FULL_BUILD_SCOPE = readScope('full')
echo "发布范围:AGC=${env.AGC_RELEASE_SCOPE} FullBuild=${env.FULL_BUILD_SCOPE}(上一轮已触发 revision=${env.LAST_TRIGGERED_REVISION ?: '无'}"
}
}
}
@@ -128,7 +144,13 @@ pipeline {
string(name: 'COMMIT_HASH', value: pinnedRevision),
string(name: 'DATABASE_BACKUP_MODE', value: 'skip'),
]
build job: env.FULL_BUILD_JOB_NAME, wait: false, propagate: false, parameters: pinnedParameters
def fullTriggered = false
if (params.FORCE_TRIGGER || env.FULL_BUILD_SCOPE != 'unchanged') {
build job: env.FULL_BUILD_JOB_NAME, wait: false, propagate: false, parameters: pinnedParameters
fullTriggered = true
} else {
echo "本轮提交全部与线上站点/后端无关,跳过 ${env.FULL_BUILD_JOB_NAME};需要强制发布时勾选 FORCE_TRIGGER"
}
def agcTriggered = false
if (params.FORCE_TRIGGER || env.AGC_RELEASE_SCOPE != 'unchanged') {
build job: env.AGC_BUILD_JOB_NAME, wait: false, propagate: false, parameters: pinnedParameters
@@ -137,9 +159,13 @@ pipeline {
echo "本轮提交不含 AGC 相关路径,跳过 ${env.AGC_BUILD_JOB_NAME};需要强制发布时勾选 FORCE_TRIGGER"
}
writeFile file: env.REVISION_STATE_FILE, text: pinnedRevision
currentBuild.description = agcTriggered
? "已触发 ${env.FULL_BUILD_JOB_NAME} 与 ${env.AGC_BUILD_JOB_NAME}${env.SOURCE_BRANCH}@${pinnedRevision.take(12)}"
: "已触发 ${env.FULL_BUILD_JOB_NAME}AGC 渠道未发布:本次提交不含 AGC 相关路径):${env.SOURCE_BRANCH}@${pinnedRevision.take(12)}"
def triggered = []
if (fullTriggered) { triggered.add(env.FULL_BUILD_JOB_NAME) }
if (agcTriggered) { triggered.add(env.AGC_BUILD_JOB_NAME) }
def target = "${env.SOURCE_BRANCH}@${pinnedRevision.take(12)}"
currentBuild.description = triggered.isEmpty()
? "本轮提交与两条下游都无关,未触发任何发布:${target}"
: "已触发 ${triggered.join(' 与 ')}${target}"
echo currentBuild.description
}
}
@@ -1,7 +1,7 @@
<?xml version='1.1' encoding='UTF-8'?>
<flow-definition plugin="workflow-job">
<actions/>
<description>按小时检查源码分支版本,只有版本变化时用同一个 commit 触发 Full BuildAGC Windows Build 额外按变更路径过滤,只有本轮提交触及客户端相关路径时触发。</description>
<description>按小时检查源码分支版本,只有版本变化时用同一个 commit 触发下游;两条下游各自按变更路径过滤:AGC Windows Build 仅在本轮提交触及客户端相关路径时触发,Full Build 仅在本轮提交不只是文档 / 流水线 / 客户端改动时触发。</description>
<keepDependencies>false</keepDependencies>
<properties>
<hudson.model.ParametersDefinitionProperty>