Files
Genarrative/jenkins/Jenkinsfile.agc-global-version-issue
T
kdletters f947807fba
Project CI / AI game creator shell Rust crates (push) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (push) Successful in 3m56s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 7m17s
Project CI / Backend tests (push) Successful in 6m48s
Project CI / AI game creator shell Rust lane 1/2 (push) Failing after 9m58s
Project CI / Native shell tests (push) Successful in 8m17s
Project CI / Frontend tests (push) Successful in 5m58s
Project CI / AI game creator shell web tests (push) Successful in 3m35s
Project CI / Repository checks (push) Successful in 4m1s
修复手动发布的发号 Job Copy Artifact 授权
- 发号 Job 的 copyArtifactPermission 补齐手动发布消费者,用户触发的构建不再报 Unable to find project for artifact copy
- 同步总版本号技术方案与共享记忆排障记录,写明 SYSTEM 定时构建短路、用户触发按认证用户判权的差异与回写 Job property 的步骤
2026-09-21 08:54:17 +08:00

168 lines
8.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// AGC 总版本号发号 Job:唯一的发号入口。
//
// 设计要点(见 docs/technical/【技术方案】AGC总版本号与发号-2026-09-20.md):
// - 发号顺序固定为「先写总号 → 再构建 → 再发渠道清单」,失败不回滚,只烧号;
// - 各渠道构建只接收号(AGC_RELEASE_VERSION),不自己加号;
// - 本 Job 用 disableConcurrentBuilds() 串行化发号(集群未安装 lockable-resources
// 插件,因此以 Job 级串行 + 写后回读校验兜底并发);
// - dry-run 只预览下一位,不写回、不烧号。
pipeline {
agent {
label 'linux && genarrative-build'
}
options {
disableConcurrentBuilds()
skipDefaultCheckout(true)
buildDiscarder(logRotator(numToKeepStr: '100', artifactNumToKeepStr: '20'))
// Copy Artifact 保持 Production 权限模式:生产者必须显式授权消费者。
// 调度管线与手动发布管线都要 copy 本 Job 的 agc-global-version.txt 才能把总号
// 透传给渠道构建;缺授权时 copyArtifacts 会报「Unable to find project for
// artifact copy」(用户触发的构建按「认证用户」判权,SYSTEM 定时构建会短路放行,
// 所以定时调度能过、手动发布必挂;2026-09-20 #103 与 2026-09-21 手动发布
// #6/#7 均命中)。这里必须同时列出两条消费者:Genarrative-Scheduled-Revision-Trigger
// 与 Genarrative-Manual-Build-And-Deploy。改完本文件后必须先单独跑一次本 Job,
// 让 Declarative Pipeline 把 Job property 写回 Jenkins,再重跑调度或手动发布。
copyArtifactPermission('Genarrative-Scheduled-Revision-Trigger,Genarrative-Manual-Build-And-Deploy')
}
environment {
GIT_REMOTE_URL = 'ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git'
GIT_REMOTE_CREDENTIAL_ID = 'genarrative-local-gitea-ssh'
GLOBAL_VERSION_OUT = 'agc-global-version.txt'
OSSUTIL_TOOLS_DIR = '.jenkins-ossutil'
}
parameters {
choice(name: 'AGC_CHANNEL', choices: ['unified', 'dev-win', 'dev-mac'], description: '发号用途:unified 为统一构建(一个号同时给 dev-win 与 dev-mac),其余为单渠道热修')
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '取发号脚本的源码分支;默认 master')
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选:固定提交;留空则解析 SOURCE_BRANCH 当前 revision')
booleanParam(name: 'SEED_ONLY', defaultValue: false, description: '仅做一次性播种:写入渠道与仓库版本的最大值作为基线,不递增、不烧号')
booleanParam(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false, description: '勾选后只预览下一个总号,不写回、不烧号')
string(name: 'OSSUTIL_BIN', defaultValue: 'ossutil', description: 'ossutil 命令名或绝对路径;缺省时本 Job 会下载到工作区私有目录')
}
stages {
stage('Checkout') {
steps {
deleteDir()
script {
def checkoutFromRemote = { String remoteUrl, String credentialsId ->
checkout([
$class: 'GitSCM',
branches: [[name: params.COMMIT_HASH?.trim() ?: "*/${params.SOURCE_BRANCH}"]],
doGenerateSubmoduleConfigurations: false,
extensions: [
[$class: 'CleanBeforeCheckout'],
[$class: 'CloneOption', shallow: true, depth: 1, noTags: true, timeout: 30, honorRefspec: true],
],
userRemoteConfigs: [[
url: remoteUrl,
credentialsId: credentialsId,
refspec: "+refs/heads/${params.SOURCE_BRANCH}:refs/remotes/origin/${params.SOURCE_BRANCH}",
]],
])
}
checkoutFromRemote(env.GIT_REMOTE_URL, env.GIT_REMOTE_CREDENTIAL_ID)
env.SOURCE_COMMIT = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
if (params.COMMIT_HASH?.trim() && env.SOURCE_COMMIT != params.COMMIT_HASH.trim()) {
// 浅取分支不再含目标提交时立即失败,避免拿到错版本发号。
def contains = sh(
script: "git merge-base --is-ancestor ${params.COMMIT_HASH.trim()} HEAD && echo yes || echo no",
returnStdout: true,
).trim()
if (contains != 'yes') {
error("COMMIT_HASH ${params.COMMIT_HASH.trim()} 不属于 ${params.SOURCE_BRANCH}")
}
sh "git checkout --detach ${params.COMMIT_HASH.trim()}"
env.SOURCE_COMMIT = params.COMMIT_HASH.trim()
}
}
}
}
stage('Ensure ossutil') {
steps {
script {
def requested = params.OSSUTIL_BIN?.trim() ?: 'ossutil'
def resolved = sh(
script: "command -v ${requested} 2>/dev/null || true",
returnStdout: true,
).trim()
if (resolved) {
env.EFFECTIVE_OSSUTIL_BIN = resolved
} else {
// 构建节点没有 ossutil 时下载到工作区私有目录,不改主机环境。
sh '''#!/usr/bin/env bash
set -euo pipefail
version="${AGC_OSSUTIL_VERSION:-2.1.2}"
mkdir -p "${OSSUTIL_TOOLS_DIR}"
if [[ ! -x "${OSSUTIL_TOOLS_DIR}/ossutil" ]]; then
curl -fsSL -o "${OSSUTIL_TOOLS_DIR}/ossutil.zip" \\
"https://gosspublic.alicdn.com/ossutil/v2/${version}/ossutil-${version}-linux-amd64.zip"
# 官方 zip 解压后是带版本号的目录,二进制不在包根,必须先定位再落到固定路径。
unzip -o -q "${OSSUTIL_TOOLS_DIR}/ossutil.zip" -d "${OSSUTIL_TOOLS_DIR}"
extracted="$(find "${OSSUTIL_TOOLS_DIR}" -maxdepth 3 -type f -name ossutil -print -quit)"
if [[ -z "${extracted}" ]]; then
echo "ossutil 解压后未找到可执行文件:${OSSUTIL_TOOLS_DIR}" >&2
exit 1
fi
cp -f "${extracted}" "${OSSUTIL_TOOLS_DIR}/ossutil"
chmod +x "${OSSUTIL_TOOLS_DIR}/ossutil"
fi
# ossutil v2 没有 `--version` 选项,这里用可执行性校验代替版本打印,避免误判失败。
if [[ ! -x "${OSSUTIL_TOOLS_DIR}/ossutil" ]]; then
echo "ossutil 未就绪:${OSSUTIL_TOOLS_DIR}/ossutil" >&2
exit 1
fi
echo "ossutil ready: ${OSSUTIL_TOOLS_DIR}/ossutil"
'''
env.EFFECTIVE_OSSUTIL_BIN = "${env.WORKSPACE}/${env.OSSUTIL_TOOLS_DIR}/ossutil"
}
}
}
}
stage('Issue Global Version') {
steps {
withCredentials([
string(credentialsId: 'AliyunAccessKeyId', variable: 'AGC_OSS_ACCESS_KEY_ID'),
string(credentialsId: 'AliyunaccessKeySecret', variable: 'AGC_OSS_ACCESS_KEY_SECRET'),
]) {
withEnv([
"OSSUTIL_BIN=${env.EFFECTIVE_OSSUTIL_BIN}",
"AGC_RELEASE_DRY_RUN=${params.AGC_RELEASE_DRY_RUN ? '1' : '0'}",
]) {
sh '''#!/usr/bin/env bash
set -euo pipefail
seed_args=()
if [[ "${SEED_ONLY}" == "true" ]]; then seed_args+=(--seed-only); fi
dry_args=()
if [[ "${AGC_RELEASE_DRY_RUN}" == "1" ]]; then dry_args+=(--dry-run); fi
node apps/ai-game-creator-shell/scripts/issue-global-version.mjs \\
--channel "${AGC_CHANNEL}" \\
--commit "${SOURCE_COMMIT}" \\
--build-id "${BUILD_NUMBER}" \\
--out "${GLOBAL_VERSION_OUT}" \\
"${seed_args[@]}" "${dry_args[@]}"
'''
script {
env.AGC_GLOBAL_VERSION = readFile(env.GLOBAL_VERSION_OUT).trim()
currentBuild.description =
"AGC 总版本号 ${env.AGC_GLOBAL_VERSION}channel=${params.AGC_CHANNEL}" +
"${params.SEED_ONLY ? ',播种' : ''}${params.AGC_RELEASE_DRY_RUN ? 'dry-run' : ''}"
echo currentBuild.description
}
}
}
}
}
}
post {
always {
archiveArtifacts artifacts: "agc-global-version.txt", allowEmptyArchive: true, fingerprint: true
}
}
}