Add production Jenkins release pipelines
This commit is contained in:
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
# 复制到 /etc/genarrative/api-server.env 后再填入真实生产值。
|
||||
# 该文件只能保存在生产服务器,不进入构建产物,不提交真实密钥。
|
||||
|
||||
GENARRATIVE_ENV=production
|
||||
GENARRATIVE_API_HOST=127.0.0.1
|
||||
GENARRATIVE_API_PORT=8082
|
||||
GENARRATIVE_API_LOG=info,tower_http=info
|
||||
|
||||
GENARRATIVE_ADMIN_USERNAME=
|
||||
GENARRATIVE_ADMIN_PASSWORD=
|
||||
GENARRATIVE_ADMIN_TOKEN_TTL_SECONDS=14400
|
||||
GENARRATIVE_INTERNAL_API_SECRET=CHANGE_ME_FOR_PRODUCTION
|
||||
|
||||
GENARRATIVE_JWT_ISSUER=genarrative-production
|
||||
GENARRATIVE_JWT_SECRET=CHANGE_ME_FOR_PRODUCTION
|
||||
GENARRATIVE_JWT_ACCESS_TOKEN_TTL_SECONDS=7200
|
||||
AUTH_REFRESH_COOKIE_NAME=genarrative_refresh_session
|
||||
AUTH_REFRESH_SESSION_TTL_DAYS=30
|
||||
AUTH_REFRESH_COOKIE_PATH=/api/auth
|
||||
AUTH_REFRESH_COOKIE_SAME_SITE=Lax
|
||||
AUTH_REFRESH_COOKIE_SECURE=true
|
||||
GENARRATIVE_AUTH_STORE_PATH=/var/lib/genarrative/auth/auth-store.json
|
||||
GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=false
|
||||
|
||||
GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3000
|
||||
GENARRATIVE_SPACETIME_DATABASE=genarrative-prod
|
||||
GENARRATIVE_SPACETIME_TOKEN=
|
||||
GENARRATIVE_SPACETIME_POOL_SIZE=8
|
||||
GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS=45
|
||||
|
||||
GENARRATIVE_LLM_PROVIDER=openai-compatible
|
||||
GENARRATIVE_LLM_BASE_URL=
|
||||
GENARRATIVE_LLM_API_KEY=
|
||||
GENARRATIVE_LLM_MODEL=
|
||||
GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED=false
|
||||
GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED=false
|
||||
|
||||
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||
DASHSCOPE_API_KEY=
|
||||
DASHSCOPE_IMAGE_MODEL=wan2.7-image
|
||||
DASHSCOPE_IMAGE_REQUEST_TIMEOUT_MS=150000
|
||||
DASHSCOPE_CHARACTER_VISUAL_MODEL=wan2.7-image-pro
|
||||
DASHSCOPE_CHARACTER_IMAGE_SEQUENCE_MODEL=wan2.7-image-pro
|
||||
DASHSCOPE_CHARACTER_REFERENCE_VIDEO_MODEL=wan2.7-r2v
|
||||
DASHSCOPE_CHARACTER_MOTION_TRANSFER_MODEL=wan2.2-animate-move
|
||||
DASHSCOPE_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
|
||||
|
||||
SMS_AUTH_ENABLED=false
|
||||
SMS_AUTH_PROVIDER=aliyun
|
||||
ALIYUN_SMS_ACCESS_KEY_ID=
|
||||
ALIYUN_SMS_ACCESS_KEY_SECRET=
|
||||
ALIYUN_SMS_ENDPOINT=dypnsapi.aliyuncs.com
|
||||
ALIYUN_SMS_SIGN_NAME=
|
||||
ALIYUN_SMS_TEMPLATE_CODE=
|
||||
ALIYUN_SMS_TEMPLATE_PARAM_KEY=code
|
||||
ALIYUN_SMS_COUNTRY_CODE=86
|
||||
|
||||
WECHAT_AUTH_ENABLED=false
|
||||
WECHAT_AUTH_PROVIDER=real
|
||||
WECHAT_APP_ID=
|
||||
WECHAT_APP_SECRET=
|
||||
WECHAT_CALLBACK_PATH=/api/auth/wechat/callback
|
||||
WECHAT_REDIRECT_PATH=/
|
||||
WECHAT_AUTHORIZE_ENDPOINT=https://open.weixin.qq.com/connect/qrconnect
|
||||
WECHAT_ACCESS_TOKEN_ENDPOINT=https://api.weixin.qq.com/sns/oauth2/access_token
|
||||
WECHAT_USER_INFO_ENDPOINT=https://api.weixin.qq.com/sns/userinfo
|
||||
WECHAT_STATE_TTL_MINUTES=15
|
||||
|
||||
ALIYUN_OSS_BUCKET=
|
||||
ALIYUN_OSS_ENDPOINT=oss-cn-shanghai.aliyuncs.com
|
||||
ALIYUN_OSS_ACCESS_KEY_ID=
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET=
|
||||
ALIYUN_OSS_READ_EXPIRE_SECONDS=600
|
||||
ALIYUN_OSS_POST_EXPIRE_SECONDS=600
|
||||
ALIYUN_OSS_POST_MAX_SIZE_BYTES=20971520
|
||||
ALIYUN_OSS_SUCCESS_ACTION_STATUS=200
|
||||
@@ -0,0 +1,101 @@
|
||||
# 生产域名需要在部署前替换为真实域名,并由 certbot 或等价流程写入 HTTPS 证书配置。
|
||||
server {
|
||||
listen 80;
|
||||
server_name genarrative.example.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name genarrative.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/genarrative.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/genarrative.example.com/privkey.pem;
|
||||
|
||||
root /srv/genarrative/web;
|
||||
index index.html;
|
||||
|
||||
include /etc/nginx/snippets/genarrative-maintenance.conf;
|
||||
|
||||
location ^~ /admin/api/ {
|
||||
default_type application/json;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}';
|
||||
}
|
||||
|
||||
proxy_pass http://127.0.0.1:8082/admin/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Request-Id $request_id;
|
||||
}
|
||||
|
||||
location = /admin {
|
||||
return 301 /admin/;
|
||||
}
|
||||
|
||||
location ^~ /admin/assets/ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ^~ /admin/ {
|
||||
error_page 503 /maintenance.html;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
}
|
||||
|
||||
location ^~ /assets/ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# 生产公网不再暴露旧一体化 API、生成资源代理和健康检查入口。
|
||||
location ~ ^/(api|generated-|healthz) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# SpacetimeDB 只开放 TypeScript SDK 运行所需的最小公网路由。
|
||||
location ~ ^/v1/database/[^/]+/subscribe$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
location ^~ /v1/identity {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location ^~ /v1/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
error_page 503 /maintenance.html;
|
||||
|
||||
if ($genarrative_maintenance) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# 维护模式由发布脚本或人工运维通过固定文件控制。
|
||||
# 文件存在时,普通页面展示维护页,管理 API 返回 503。
|
||||
set $genarrative_maintenance 0;
|
||||
if (-f /var/lib/genarrative/maintenance/enabled) {
|
||||
set $genarrative_maintenance 1;
|
||||
}
|
||||
|
||||
location = /maintenance.html {
|
||||
root /srv/genarrative/web;
|
||||
add_header Cache-Control "no-store";
|
||||
internal;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=Genarrative Rust API Server
|
||||
After=network-online.target spacetimedb.service
|
||||
Wants=network-online.target
|
||||
Requires=spacetimedb.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=genarrative
|
||||
Group=genarrative
|
||||
WorkingDirectory=/opt/genarrative/current
|
||||
EnvironmentFile=/etc/genarrative/api-server.env
|
||||
ExecStart=/opt/genarrative/current/api-server
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=30
|
||||
|
||||
# api-server 只读发布目录,运行态写入必须显式落到环境变量指定的服务端私有目录。
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/opt/genarrative /var/lib/genarrative
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=SpacetimeDB Server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=spacetimedb
|
||||
Group=spacetimedb
|
||||
WorkingDirectory=/stdb
|
||||
ExecStart=/stdb/spacetime --root-dir=/stdb start --listen-addr=127.0.0.1:3000
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
LimitNOFILE=1048576
|
||||
|
||||
# 生产库只监听本机端口,由 Nginx 暴露最小客户端路由。
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ReadWritePaths=/stdb
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+2
-2
@@ -13,14 +13,14 @@
|
||||
重点补充:RPG 创作与运行时脚本职责地图见 [RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md](./reference/RPG_CREATION_AND_RUNTIME_SCRIPT_RESPONSIBILITY_MAP_2026-04-28.md)。
|
||||
- [PRD](./prd):产品需求与阶段计划;后台管理独立前端工程见 [ADMIN_WEB_CONSOLE_PRD_2026-04-30.md](./prd/ADMIN_WEB_CONSOLE_PRD_2026-04-30.md),新增 RPG 开场动画方案见 [AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md](./prd/AI_NATIVE_RPG_OPENING_ANIMATION_PRD_2026-04-25.md),新增抓大鹅 Match3D 玩法方案见 [AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md](./prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md)。
|
||||
|
||||
SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和 Jenkins 数据库迁移流水线见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
|
||||
生产部署切换到 systemd + Nginx + SpacetimeDB 自托管的总方案见 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),该文档也是当前生产 Jenkinsfile 的唯一入口。SpacetimeDB 表结构变更、自动迁移边界和保留旧数据的分阶段迁移流程见 [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md);private 表迁移 JSON 导入导出、HTTP 413 分片导入和旧数据库迁移流水线经验见 [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./technical/SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md) 与 [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md);后台管理独立前端工程技术方案见 [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./technical/ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md)。
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
|
||||
2. 再看 [工程审查总览](./audits/engineering/README.md) 和 [文本审计总览](./audits/text/README.md),了解当前风险。
|
||||
3. 需要排期时看 [规划与优先级](./planning/README.md)。
|
||||
4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md);涉及后端先看 [当前后端实现基线](./technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md),涉及 SpacetimeDB 表结构变更时再看 [表结构变更约束](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
|
||||
4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md);涉及后端先看 [当前后端实现基线](./technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md),涉及生产发布链路先看 [生产部署计划](./technical/PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md),涉及 SpacetimeDB 表结构变更时再看 [表结构变更约束](./technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md)。
|
||||
5. 需要对齐目标边界时再进入 [PRD](./prd)。
|
||||
|
||||
## 分类规则
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
日期:`2026-04-23`
|
||||
|
||||
状态:历史方案,已被 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 中的 `jenkins/Jenkinsfile.production-*` 生产流水线替代。旧 Jenkinsfile 已从仓库删除,本文只保留旧本地目录部署链的设计背景和迁移参考,不能再作为新 Jenkins Job 的脚本路径来源。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本方案为当前仓库补齐 3 条 Jenkins 流水线:
|
||||
@@ -47,11 +49,7 @@
|
||||
|
||||
### 4.1 构建
|
||||
|
||||
脚本路径:
|
||||
|
||||
```text
|
||||
jenkins/Jenkinsfile.build
|
||||
```
|
||||
旧脚本路径:`jenkins/Jenkinsfile.build`。该文件当前仓库已不存在;生产构建改用 `jenkins/Jenkinsfile.production-web-build`、`jenkins/Jenkinsfile.production-api-build`、`jenkins/Jenkinsfile.production-stdb-module-build`。
|
||||
|
||||
核心流程:
|
||||
|
||||
@@ -74,11 +72,7 @@ BUILD_VERSION = Jenkins BUILD_NUMBER
|
||||
|
||||
### 4.2 部署
|
||||
|
||||
脚本路径:
|
||||
|
||||
```text
|
||||
jenkins/Jenkinsfile.deploy
|
||||
```
|
||||
旧脚本路径:`jenkins/Jenkinsfile.deploy`。该文件当前仓库已删除;生产发布改用 `jenkins/Jenkinsfile.production-web-deploy`、`jenkins/Jenkinsfile.production-api-deploy`、`jenkins/Jenkinsfile.production-stdb-module-publish`。
|
||||
|
||||
核心流程:
|
||||
|
||||
@@ -114,11 +108,7 @@ scripts/jenkins-deploy-release.sh \
|
||||
|
||||
### 4.3 构建并部署
|
||||
|
||||
脚本路径:
|
||||
|
||||
```text
|
||||
jenkins/Jenkinsfile.build-and-deploy
|
||||
```
|
||||
旧脚本路径:`jenkins/Jenkinsfile.build-and-deploy`。该文件当前仓库已删除;全量编排改用 `jenkins/Jenkinsfile.production-full-build-and-deploy`,并按 Web / API / Stdb 并行构建、Stdb / API / Web 顺序发布执行。
|
||||
|
||||
核心流程:
|
||||
|
||||
@@ -198,11 +188,11 @@ jenkins ALL=(root) NOPASSWD: /var/lib/jenkins/deploy/Genarrative/stop.sh
|
||||
|
||||
## 6. 推荐 Job 命名
|
||||
|
||||
建议在 Jenkins 中创建以下 3 个 Pipeline Job,并分别指向仓库中的脚本路径:
|
||||
以下旧 Job 命名只保留为历史记录,不再创建或关联到仓库脚本:
|
||||
|
||||
1. `Genarrative-Build` -> `jenkins/Jenkinsfile.build`
|
||||
2. `Genarrative-Deploy` -> `jenkins/Jenkinsfile.deploy`
|
||||
3. `Genarrative-Build-And-Deploy` -> `jenkins/Jenkinsfile.build-and-deploy`
|
||||
1. `Genarrative-Build`
|
||||
2. `Genarrative-Deploy`
|
||||
3. `Genarrative-Build-And-Deploy`
|
||||
|
||||
同时给 `Genarrative-Deploy` 配置环境变量:
|
||||
|
||||
@@ -218,14 +208,7 @@ game/Genarrative-Build-And-Deploy
|
||||
|
||||
## 7. 文件清单
|
||||
|
||||
本方案对应的仓库文件:
|
||||
|
||||
```text
|
||||
jenkins/Jenkinsfile.build
|
||||
jenkins/Jenkinsfile.deploy
|
||||
jenkins/Jenkinsfile.build-and-deploy
|
||||
scripts/jenkins-deploy-release.sh
|
||||
```
|
||||
本方案原对应 Jenkinsfile 已删除。生产发布链的当前文件清单见 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`;旧部署脚本如 `scripts/jenkins-deploy-release.sh` 只作为旧发布包链路参考,不再作为生产 Jenkins 入口。
|
||||
|
||||
## 8. 风险与边界
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
日期:`2026-04-29`
|
||||
|
||||
状态:历史方案。旧数据库导入导出 Jenkinsfile 已从仓库删除,生产版 `Genarrative-Database-Export` / `Genarrative-Database-Import` 已按 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 落地到 `jenkins/Jenkinsfile.production-database-export` 与 `jenkins/Jenkinsfile.production-database-import`。本文只保留迁移脚本参数、权限边界和 `CHUNK_SIZE` 等经验;生产 Job 入口以生产部署计划和 `jenkins/Jenkinsfile.production-*` 为准。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
为 Jenkins 增加两条人工触发的数据库迁移流水线:
|
||||
@@ -24,11 +26,7 @@
|
||||
|
||||
### 3.1 数据库导出
|
||||
|
||||
脚本路径:
|
||||
|
||||
```text
|
||||
jenkins/Jenkinsfile.database-export
|
||||
```
|
||||
旧脚本路径:`jenkins/Jenkinsfile.database-export`。该文件当前仓库已删除;生产版入口为 `jenkins/Jenkinsfile.production-database-export`。
|
||||
|
||||
推荐作业名:
|
||||
|
||||
@@ -38,28 +36,30 @@ Genarrative-Database-Export
|
||||
|
||||
关键参数:
|
||||
|
||||
1. `DATABASE`:目标 SpacetimeDB 数据库名;留空时读取仓库环境变量。
|
||||
2. `SERVER`:SpacetimeDB server 别名,默认 `maincloud`。
|
||||
3. `SERVER_URL`:显式服务地址;填写后优先于 `SERVER`。
|
||||
4. `DEPLOY_DIRECTORY`:固定部署目录,默认 `/var/lib/jenkins/deploy/Genarrative`。
|
||||
5. `ROOT_DIR`:可选,透传给 `spacetime --root-dir`;为空时使用 `<DEPLOY_DIRECTORY>/.spacetimedb`。
|
||||
6. `INCLUDE_TABLES`:可选,逗号分隔的表名白名单。
|
||||
7. `OUTPUT_DIRECTORY`:导出文件目录,默认 `database-exports`。
|
||||
8. `EXPORT_NAME`:导出文件名;留空时使用 `spacetime-migration-<BUILD_NUMBER>.json`。
|
||||
1. `DEPLOY_TARGET`:逻辑导出目标,`development` 映射到 `linux && genarrative-build`,`release` 映射到 `linux && genarrative-release-deploy`。
|
||||
2. `CONFIRM_RELEASE_DEPLOY_AGENT`:`DEPLOY_TARGET=release` 时必填确认。
|
||||
3. `SOURCE_BRANCH` / `COMMIT_HASH`:固定本次执行的迁移脚本版本。
|
||||
4. `DATABASE`:必填,目标 SpacetimeDB 数据库名。
|
||||
5. `SPACETIME_SERVER`:SpacetimeDB server 别名,默认 `local`。
|
||||
6. `SPACETIME_SERVER_URL`:显式服务地址;填写后优先于 `SPACETIME_SERVER`。
|
||||
7. `SPACETIME_ROOT_DIR`:`spacetime --root-dir`,默认 `/stdb`。
|
||||
8. `INCLUDE_TABLES`:可选,逗号分隔的表名白名单。
|
||||
9. `WORKSPACE_EXPORT_DIRECTORY`:Jenkins workspace 内导出目录,默认 `database-exports`。
|
||||
10. `SERVER_BACKUP_DIRECTORY`:可选,目标机器上的额外备份目录;留空则不保存服务器副本。
|
||||
11. `EXPORT_NAME`:导出文件名;留空时使用 `spacetime-migration-<BUILD_NUMBER>.json`。
|
||||
12. `TOKEN_CREDENTIAL_ID`:可选,已授权迁移 operator token 的 Jenkins Secret Text 凭据 ID。
|
||||
13. `BOOTSTRAP_SECRET_CREDENTIAL_ID`:可选,迁移 bootstrap secret 的 Jenkins Secret Text 凭据 ID。
|
||||
|
||||
导出成功后,Jenkins 归档:
|
||||
|
||||
```text
|
||||
<OUTPUT_DIRECTORY>/<EXPORT_NAME>
|
||||
<WORKSPACE_EXPORT_DIRECTORY>/<EXPORT_NAME>
|
||||
<WORKSPACE_EXPORT_DIRECTORY>/<EXPORT_NAME>.sha256
|
||||
```
|
||||
|
||||
### 3.2 数据库导入
|
||||
|
||||
脚本路径:
|
||||
|
||||
```text
|
||||
jenkins/Jenkinsfile.database-import
|
||||
```
|
||||
旧脚本路径:`jenkins/Jenkinsfile.database-import`。该文件当前仓库已删除;生产版入口为 `jenkins/Jenkinsfile.production-database-import`。
|
||||
|
||||
推荐作业名:
|
||||
|
||||
@@ -69,25 +69,29 @@ Genarrative-Database-Import
|
||||
|
||||
关键参数:
|
||||
|
||||
1. `INPUT_FILE`:必填,迁移 JSON 文件路径。
|
||||
2. `DATABASE`、`SERVER`、`SERVER_URL`、`DEPLOY_DIRECTORY`、`ROOT_DIR`:与导出流水线一致。
|
||||
3. `INCLUDE_TABLES`:可选,只导入指定表。
|
||||
4. `CHUNK_SIZE`:迁移 JSON 分片大小,默认 `524288` bytes。导入脚本会在文件超过该大小或直接导入触发 HTTP 413 时自动分片上传。
|
||||
5. `DRY_RUN`:默认 `true`,只校验不写入。
|
||||
6. `INCREMENTAL`:默认 `true`,跳过已存在或冲突的行。
|
||||
7. `REPLACE_EXISTING`:默认 `false`,只覆盖本次迁移文件中涉及的表;不可与 `INCREMENTAL` 同时启用。
|
||||
8. `BOOTSTRAP_SECRET`:可选,用于授权临时 Web API identity。
|
||||
9. `TOKEN`:可选,SpacetimeDB 客户端连接 token;留空时脚本会自动创建临时 identity 并在结束后撤销。
|
||||
10. `NOTE`:迁移授权备注。
|
||||
1. `INPUT_SOURCE`:必填,`pipeline_archive` 表示从 `Genarrative-Database-Export` 归档复制输入文件,`manual_upload` 表示本次构建手动上传数据源;两种方式互斥。
|
||||
2. `EXPORT_JOB_NAME` / `EXPORT_BUILD_NUMBER_TO_IMPORT` / `INPUT_FILE`:仅 `pipeline_archive` 模式使用。`EXPORT_JOB_NAME` 默认是导出流水线 `Genarrative-Database-Export`;`INPUT_FILE` 可留空,留空时按导出流水线默认归档路径解析为 `database-exports/spacetime-migration-<导出构建号>.json`。
|
||||
3. `MANUAL_INPUT_FILE`:仅 `manual_upload` 模式使用,Jenkins 通过 file parameter 接收本次构建上传文件。
|
||||
4. `DATABASE`、`SERVER`、`SERVER_URL`、`DEPLOY_DIRECTORY`、`ROOT_DIR`:与导出流水线一致。
|
||||
5. `INCLUDE_TABLES`:可选,只导入指定表。
|
||||
6. `CHUNK_SIZE`:迁移 JSON 分片大小,默认 `524288` bytes。导入脚本会在文件超过该大小或直接导入触发 HTTP 413 时自动分片上传。
|
||||
7. `DRY_RUN`:默认 `true`,只校验不写入。
|
||||
8. `INCREMENTAL`:默认 `true`,跳过已存在或冲突的行。
|
||||
9. `REPLACE_EXISTING`:默认 `false`,只覆盖本次迁移文件中涉及的表;不可与 `INCREMENTAL` 同时启用。
|
||||
10. `BOOTSTRAP_SECRET`:可选,用于授权临时 Web API identity。
|
||||
11. `TOKEN`:可选,SpacetimeDB 客户端连接 token;留空时脚本会自动创建临时 identity 并在结束后撤销。
|
||||
12. `NOTE`:迁移授权备注。
|
||||
|
||||
## 4. 安全边界
|
||||
|
||||
1. 导入流水线默认 `DRY_RUN=true`,需要人工明确关闭才会写入数据。
|
||||
2. `INCREMENTAL` 与 `REPLACE_EXISTING` 互斥,Jenkinsfile 会在执行前阻止同时启用。
|
||||
3. Jenkinsfile 不打印 token;生产环境应通过 Jenkins 凭据或目标机器环境变量传入敏感值。
|
||||
4. 如果不传 `TOKEN`,导入脚本会创建临时 Web API identity,并调用迁移授权/撤销 procedure 收敛权限窗口。
|
||||
5. 导入导出流水线在调用仓库内迁移脚本前都会执行 `git reset --hard HEAD`,确保固定源码目录中的本地改动不会影响本次迁移操作。
|
||||
6. 如果日志出现 `SpacetimeDB HTTP 413: Failed to buffer the request body: length limit exceeded`,优先把 `CHUNK_SIZE` 调低到 `262144` 或更小后重跑。该参数只降低单次 HTTP body,不改变导入表范围。
|
||||
3. `INPUT_SOURCE=pipeline_archive` 时必须填写 `EXPORT_BUILD_NUMBER_TO_IMPORT`;`EXPORT_JOB_NAME` 默认使用导出流水线名称,`INPUT_FILE` 默认使用导出流水线默认归档路径,只有导出时自定义目录或文件名才需要显式填写。
|
||||
4. `INPUT_SOURCE=manual_upload` 时必须上传 `MANUAL_INPUT_FILE`,并把 `CONFIRM_INPUT_FILE` 填成原始文件名;`EXPORT_JOB_NAME` 的默认值可以保留,不参与该模式的输入边界。
|
||||
5. Jenkinsfile 不打印 token;生产环境应通过 Jenkins 凭据或目标机器环境变量传入敏感值。
|
||||
6. 如果不传 `TOKEN`,导入脚本会创建临时 Web API identity,并调用迁移授权/撤销 procedure 收敛权限窗口。
|
||||
7. 导入导出流水线在调用仓库内迁移脚本前都会执行 `git reset --hard HEAD`,确保固定源码目录中的本地改动不会影响本次迁移操作。
|
||||
8. 如果日志出现 `SpacetimeDB HTTP 413: Failed to buffer the request body: length limit exceeded`,优先把 `CHUNK_SIZE` 调低到 `262144` 或更小后重跑。该参数只降低单次 HTTP body,不改变导入表范围。
|
||||
|
||||
## 5. 本地部署测试参数
|
||||
|
||||
@@ -112,7 +116,7 @@ DEPLOY_DIRECTORY=/var/lib/jenkins/deploy/Genarrative
|
||||
## 6. 文件清单
|
||||
|
||||
```text
|
||||
jenkins/Jenkinsfile.database-export
|
||||
jenkins/Jenkinsfile.database-import
|
||||
jenkins/Jenkinsfile.production-database-export
|
||||
jenkins/Jenkinsfile.production-database-import
|
||||
docs/technical/JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md
|
||||
```
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
||||
## 文档列表
|
||||
|
||||
- [PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md](./PRODUCT_NAMING_BAIMENG_RENAME_2026-05-01.md):冻结当前对外中文命名,产品展示名统一为“百梦”,消费单位为“光点”,公开账号标识为“百梦号”,创作侧称谓为“百梦主”。
|
||||
- [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md):冻结单机生产部署目标,从旧一体化启动脚本切到 Nginx、systemd 托管 SpacetimeDB 与 Rust `api-server`,并记录生产 Jenkins 流水线拆分计划和首批部署骨架。
|
||||
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
|
||||
- [SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md](./SPACETIMEDB_LOCAL_REPLICA_IDENTITY_MISMATCH_FIX_2026-04-30.md):记录本地 standalone 启动时报 `mismatched database identity` 的 root-dir/replica 数据残留根因、备份重建步骤和脚本诊断口径。
|
||||
- [AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md](./AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md):记录 Maincloud `xushi-p4wfr` 挂起导致认证快照同步和抓大鹅创作失败的根因、认证同步非阻断修复、`/api/creation` Vite 代理补齐和本地 SpacetimeDB 可跑链路。
|
||||
@@ -19,7 +20,7 @@
|
||||
- [MATCH3D_Q1_INTEGRATION_ACCEPTANCE_2026-05-01.md](./MATCH3D_Q1_INTEGRATION_ACCEPTANCE_2026-05-01.md):记录抓大鹅 Match3D 第一至第三波完成度复核、Q1 主链集成落点、定向验收命令和遗留风险。
|
||||
- [PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md](./PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md):记录平台首页底部 dock 在手机浏览器地址栏展开时脱离可见区域的根因,以及 `100dvh`、固定底部锚点和安全区占位的修复口径。
|
||||
- [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md):记录 SpacetimeDB private 表迁移 JSON 导出/导入 procedure、迁移操作员授权、HTTP 413 分片导入、Jenkins 自动迁移回灌和导入脚本参数。
|
||||
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):记录 `Genarrative-Database-Export` / `Genarrative-Database-Import` 两条 SCM-backed 数据库迁移流水线参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避参数。
|
||||
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):历史记录,保留 `Genarrative-Database-Export` / `Genarrative-Database-Import` 迁移参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避经验;旧 Jenkinsfile 已删除,生产版入口以 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 为准。
|
||||
- [ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md](./ADMIN_WEB_CONSOLE_TECHNICAL_SOLUTION_2026-04-30.md):冻结后台管理独立前端工程 `apps/admin-web` 的技术方案,明确管理端只做表现、全部数据和写操作走 `server-rs` 的 `/admin/api/*`,并接管旧 `GET /admin` 内嵌页面的 UI 职责。
|
||||
- [RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md](./RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md):冻结 RPG 提示词禁止存在前端的边界,明确前端只保留 API client,角色私聊/NPC 对话/剧情续写等 prompt 统一收口到 `server-rs`。
|
||||
- [RPG_CREATION_RESULT_VIEW_BACKEND_TRUTH_MIGRATION_2026-04-28.md](./RPG_CREATION_RESULT_VIEW_BACKEND_TRUTH_MIGRATION_2026-04-28.md):冻结 RPG 创作结果页保存、Agent session/result preview 真相优先级和结果页入口裁决迁移到后端 result-view 的落地边界。
|
||||
@@ -82,9 +83,9 @@
|
||||
- [CREATION_AGENT_PUBLISH_GATE_SCHEMA_ALIGNMENT_FIX_2026-04-23.md](./CREATION_AGENT_PUBLISH_GATE_SCHEMA_ALIGNMENT_FIX_2026-04-23.md):记录发布阻断项仍按旧 `worldHook / playerPremise / sceneChapters` schema 校验的问题,以及将 Rust `publish gate` 对齐到 `anchorContent / creatorIntent / sceneChapterBlueprints` 当前主链结构的修复口径。
|
||||
- [CREATION_HUB_CARD_ACTIONS_2026-04-22.md](./CREATION_HUB_CARD_ACTIONS_2026-04-22.md):冻结创作中心作品卡“体验 / 删除”入口的最小落地语义,明确 RPG 已发布作品软删除、卡片直达运行时,以及暂不扩草稿 / 拼图删除契约。
|
||||
- [CREATION_CATEGORY_OPENING_TIMEOUT_GUARD_FIX_2026-04-22.md](./CREATION_CATEGORY_OPENING_TIMEOUT_GUARD_FIX_2026-04-22.md):记录创作中心点击类别后长时间停留在“正在开启”的根因与修复口径,收口前端创建会话启动超时、中文错误提示以及 Big Fish / 拼图代理上游超时兜底。
|
||||
- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):冻结 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本、发布包覆盖策略,以及部署阶段 SpacetimeDB schema 冲突自动导出、清库发布、导入回灌能力。
|
||||
- [JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md](./JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md):历史记录,保留旧 Jenkins `构建 / 部署 / 构建并部署` 三条流水线的职责、版本号传递、上游触发门禁、本地目录部署脚本、发布包覆盖策略,以及部署阶段 SpacetimeDB schema 冲突自动导出、清库发布、导入回灌经验;旧 Jenkinsfile 已删除,生产版入口以 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 为准。
|
||||
- [JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md](./JENKINS_DEPLOY_ENV_BOM_FIX_2026-04-25.md):记录 Jenkins 部署时 `.env.local` 首行 UTF-8 BOM 导致 `start.sh` 加载失败的根因,并冻结发布包构建、部署脚本和启动脚本的环境文件净化规则。
|
||||
- [RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](./RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md):冻结 Rust 本地一键联调脚本与 Ubuntu 发布包构建脚本的执行口径,覆盖 `npm run dev:rust`、`npm run build:rust:ubuntu`、Vite release、Linux `api-server`、SpacetimeDB wasm、启动停止脚本、默认 scp 上传、安全清库开关,以及发布包内 schema 冲突自动迁移脚本。
|
||||
- [RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](./RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md):历史记录,保留 Rust 本地一键联调脚本与旧 Ubuntu 发布包构建脚本的执行口径;生产发布链路以 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 的 systemd、Nginx、生产 Jenkinsfile 与归档产物拆分为准。
|
||||
- [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md):记录当前 Rust `api-server` 已挂载的 Axum 路由,并补充管理 API 索引,按 auth、assets、runtime、custom world、story、generated path 等挂载面归类,用于对照 Node 能力基线与切流 smoke 清单。
|
||||
- [BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md](./BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md):冻结后端重写收口阶段的横向治理规则,覆盖 TypeScript contract 到 Rust DTO 映射、SpacetimeDB schema 演进、大对象 / workflow cache 存储边界和文档维护门禁。
|
||||
- [PLATFORM_LLM_TEXT_GATEWAY_DESIGN_2026-04-21.md](./PLATFORM_LLM_TEXT_GATEWAY_DESIGN_2026-04-21.md):`platform-llm` 文本模型网关首版设计,冻结 OpenAI 兼容 `/chat/completions`、SSE 增量解析、错误模型与重试边界。
|
||||
@@ -208,11 +209,11 @@
|
||||
- [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md):AI 生成角色形象与角色动画的技术路线。
|
||||
- [ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md](./ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md):面向编辑器的阿里云 NPC 形象与动作实验方案,按 4 条生成链路对比。
|
||||
- [PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md](./PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md):PixelMotion 产品形态与能力拆解。
|
||||
- [SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md](./SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md):服务端部署、代理层与 CORS 方案。
|
||||
- [SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md](./SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md):旧服务端部署、代理层与 CORS 方案,保留同源代理和密钥不进前端的背景判断;当前生产拓扑和发布入口以 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md` 为准。
|
||||
- [WORLD_DRAFT_FOUNDATION_EDITOR_TARGET_FIX_2026-04-25.md](./WORLD_DRAFT_FOUNDATION_EDITOR_TARGET_FIX_2026-04-25.md):记录世界草稿“基本设定”独立编辑目标、分号标签化展示与编辑回写边界。
|
||||
|
||||
## 使用建议
|
||||
|
||||
- 做实现选型时,优先看这一组。
|
||||
- 做实现选型时,优先看这一组;做生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`,再按需追溯旧 Jenkins / CORS / 本地发布脚本文档。
|
||||
- 做后端实现前,先看 `CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`;涉及 SpacetimeDB 表结构、发布或迁移时,再看 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`,最后进入具体 Rust / SpacetimeDB 方案。
|
||||
- 做阶段排期时,把这一组和 `docs/planning/`、`docs/prd/` 一起看,更容易判断先后顺序。
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
日期:`2026-04-22`
|
||||
|
||||
状态:本地联调口径仍可参考;远端发布包和旧一体化启动脚本属于历史方案。生产发布、生产 Jenkins Job、systemd/Nginx 拓扑和 release/current 目录规则以 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md) 为准。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本方案补齐 `server-rs` 在 M7 切流前需要的两类工程脚本:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
日期:`2026-04-05`
|
||||
|
||||
状态:历史部署背景文档。本文保留同源代理、密钥不进入前端、CORS 不作为主解法等判断;当前生产拓扑、systemd/Nginx 配置和 Jenkins 发布入口以 [PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md](./PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md) 为准。
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文要解决的不只是“当前部署到服务器后浏览器报 CORS”,而是同时解决下面几类问题:
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '构建节点标签')
|
||||
string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定要构建的 Git commit hash;留空则使用 SCM 当前检出的提交')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-pipeline-local-test', description: '发布包默认 SpacetimeDB database')
|
||||
string(name: 'API_PORT', defaultValue: '8082', description: '发布包内 api-server 端口')
|
||||
string(name: 'WEB_PORT', defaultValue: '25001', description: '发布包内静态网站端口,默认 25001')
|
||||
string(name: 'SPACETIME_PORT', defaultValue: '3101', description: '发布包内本地 SpacetimeDB 端口')
|
||||
booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm')
|
||||
booleanParam(name: 'MIGRATE_ON_CONFLICT', defaultValue: true, description: '普通发布遇到 SpacetimeDB schema 冲突时自动导出、清库发布并导入回灌')
|
||||
string(name: 'MIGRATION_DIRECTORY', defaultValue: '', description: '自动迁移 JSON 输出目录,留空则使用部署目录内 database-migrations/<database>')
|
||||
password(name: 'MIGRATION_EXPORT_TOKEN', defaultValue: '', description: '可选,旧库已授权迁移操作员 token,仅部署阶段用于 schema 冲突导出')
|
||||
password(name: 'MIGRATION_IMPORT_TOKEN', defaultValue: '', description: '可选,新库已授权迁移操作员 token,仅部署阶段用于 schema 冲突导入')
|
||||
booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Deploy', description: '部署流水线作业名')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录')
|
||||
booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('构建发布包') {
|
||||
agent {
|
||||
label "${params.AGENT_LABEL}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 统一在脚本块里计算版本号,避免 declarative environment 对表达式求值不一致。
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
// 允许 Jenkins Job 直接指定固定源码目录,未指定时回退到当前工作区。
|
||||
env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd()
|
||||
def commitHash = params.COMMIT_HASH?.trim()
|
||||
if (commitHash && !(commitHash ==~ /^[0-9a-fA-F]{7,40}$/)) {
|
||||
error('COMMIT_HASH 只能填写 7 到 40 位十六进制 Git commit hash,当前值: ' + commitHash)
|
||||
}
|
||||
env.COMMIT_HASH = commitHash ?: ''
|
||||
def database = params.DATABASE?.trim()
|
||||
if (!database) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
if (!(database ==~ /^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
error('DATABASE 必须匹配 SpacetimeDB 数据库名规则 ^[a-z0-9]+(-[a-z0-9]+)*$,只能使用小写字母、数字,并用单个短横线分隔,当前值: ' + database)
|
||||
}
|
||||
env.EFFECTIVE_DATABASE = database
|
||||
echo "SpacetimeDB 发布数据库: ${env.EFFECTIVE_DATABASE}"
|
||||
def apiPort = params.API_PORT?.trim()
|
||||
if (!apiPort) {
|
||||
error('API_PORT 不能为空。')
|
||||
}
|
||||
if (!(apiPort ==~ /^[0-9]+$/)) {
|
||||
error("API_PORT 必须是数字端口,当前值: ${apiPort}")
|
||||
}
|
||||
if (apiPort.length() > 5) {
|
||||
error("API_PORT 必须在 1-65535 之间,当前值: ${apiPort}")
|
||||
}
|
||||
def parsedApiPort = apiPort.toInteger()
|
||||
if (parsedApiPort < 1 || parsedApiPort > 65535) {
|
||||
error("API_PORT 必须在 1-65535 之间,当前值: ${apiPort}")
|
||||
}
|
||||
env.EFFECTIVE_API_PORT = apiPort
|
||||
def webPort = params.WEB_PORT?.trim()
|
||||
if (!webPort) {
|
||||
error('WEB_PORT 不能为空。')
|
||||
}
|
||||
if (!(webPort ==~ /^[0-9]+$/)) {
|
||||
error("WEB_PORT 必须是数字端口,当前值: ${webPort}")
|
||||
}
|
||||
if (webPort.length() > 5) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
def parsedWebPort = webPort.toInteger()
|
||||
if (parsedWebPort < 1 || parsedWebPort > 65535) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
// 后续构建与下游部署都使用校验后的同一端口值,避免参数空格导致上下游不一致。
|
||||
env.EFFECTIVE_WEB_PORT = webPort
|
||||
def spacetimePort = params.SPACETIME_PORT?.trim()
|
||||
if (!spacetimePort) {
|
||||
error('SPACETIME_PORT 不能为空。')
|
||||
}
|
||||
if (!(spacetimePort ==~ /^[0-9]+$/)) {
|
||||
error("SPACETIME_PORT 必须是数字端口,当前值: ${spacetimePort}")
|
||||
}
|
||||
if (spacetimePort.length() > 5) {
|
||||
error("SPACETIME_PORT 必须在 1-65535 之间,当前值: ${spacetimePort}")
|
||||
}
|
||||
def parsedSpacetimePort = spacetimePort.toInteger()
|
||||
if (parsedSpacetimePort < 1 || parsedSpacetimePort > 65535) {
|
||||
error("SPACETIME_PORT 必须在 1-65535 之间,当前值: ${spacetimePort}")
|
||||
}
|
||||
env.EFFECTIVE_SPACETIME_PORT = spacetimePort
|
||||
// 记录当前构建节点名,部署阶段必须回到同一节点读取本地 build 目录。
|
||||
env.SOURCE_NODE_NAME = env.NODE_NAME
|
||||
}
|
||||
|
||||
dir("${env.WORKSPACE_ROOT}") {
|
||||
checkout scm
|
||||
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 每条流水线开头先强制回到 SCM 检出的干净提交,避免固定源码目录残留改动影响本次执行。
|
||||
git reset --hard HEAD
|
||||
requested_commit="${COMMIT_HASH:-}"
|
||||
if [[ -n "${requested_commit}" ]]; then
|
||||
# Jenkins 先按 SCM 配置完成 checkout;如指定 commit,再拉取远端引用并切到该提交构建。
|
||||
git fetch --tags --prune origin "+refs/heads/*:refs/remotes/origin/*" || git fetch --all --tags --prune
|
||||
if [[ "$(git rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; then
|
||||
git fetch --unshallow --tags || true
|
||||
fi
|
||||
git cat-file -e "${requested_commit}^{commit}"
|
||||
resolved_commit="$(git rev-parse "${requested_commit}^{commit}")"
|
||||
git checkout --detach "${resolved_commit}"
|
||||
echo "[build-and-deploy] 使用指定 commit 构建: ${resolved_commit}"
|
||||
else
|
||||
resolved_commit="$(git rev-parse HEAD)"
|
||||
echo "[build-and-deploy] 使用 SCM checkout commit 构建: ${resolved_commit}"
|
||||
fi
|
||||
# 构建前清理工作区内的 Git 变更和未跟踪文件,避免复用固定源码目录时受到上次构建残留影响。
|
||||
# 这里不使用 -x,避免删除 node_modules 等忽略目录后与 RUN_NPM_CI=false 的配置冲突。
|
||||
git reset --hard HEAD
|
||||
git clean -fd
|
||||
echo "${resolved_commit}" > "build-and-deploy-commit.txt"
|
||||
rm -rf "build/${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
|
||||
script {
|
||||
env.EFFECTIVE_COMMIT_HASH = readFile('build-and-deploy-commit.txt').trim()
|
||||
echo "构建 commit: ${env.EFFECTIVE_COMMIT_HASH}"
|
||||
}
|
||||
|
||||
script {
|
||||
// 是否重装依赖交给流水线参数决定,避免每次构建都重复执行 npm ci。
|
||||
if (params.RUN_NPM_CI) {
|
||||
sh 'bash -lc "npm ci"'
|
||||
}
|
||||
}
|
||||
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 构建并部署流水线显式透传本地测试参数,避免发布包回退到默认库名或端口。
|
||||
npm run deploy:rust:remote -- --skip-upload \
|
||||
--name "${env.EFFECTIVE_BUILD_VERSION}" \
|
||||
--database "${env.EFFECTIVE_DATABASE}" \
|
||||
--api-port "${env.EFFECTIVE_API_PORT}" \
|
||||
--web-port "${env.EFFECTIVE_WEB_PORT}" \
|
||||
--spacetime-port "${env.EFFECTIVE_SPACETIME_PORT}"
|
||||
test -d "build/${env.EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
"""
|
||||
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/**", fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('触发部署') {
|
||||
steps {
|
||||
// 本阶段没有声明 agent,确保触发下游前已经释放构建节点,避免单执行器死锁。
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_NODE_NAME', value: env.SOURCE_NODE_NAME),
|
||||
string(name: 'SOURCE_WORKSPACE_ROOT', value: env.WORKSPACE_ROOT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_DIRECTORY', value: params.DEPLOY_DIRECTORY),
|
||||
string(name: 'WEB_PORT', value: env.EFFECTIVE_WEB_PORT),
|
||||
booleanParam(name: 'CLEAR_DATABASE', value: params.CLEAR_DATABASE),
|
||||
booleanParam(name: 'MIGRATE_ON_CONFLICT', value: params.MIGRATE_ON_CONFLICT),
|
||||
string(name: 'MIGRATION_DIRECTORY', value: params.MIGRATION_DIRECTORY),
|
||||
password(name: 'MIGRATION_EXPORT_TOKEN', value: params.MIGRATION_EXPORT_TOKEN),
|
||||
password(name: 'MIGRATION_IMPORT_TOKEN', value: params.MIGRATION_IMPORT_TOKEN),
|
||||
booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', value: params.RUN_DEPLOY_HOOKS_WITH_SUDO),
|
||||
string(name: 'EXPECTED_UPSTREAM_JOB', value: env.JOB_NAME),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "构建并部署完成,版本号: ${env.EFFECTIVE_BUILD_VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '执行节点标签')
|
||||
string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区')
|
||||
string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB 数据库名,留空则读取环境变量')
|
||||
string(name: 'SERVER', defaultValue: 'maincloud', description: 'SpacetimeDB server 别名,例如 maincloud/local/dev')
|
||||
string(name: 'SERVER_URL', defaultValue: '', description: 'SpacetimeDB server URL,填写后优先于 SERVER')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录,ROOT_DIR 为空时使用其 .spacetimedb')
|
||||
string(name: 'ROOT_DIR', defaultValue: '', description: 'spacetime CLI root-dir,可选,优先于 DEPLOY_DIRECTORY')
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'BOOTSTRAP_SECRET', defaultValue: '', description: '可选,授权临时导出 identity 的迁移引导密钥')
|
||||
string(name: 'OUTPUT_DIRECTORY', defaultValue: 'database-exports', description: '导出文件目录,相对源码根目录或绝对路径')
|
||||
string(name: 'EXPORT_NAME', defaultValue: '', description: '导出文件名,留空则自动使用构建号')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('导出数据库') {
|
||||
agent {
|
||||
label "${params.AGENT_LABEL}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 允许 Jenkins Job 指定固定源码目录;未指定时使用当前工作区,方便临时手工执行。
|
||||
env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd()
|
||||
def deployDirectory = params.DEPLOY_DIRECTORY?.trim()
|
||||
if (!deployDirectory) {
|
||||
error('DEPLOY_DIRECTORY 不能为空。')
|
||||
}
|
||||
env.EFFECTIVE_ROOT_DIR = params.ROOT_DIR?.trim() ? params.ROOT_DIR.trim() : "${deployDirectory}/.spacetimedb"
|
||||
def exportName = params.EXPORT_NAME?.trim()
|
||||
if (!exportName) {
|
||||
exportName = "spacetime-migration-${env.BUILD_NUMBER}.json"
|
||||
}
|
||||
if (!(exportName ==~ /^[A-Za-z0-9._-]+$/)) {
|
||||
error("EXPORT_NAME 只能包含字母、数字、点、下划线和短横线,当前值: ${exportName}")
|
||||
}
|
||||
env.EFFECTIVE_EXPORT_NAME = exportName
|
||||
}
|
||||
|
||||
dir("${env.WORKSPACE_ROOT}") {
|
||||
checkout scm
|
||||
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 导出流水线复用固定源码目录时,先清掉本地改动,确保执行的是 Jenkins SCM 检出的脚本。
|
||||
git reset --hard HEAD
|
||||
export_dir="${params.OUTPUT_DIRECTORY}"
|
||||
if [[ -z "\${export_dir}" ]]; then
|
||||
export_dir="database-exports"
|
||||
fi
|
||||
mkdir -p "\${export_dir}"
|
||||
output_path="\${export_dir}/${env.EFFECTIVE_EXPORT_NAME}"
|
||||
args=(scripts/spacetime-export-migration-json.mjs --out "\${output_path}")
|
||||
if [[ -n "${params.DATABASE}" ]]; then
|
||||
args+=(--database "${params.DATABASE}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER}" ]]; then
|
||||
args+=(--server "${params.SERVER}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER_URL}" ]]; then
|
||||
args+=(--server-url "${params.SERVER_URL}")
|
||||
fi
|
||||
if [[ -n "${env.EFFECTIVE_ROOT_DIR}" ]]; then
|
||||
args+=(--root-dir "${env.EFFECTIVE_ROOT_DIR}")
|
||||
fi
|
||||
if [[ -n "${params.INCLUDE_TABLES}" ]]; then
|
||||
args+=(--include "${params.INCLUDE_TABLES}")
|
||||
fi
|
||||
if [[ -n "${params.BOOTSTRAP_SECRET}" ]]; then
|
||||
args+=(--bootstrap-secret "${params.BOOTSTRAP_SECRET}")
|
||||
fi
|
||||
# 复用后端迁移 procedure 导出 JSON,避免 Jenkins 直接拼接表结构和 SQL。
|
||||
node "\${args[@]}"
|
||||
test -s "\${output_path}"
|
||||
'
|
||||
"""
|
||||
|
||||
archiveArtifacts artifacts: "${params.OUTPUT_DIRECTORY ?: 'database-exports'}/${env.EFFECTIVE_EXPORT_NAME}", fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导出完成: ${env.EFFECTIVE_EXPORT_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'AGENT_LABEL', defaultValue: 'built-in', description: '执行节点标签')
|
||||
string(name: 'GENARRATIVE_WORKSPACE_ROOT', defaultValue: '', description: '源码根目录,留空则使用当前 Jenkins 工作区')
|
||||
string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB 数据库名,留空则读取环境变量')
|
||||
string(name: 'SERVER', defaultValue: 'maincloud', description: 'SpacetimeDB server 别名,例如 maincloud/local/dev')
|
||||
string(name: 'SERVER_URL', defaultValue: '', description: 'SpacetimeDB server URL,填写后优先于 SERVER')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录,ROOT_DIR 为空时使用其 .spacetimedb')
|
||||
string(name: 'ROOT_DIR', defaultValue: '', description: 'spacetime CLI root-dir,可选,优先于 DEPLOY_DIRECTORY')
|
||||
string(name: 'INPUT_FILE', defaultValue: '', description: '必填,迁移 JSON 文件路径,相对源码根目录或绝对路径')
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'CHUNK_SIZE', defaultValue: '524288', description: '迁移 JSON 分片大小,默认 512KiB,用于规避 SpacetimeDB HTTP 413')
|
||||
booleanParam(name: 'DRY_RUN', defaultValue: true, description: '仅校验导入,不写入数据')
|
||||
booleanParam(name: 'INCREMENTAL', defaultValue: true, description: '增量导入,跳过已存在或冲突的行')
|
||||
booleanParam(name: 'REPLACE_EXISTING', defaultValue: false, description: '覆盖本次文件内涉及的表,不可与 INCREMENTAL 同时启用')
|
||||
string(name: 'BOOTSTRAP_SECRET', defaultValue: '', description: '可选,授权临时导入 identity 的迁移引导密钥')
|
||||
string(name: 'TOKEN', defaultValue: '', description: '可选,SpacetimeDB 客户端连接 token;留空则自动创建临时 identity')
|
||||
string(name: 'NOTE', defaultValue: 'jenkins database import', description: '迁移授权备注')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('校验参数') {
|
||||
agent {
|
||||
label 'built-in'
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
if (!params.INPUT_FILE?.trim()) {
|
||||
error('INPUT_FILE 不能为空。')
|
||||
}
|
||||
if (params.INCREMENTAL && params.REPLACE_EXISTING) {
|
||||
error('INCREMENTAL 不能和 REPLACE_EXISTING 同时启用。')
|
||||
}
|
||||
def deployDirectory = params.DEPLOY_DIRECTORY?.trim()
|
||||
if (!deployDirectory) {
|
||||
error('DEPLOY_DIRECTORY 不能为空。')
|
||||
}
|
||||
env.EFFECTIVE_ROOT_DIR = params.ROOT_DIR?.trim() ? params.ROOT_DIR.trim() : "${deployDirectory}/.spacetimedb"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('导入数据库') {
|
||||
agent {
|
||||
label "${params.AGENT_LABEL}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 固定源码目录可复用 Jenkins Agent 上的脚本和依赖,未指定时回退到当前工作区。
|
||||
env.WORKSPACE_ROOT = params.GENARRATIVE_WORKSPACE_ROOT?.trim() ? params.GENARRATIVE_WORKSPACE_ROOT.trim() : pwd()
|
||||
}
|
||||
|
||||
dir("${env.WORKSPACE_ROOT}") {
|
||||
checkout scm
|
||||
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 导入流水线复用固定源码目录时,先清掉本地改动,确保迁移脚本来自 Jenkins SCM 检出的版本。
|
||||
git reset --hard HEAD
|
||||
args=(scripts/spacetime-import-migration-json.mjs --in "${params.INPUT_FILE}")
|
||||
if [[ -n "${params.DATABASE}" ]]; then
|
||||
args+=(--database "${params.DATABASE}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER}" ]]; then
|
||||
args+=(--server "${params.SERVER}")
|
||||
fi
|
||||
if [[ -n "${params.SERVER_URL}" ]]; then
|
||||
args+=(--server-url "${params.SERVER_URL}")
|
||||
fi
|
||||
if [[ -n "${env.EFFECTIVE_ROOT_DIR}" ]]; then
|
||||
args+=(--root-dir "${env.EFFECTIVE_ROOT_DIR}")
|
||||
fi
|
||||
if [[ -n "${params.INCLUDE_TABLES}" ]]; then
|
||||
args+=(--include "${params.INCLUDE_TABLES}")
|
||||
fi
|
||||
if [[ -n "${params.CHUNK_SIZE}" ]]; then
|
||||
args+=(--chunk-size "${params.CHUNK_SIZE}")
|
||||
fi
|
||||
if [[ "${params.DRY_RUN}" == "true" ]]; then
|
||||
args+=(--dry-run)
|
||||
fi
|
||||
if [[ "${params.INCREMENTAL}" == "true" ]]; then
|
||||
args+=(--incremental)
|
||||
fi
|
||||
if [[ "${params.REPLACE_EXISTING}" == "true" ]]; then
|
||||
args+=(--replace-existing)
|
||||
fi
|
||||
if [[ -n "${params.BOOTSTRAP_SECRET}" ]]; then
|
||||
args+=(--bootstrap-secret "${params.BOOTSTRAP_SECRET}")
|
||||
fi
|
||||
if [[ -n "${params.TOKEN}" ]]; then
|
||||
args+=(--token "${params.TOKEN}")
|
||||
fi
|
||||
if [[ -n "${params.NOTE}" ]]; then
|
||||
args+=(--note "${params.NOTE}")
|
||||
fi
|
||||
# 导入默认 dry-run,并通过迁移 procedure 写入,保持权限校验和表级统计一致。
|
||||
node "\${args[@]}"
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导入流水线完成,dry-run: ${params.DRY_RUN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
GENARRATIVE_TOOLS_PATH = "/var/lib/jenkins/.nvm/versions/node/v22.22.2/bin:/var/lib/jenkins/.cargo/bin:/var/lib/jenkins/.local/bin:/var/lib/jenkins/bin"
|
||||
PATH = "${GENARRATIVE_TOOLS_PATH}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_NODE_NAME', defaultValue: '', description: '上游构建节点名')
|
||||
string(name: 'SOURCE_WORKSPACE_ROOT', defaultValue: '', description: '上游源码根目录')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待部署版本号')
|
||||
string(name: 'DEPLOY_DIRECTORY', defaultValue: '/var/lib/jenkins/deploy/Genarrative', description: '固定部署目录')
|
||||
string(name: 'WEB_PORT', defaultValue: '25001', description: '静态网站监听端口,默认 25001,上游构建并部署流水线会透传同名参数')
|
||||
booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '部署时是否清空 SpacetimeDB 数据后再发布 wasm')
|
||||
booleanParam(name: 'MIGRATE_ON_CONFLICT', defaultValue: true, description: '普通发布遇到 SpacetimeDB schema 冲突时自动导出、清库发布并导入回灌')
|
||||
string(name: 'MIGRATION_DIRECTORY', defaultValue: '', description: '自动迁移 JSON 输出目录,留空则使用部署目录内 database-migrations/<database>')
|
||||
password(name: 'MIGRATION_EXPORT_TOKEN', defaultValue: '', description: '可选,旧库已授权迁移操作员 token,仅用于 schema 冲突导出')
|
||||
password(name: 'MIGRATION_IMPORT_TOKEN', defaultValue: '', description: '可选,新库已授权迁移操作员 token,仅用于 schema 冲突导入')
|
||||
booleanParam(name: 'RUN_DEPLOY_HOOKS_WITH_SUDO', defaultValue: true, description: 'start.sh / stop.sh 是否通过 sudo -n 执行')
|
||||
string(name: 'EXPECTED_UPSTREAM_JOB', defaultValue: '', description: '允许触发本作业的上游作业名')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('校验触发来源') {
|
||||
agent {
|
||||
label 'built-in'
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 部署流水线允许手动启动;如存在上游触发原因,则继续执行上游作业名门禁。
|
||||
// Pipeline 的 build 步骤通常会把下游触发原因记录成 BuildUpstreamCause,
|
||||
// 直接只查经典 UpstreamCause 会把真实的上游触发误判成“人工执行”。
|
||||
def pipelineUpstreamCauses = currentBuild.getBuildCauses('org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause')
|
||||
def classicUpstreamCauses = currentBuild.getBuildCauses('hudson.model.Cause$UpstreamCause')
|
||||
def upstreamCause = null
|
||||
|
||||
if (pipelineUpstreamCauses && !pipelineUpstreamCauses.isEmpty()) {
|
||||
upstreamCause = pipelineUpstreamCauses[0]
|
||||
} else if (classicUpstreamCauses && !classicUpstreamCauses.isEmpty()) {
|
||||
upstreamCause = classicUpstreamCauses[0]
|
||||
}
|
||||
|
||||
def actualUpstreamJob = upstreamCause?.upstreamProject ?: ''
|
||||
def expectedUpstreamJob = params.EXPECTED_UPSTREAM_JOB?.trim()
|
||||
def allowedUpstreamJob = env.GENARRATIVE_ALLOWED_UPSTREAM_JOB?.trim()
|
||||
|
||||
if (!params.BUILD_VERSION?.trim()) {
|
||||
error('BUILD_VERSION 不能为空。')
|
||||
}
|
||||
|
||||
if (!params.SOURCE_WORKSPACE_ROOT?.trim()) {
|
||||
error('SOURCE_WORKSPACE_ROOT 不能为空。')
|
||||
}
|
||||
|
||||
if (!params.SOURCE_NODE_NAME?.trim()) {
|
||||
error('SOURCE_NODE_NAME 不能为空。')
|
||||
}
|
||||
|
||||
def webPort = params.WEB_PORT?.trim()
|
||||
if (!webPort) {
|
||||
error('WEB_PORT 不能为空。')
|
||||
}
|
||||
|
||||
if (!(webPort ==~ /^[0-9]+$/)) {
|
||||
error("WEB_PORT 必须是数字端口,当前值: ${webPort}")
|
||||
}
|
||||
|
||||
if (webPort.length() > 5) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
|
||||
def parsedWebPort = webPort.toInteger()
|
||||
if (parsedWebPort < 1 || parsedWebPort > 65535) {
|
||||
error("WEB_PORT 必须在 1-65535 之间,当前值: ${webPort}")
|
||||
}
|
||||
// 部署脚本只接收校验后的端口值,避免手工参数前后空格传到 Bash。
|
||||
env.EFFECTIVE_WEB_PORT = webPort
|
||||
|
||||
if (upstreamCause && !actualUpstreamJob?.trim()) {
|
||||
error('无法从上游触发原因中解析作业名,请检查 Jenkins Pipeline Build Step 插件版本与触发链。')
|
||||
}
|
||||
|
||||
if (actualUpstreamJob && expectedUpstreamJob && actualUpstreamJob != expectedUpstreamJob) {
|
||||
error("上游作业校验失败,期望 ${expectedUpstreamJob},实际 ${actualUpstreamJob}")
|
||||
}
|
||||
|
||||
if (actualUpstreamJob && allowedUpstreamJob && actualUpstreamJob != allowedUpstreamJob) {
|
||||
error("环境门禁校验失败,仅允许 ${allowedUpstreamJob} 触发,实际 ${actualUpstreamJob}")
|
||||
}
|
||||
|
||||
env.UPSTREAM_JOB_NAME = actualUpstreamJob ?: 'MANUAL'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('部署指定版本') {
|
||||
agent {
|
||||
label "${params.SOURCE_NODE_NAME}"
|
||||
}
|
||||
|
||||
steps {
|
||||
script {
|
||||
// 部署脚本使用当前 Deploy 作业 checkout 出来的版本,避免重放旧 build 目录时继续执行旧脚本。
|
||||
env.DEPLOY_SCRIPT_PATH = "${pwd()}/scripts/jenkins-deploy-release.sh"
|
||||
}
|
||||
|
||||
dir("${params.SOURCE_WORKSPACE_ROOT}") {
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
# 部署流水线也先清回上游工作区的当前提交,避免复用目录中的本地改动影响部署脚本或产物选择。
|
||||
git reset --hard HEAD
|
||||
test -d "build/${params.BUILD_VERSION}"
|
||||
deploy_script="${env.DEPLOY_SCRIPT_PATH}"
|
||||
chmod +x "\${deploy_script}"
|
||||
deploy_args=(
|
||||
--source-dir "build/${params.BUILD_VERSION}"
|
||||
--deploy-dir "${params.DEPLOY_DIRECTORY}"
|
||||
--web-port "${env.EFFECTIVE_WEB_PORT}"
|
||||
)
|
||||
if [[ "${params.CLEAR_DATABASE}" == "true" ]]; then
|
||||
deploy_args+=(--clear-database)
|
||||
fi
|
||||
if [[ "${params.MIGRATE_ON_CONFLICT}" == "true" ]]; then
|
||||
deploy_args+=(--migrate-on-conflict)
|
||||
else
|
||||
deploy_args+=(--no-migrate-on-conflict)
|
||||
fi
|
||||
if [[ -n "${params.MIGRATION_DIRECTORY}" ]]; then
|
||||
deploy_args+=(--migration-dir "${params.MIGRATION_DIRECTORY}")
|
||||
fi
|
||||
if [[ -n "${params.MIGRATION_EXPORT_TOKEN}" ]]; then
|
||||
deploy_args+=(--migration-export-token "${params.MIGRATION_EXPORT_TOKEN}")
|
||||
fi
|
||||
if [[ -n "${params.MIGRATION_IMPORT_TOKEN}" ]]; then
|
||||
deploy_args+=(--migration-import-token "${params.MIGRATION_IMPORT_TOKEN}")
|
||||
fi
|
||||
if [[ "${params.RUN_DEPLOY_HOOKS_WITH_SUDO}" == "true" ]]; then
|
||||
deploy_args+=(--hook-with-sudo)
|
||||
fi
|
||||
# 只部署上游已构建好的版本目录,避免部署阶段再次构建产生漂移。
|
||||
"\${deploy_script}" "\${deploy_args[@]}"
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "部署完成,版本号: ${params.BUILD_VERSION},上游作业: ${env.UPSTREAM_JOB_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
CARGO_HOME = '/var/cache/genarrative-build/cargo-home'
|
||||
CARGO_TARGET_DIR = '/var/cache/genarrative-build/cargo-target/prod-release'
|
||||
CARGO_INCREMENTAL = '0'
|
||||
RUSTC_WRAPPER = 'sccache'
|
||||
SCCACHE_DIR = '/var/cache/genarrative-build/sccache'
|
||||
SCCACHE_CACHE_SIZE = '30G'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 API 发布')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Api-Deploy', description: 'API 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: 'PUBLISH_AFTER_BUILD=true 且目标为 release 时必须确认已有独立 release 部署 agent')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Api') {
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
mkdir -p "${CARGO_HOME}" "${CARGO_TARGET_DIR}" "${SCCACHE_DIR}"
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
npm run build:production-release -- --component api-server --name "${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
steps {
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/api-server,build/${env.EFFECTIVE_BUILD_VERSION}/api-server.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json", fingerprint: true
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
when {
|
||||
expression { return params.PUBLISH_AFTER_BUILD }
|
||||
}
|
||||
steps {
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "API 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号')
|
||||
string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Api-Build', description: 'API 构建流水线作业名')
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号')
|
||||
string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: '生产 release 根目录')
|
||||
string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接')
|
||||
string(name: 'SERVICE_NAME', defaultValue: 'genarrative-api.service', description: 'systemd 服务名')
|
||||
string(name: 'HEALTH_URL', defaultValue: 'http://127.0.0.1:8082/healthz', description: '本机健康检查地址')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||||
}
|
||||
if (!params.BUILD_VERSION?.trim()) {
|
||||
error('BUILD_VERSION 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_JOB_NAME?.trim()) {
|
||||
error('BUILD_JOB_NAME 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) {
|
||||
error('BUILD_NUMBER_TO_DEPLOY 不能为空。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout Deploy Scripts') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Fetch Artifact') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
copyArtifacts(
|
||||
projectName: params.BUILD_JOB_NAME,
|
||||
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
|
||||
filter: "build/${params.BUILD_VERSION}/api-server,build/${params.BUILD_VERSION}/api-server.sha256,build/${params.BUILD_VERSION}/release-manifest.json",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Api') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/deploy/production-api-deploy.sh scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
scripts/deploy/production-api-deploy.sh \
|
||||
--source-dir "build/${BUILD_VERSION}" \
|
||||
--version "${BUILD_VERSION}" \
|
||||
--release-root "${RELEASE_ROOT}" \
|
||||
--current-link "${CURRENT_LINK}" \
|
||||
--service "${SERVICE_NAME}" \
|
||||
--health-url "${HEALTH_URL}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "API 发布完成: version=${params.BUILD_VERSION}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑导出目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '导出脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '导出脚本来源 commit')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: 'SpacetimeDB database')
|
||||
string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias')
|
||||
string(name: 'SPACETIME_SERVER_URL', defaultValue: '', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER')
|
||||
string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dir;release 自托管默认 /stdb')
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'WORKSPACE_EXPORT_DIRECTORY', defaultValue: 'database-exports', description: 'Jenkins workspace 内的导出目录,用于归档')
|
||||
string(name: 'SERVER_BACKUP_DIRECTORY', defaultValue: '/var/lib/genarrative/database-exports', description: '可选,额外保存在目标机器上的备份目录;留空则不保存服务器副本')
|
||||
string(name: 'EXPORT_NAME', defaultValue: '', description: '导出文件名,留空则使用 spacetime-migration-<BUILD_NUMBER>.json')
|
||||
string(name: 'TOKEN_CREDENTIAL_ID', defaultValue: '', description: '可选,SpacetimeDB 客户端连接 token 的 Jenkins Secret Text 凭据 ID')
|
||||
string(name: 'BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: '', description: '可选,迁移 bootstrap secret 的 Jenkins Secret Text 凭据 ID')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 数据库导出需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。')
|
||||
}
|
||||
if (!params.DATABASE?.trim()) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
if (!(params.DATABASE.trim() ==~ /^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
error("DATABASE 必须匹配 ^[a-z0-9]+(-[a-z0-9]+)*$: ${params.DATABASE}")
|
||||
}
|
||||
|
||||
def spacetimeRootDir = params.SPACETIME_ROOT_DIR?.trim() ? params.SPACETIME_ROOT_DIR.trim() : '/stdb'
|
||||
if (!spacetimeRootDir.startsWith('/') || spacetimeRootDir.contains('..')) {
|
||||
error("SPACETIME_ROOT_DIR 必须是 Linux 绝对路径且不能包含 ..: ${spacetimeRootDir}")
|
||||
}
|
||||
|
||||
def serverBackupDirectory = params.SERVER_BACKUP_DIRECTORY?.trim()
|
||||
if (serverBackupDirectory && (!serverBackupDirectory.startsWith('/') || serverBackupDirectory.contains('..'))) {
|
||||
error("SERVER_BACKUP_DIRECTORY 必须是 Linux 绝对路径且不能包含 ..: ${serverBackupDirectory}")
|
||||
}
|
||||
|
||||
def exportDirectory = params.WORKSPACE_EXPORT_DIRECTORY?.trim() ? params.WORKSPACE_EXPORT_DIRECTORY.trim() : 'database-exports'
|
||||
if (exportDirectory.startsWith('/') || exportDirectory.contains('..') || !(exportDirectory ==~ /^[A-Za-z0-9._\/-]+$/)) {
|
||||
error("WORKSPACE_EXPORT_DIRECTORY 必须是安全的相对路径: ${exportDirectory}")
|
||||
}
|
||||
|
||||
def exportName = params.EXPORT_NAME?.trim()
|
||||
if (!exportName) {
|
||||
exportName = "spacetime-migration-${env.BUILD_NUMBER}.json"
|
||||
}
|
||||
if (!(exportName ==~ /^[A-Za-z0-9._-]+$/)) {
|
||||
error("EXPORT_NAME 只能包含字母、数字、点、下划线和短横线: ${exportName}")
|
||||
}
|
||||
|
||||
env.WORKSPACE_EXPORT_DIRECTORY = exportDirectory
|
||||
env.EFFECTIVE_EXPORT_NAME = exportName
|
||||
env.EFFECTIVE_SPACETIME_ROOT_DIR = spacetimeRootDir
|
||||
env.EFFECTIVE_SERVER_BACKUP_DIRECTORY = serverBackupDirectory ?: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Export Database') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
def credentialBindings = []
|
||||
if (params.TOKEN_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.TOKEN_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_TOKEN'))
|
||||
}
|
||||
if (params.BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.BOOTSTRAP_SECRET_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET'))
|
||||
}
|
||||
|
||||
def exportStep = {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
chmod +x scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
|
||||
export_dir="${WORKSPACE_EXPORT_DIRECTORY}"
|
||||
output_path="${export_dir}/${EFFECTIVE_EXPORT_NAME}"
|
||||
mkdir -p "${export_dir}"
|
||||
|
||||
maintenance_entered=0
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
if [[ "${exit_code}" -ne 0 && "${maintenance_entered}" -eq 1 ]]; then
|
||||
echo "[database-export] 导出失败,保持维护模式。" >&2
|
||||
elif [[ "${exit_code}" -ne 0 ]]; then
|
||||
echo "[database-export] 导出准备失败,尚未进入维护模式。" >&2
|
||||
fi
|
||||
exit "${exit_code}"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
|
||||
scripts/deploy/maintenance-on.sh "database export ${DATABASE}"
|
||||
maintenance_entered=1
|
||||
|
||||
args=(scripts/spacetime-export-migration-json.mjs --out "${output_path}" --database "${DATABASE}")
|
||||
if [[ -n "${SPACETIME_SERVER_URL}" ]]; then
|
||||
args+=(--server-url "${SPACETIME_SERVER_URL}")
|
||||
elif [[ -n "${SPACETIME_SERVER}" ]]; then
|
||||
args+=(--server "${SPACETIME_SERVER}")
|
||||
fi
|
||||
if [[ -n "${EFFECTIVE_SPACETIME_ROOT_DIR}" ]]; then
|
||||
args+=(--root-dir "${EFFECTIVE_SPACETIME_ROOT_DIR}")
|
||||
fi
|
||||
if [[ -n "${INCLUDE_TABLES}" ]]; then
|
||||
args+=(--include "${INCLUDE_TABLES}")
|
||||
fi
|
||||
args+=(--note "jenkins database export ${BUILD_TAG}")
|
||||
|
||||
node "${args[@]}"
|
||||
test -s "${output_path}"
|
||||
sha256sum "${output_path}" >"${output_path}.sha256"
|
||||
|
||||
if [[ -n "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}" ]]; then
|
||||
mkdir -p "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}"
|
||||
install -m 0640 "${output_path}" "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_EXPORT_NAME}"
|
||||
install -m 0640 "${output_path}.sha256" "${EFFECTIVE_SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_EXPORT_NAME}.sha256"
|
||||
fi
|
||||
|
||||
echo "[database-export] 完成: ${output_path}, source_commit=$(cat .jenkins-source-commit)"
|
||||
'
|
||||
'''
|
||||
}
|
||||
|
||||
boolean archiveSucceeded = false
|
||||
try {
|
||||
if (credentialBindings) {
|
||||
withCredentials(credentialBindings) {
|
||||
exportStep()
|
||||
}
|
||||
} else {
|
||||
exportStep()
|
||||
}
|
||||
archiveArtifacts artifacts: "${env.WORKSPACE_EXPORT_DIRECTORY}/${env.EFFECTIVE_EXPORT_NAME},${env.WORKSPACE_EXPORT_DIRECTORY}/${env.EFFECTIVE_EXPORT_NAME}.sha256", fingerprint: true
|
||||
archiveSucceeded = true
|
||||
} finally {
|
||||
if (archiveSucceeded) {
|
||||
// 先确认导出和归档都已完成,再退出维护模式,避免归档异常把站点留在维护页。
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
scripts/deploy/maintenance-off.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导出完成: target=${params.DEPLOY_TARGET}, database=${params.DATABASE}, file=${env.EFFECTIVE_EXPORT_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑导入目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '导入脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '导入脚本来源 commit')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: 'SpacetimeDB database')
|
||||
string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias')
|
||||
string(name: 'SPACETIME_SERVER_URL', defaultValue: '', description: '显式 SpacetimeDB server URL,填写后优先于 SPACETIME_SERVER')
|
||||
string(name: 'SPACETIME_ROOT_DIR', defaultValue: '/stdb', description: 'spacetime CLI root-dir;release 自托管默认 /stdb')
|
||||
choice(name: 'INPUT_SOURCE', choices: ['pipeline_archive', 'manual_upload'], description: '导入数据源;pipeline_archive 从导出流水线归档获取,manual_upload 使用本次构建手动上传文件')
|
||||
string(name: 'INPUT_FILE', defaultValue: '', description: 'pipeline_archive 模式可选;留空时使用导出流水线默认归档路径 database-exports/spacetime-migration-<导出构建号>.json')
|
||||
string(name: 'EXPORT_JOB_NAME', defaultValue: 'Genarrative-Database-Export', description: 'pipeline_archive 模式使用的数据库导出流水线作业名')
|
||||
string(name: 'EXPORT_BUILD_NUMBER_TO_IMPORT', defaultValue: '', description: 'pipeline_archive 模式必填,要复制 INPUT_FILE 的导出构建号')
|
||||
stashedFile 'MANUAL_INPUT_FILE'
|
||||
string(name: 'INCLUDE_TABLES', defaultValue: '', description: '可选,逗号分隔的表名白名单')
|
||||
string(name: 'CHUNK_SIZE', defaultValue: '524288', description: '迁移 JSON 分片大小,默认 512KiB,用于规避 HTTP 413')
|
||||
booleanParam(name: 'DRY_RUN', defaultValue: true, description: '只校验导入,不写入数据')
|
||||
booleanParam(name: 'INCREMENTAL', defaultValue: true, description: '增量导入,跳过已存在或冲突的行')
|
||||
booleanParam(name: 'REPLACE_EXISTING', defaultValue: false, description: '覆盖本次文件内涉及的表,不可与 INCREMENTAL 同时启用')
|
||||
booleanParam(name: 'CONFIRM_IMPORT', defaultValue: false, description: 'DRY_RUN=false 时必须勾选')
|
||||
string(name: 'CONFIRM_DATABASE', defaultValue: '', description: 'DRY_RUN=false 时必须填写与 DATABASE 完全一致')
|
||||
string(name: 'CONFIRM_INPUT_FILE', defaultValue: '', description: 'DRY_RUN=false 时必须确认输入文件;pipeline_archive 填实际归档输入路径,manual_upload 填上传原始文件名')
|
||||
booleanParam(name: 'CONFIRM_REPLACE_EXISTING', defaultValue: false, description: 'REPLACE_EXISTING=true 且 DRY_RUN=false 时必须勾选')
|
||||
string(name: 'PRE_IMPORT_BACKUP_DIRECTORY', defaultValue: 'database-pre-import-backups', description: 'Jenkins workspace 内的导入前备份目录,用于归档')
|
||||
string(name: 'SERVER_BACKUP_DIRECTORY', defaultValue: '/var/lib/genarrative/database-backups', description: '可选,额外保存在目标机器上的导入前备份目录;留空则不保存服务器副本')
|
||||
booleanParam(name: 'RUN_SMOKE_TEST', defaultValue: true, description: '导入成功后是否执行服务健康检查')
|
||||
string(name: 'SMOKE_HEALTH_URL', defaultValue: 'http://127.0.0.1:8082/healthz', description: '目标机器本机健康检查地址')
|
||||
string(name: 'TOKEN_CREDENTIAL_ID', defaultValue: '', description: '可选,SpacetimeDB 客户端连接 token 的 Jenkins Secret Text 凭据 ID')
|
||||
string(name: 'BOOTSTRAP_SECRET_CREDENTIAL_ID', defaultValue: '', description: '可选,迁移 bootstrap secret 的 Jenkins Secret Text 凭据 ID')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 数据库导入需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。')
|
||||
}
|
||||
if (!params.DATABASE?.trim()) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
if (!(params.DATABASE.trim() ==~ /^[a-z0-9]+(-[a-z0-9]+)*$/)) {
|
||||
error("DATABASE 必须匹配 ^[a-z0-9]+(-[a-z0-9]+)*$: ${params.DATABASE}")
|
||||
}
|
||||
|
||||
def inputSource = params.INPUT_SOURCE?.trim()
|
||||
if (!(inputSource in ['pipeline_archive', 'manual_upload'])) {
|
||||
error("INPUT_SOURCE 只能是 pipeline_archive 或 manual_upload,当前值: ${params.INPUT_SOURCE}")
|
||||
}
|
||||
def manualInputFilename = env.MANUAL_INPUT_FILE_FILENAME?.trim()
|
||||
if (inputSource == 'pipeline_archive') {
|
||||
if (!params.EXPORT_JOB_NAME?.trim()) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时 EXPORT_JOB_NAME 不能为空。')
|
||||
}
|
||||
if (!params.EXPORT_BUILD_NUMBER_TO_IMPORT?.trim()) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时 EXPORT_BUILD_NUMBER_TO_IMPORT 不能为空。')
|
||||
}
|
||||
if (!(params.EXPORT_BUILD_NUMBER_TO_IMPORT.trim() ==~ /^[1-9][0-9]*$/)) {
|
||||
error("INPUT_SOURCE=pipeline_archive 时 EXPORT_BUILD_NUMBER_TO_IMPORT 必须是导出流水线构建号: ${params.EXPORT_BUILD_NUMBER_TO_IMPORT}")
|
||||
}
|
||||
def pipelineInputFile = params.INPUT_FILE?.trim()
|
||||
if (!pipelineInputFile) {
|
||||
pipelineInputFile = "database-exports/spacetime-migration-${params.EXPORT_BUILD_NUMBER_TO_IMPORT.trim()}.json"
|
||||
}
|
||||
if (pipelineInputFile.startsWith('/')) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时 INPUT_FILE 必须是 Jenkins 归档内的 workspace 相对路径。')
|
||||
}
|
||||
if (pipelineInputFile.contains('..') || !(pipelineInputFile ==~ /^[A-Za-z0-9._\/-]+$/)) {
|
||||
error("INPUT_SOURCE=pipeline_archive 时 INPUT_FILE 必须是安全的归档相对路径: ${pipelineInputFile}")
|
||||
}
|
||||
if (manualInputFilename) {
|
||||
error('INPUT_SOURCE=pipeline_archive 时不能同时上传 MANUAL_INPUT_FILE。')
|
||||
}
|
||||
env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE = pipelineInputFile
|
||||
} else {
|
||||
if (!manualInputFilename) {
|
||||
error('INPUT_SOURCE=manual_upload 时必须上传 MANUAL_INPUT_FILE。')
|
||||
}
|
||||
if (params.EXPORT_BUILD_NUMBER_TO_IMPORT?.trim()) {
|
||||
error('INPUT_SOURCE=manual_upload 时不能填写 EXPORT_BUILD_NUMBER_TO_IMPORT。')
|
||||
}
|
||||
if (params.INPUT_FILE?.trim()) {
|
||||
error('INPUT_SOURCE=manual_upload 时不能填写 INPUT_FILE;请使用 MANUAL_INPUT_FILE 上传数据源。')
|
||||
}
|
||||
}
|
||||
if (params.INCREMENTAL && params.REPLACE_EXISTING) {
|
||||
error('INCREMENTAL 不能和 REPLACE_EXISTING 同时启用。')
|
||||
}
|
||||
if (!params.DRY_RUN) {
|
||||
if (!params.CONFIRM_IMPORT) {
|
||||
error('DRY_RUN=false 时必须勾选 CONFIRM_IMPORT。')
|
||||
}
|
||||
if (params.CONFIRM_DATABASE?.trim() != params.DATABASE.trim()) {
|
||||
error('DRY_RUN=false 时 CONFIRM_DATABASE 必须与 DATABASE 完全一致。')
|
||||
}
|
||||
if (inputSource == 'pipeline_archive' && params.CONFIRM_INPUT_FILE?.trim() != env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE) {
|
||||
error('DRY_RUN=false 时 CONFIRM_INPUT_FILE 必须与实际归档输入路径完全一致。')
|
||||
}
|
||||
if (inputSource == 'manual_upload' && !params.CONFIRM_INPUT_FILE?.trim()) {
|
||||
error('DRY_RUN=false 且 INPUT_SOURCE=manual_upload 时 CONFIRM_INPUT_FILE 必须填写上传文件原始文件名。')
|
||||
}
|
||||
if (inputSource == 'manual_upload' && params.CONFIRM_INPUT_FILE?.trim() != manualInputFilename) {
|
||||
error('DRY_RUN=false 且 INPUT_SOURCE=manual_upload 时 CONFIRM_INPUT_FILE 必须与上传文件原始文件名完全一致。')
|
||||
}
|
||||
if (params.REPLACE_EXISTING && !params.CONFIRM_REPLACE_EXISTING) {
|
||||
error('REPLACE_EXISTING=true 且 DRY_RUN=false 时必须勾选 CONFIRM_REPLACE_EXISTING。')
|
||||
}
|
||||
}
|
||||
|
||||
def backupDirectory = params.PRE_IMPORT_BACKUP_DIRECTORY?.trim() ? params.PRE_IMPORT_BACKUP_DIRECTORY.trim() : 'database-pre-import-backups'
|
||||
if (backupDirectory.startsWith('/') || backupDirectory.contains('..') || !(backupDirectory ==~ /^[A-Za-z0-9._\/-]+$/)) {
|
||||
error("PRE_IMPORT_BACKUP_DIRECTORY 必须是安全的相对路径: ${backupDirectory}")
|
||||
}
|
||||
|
||||
env.PRE_IMPORT_BACKUP_DIRECTORY = backupDirectory
|
||||
env.EFFECTIVE_PRE_IMPORT_BACKUP_NAME = "pre-import-${env.BUILD_NUMBER}.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Import Database') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
if (params.INPUT_SOURCE == 'pipeline_archive') {
|
||||
echo "[database-import] 使用归档数据源: job=${params.EXPORT_JOB_NAME}, build=${params.EXPORT_BUILD_NUMBER_TO_IMPORT}, file=${env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE}"
|
||||
copyArtifacts(
|
||||
projectName: params.EXPORT_JOB_NAME,
|
||||
selector: specific(params.EXPORT_BUILD_NUMBER_TO_IMPORT.trim()),
|
||||
filter: "${env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE},${env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE}.sha256",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
env.EFFECTIVE_INPUT_FILE = env.EFFECTIVE_PIPELINE_ARCHIVE_INPUT_FILE
|
||||
} else {
|
||||
echo "[database-import] 使用手动上传数据源: original_filename=${env.MANUAL_INPUT_FILE_FILENAME}"
|
||||
sh 'bash -lc "rm -rf manual-import-upload && mkdir -p manual-import-upload"'
|
||||
dir('manual-import-upload') {
|
||||
unstash 'MANUAL_INPUT_FILE'
|
||||
}
|
||||
env.EFFECTIVE_INPUT_FILE = 'manual-import-upload/MANUAL_INPUT_FILE'
|
||||
if (!params.DRY_RUN) {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
manual_filename="${MANUAL_INPUT_FILE_FILENAME:-}"
|
||||
if [[ -z "${manual_filename}" ]]; then
|
||||
echo "[database-import] 无法读取 MANUAL_INPUT_FILE_FILENAME,不能确认手动上传文件名。" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${CONFIRM_INPUT_FILE}" != "${manual_filename}" ]]; then
|
||||
echo "[database-import] CONFIRM_INPUT_FILE 必须与手动上传文件原始文件名一致: ${manual_filename}" >&2
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
def credentialBindings = []
|
||||
if (params.TOKEN_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.TOKEN_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_TOKEN'))
|
||||
}
|
||||
if (params.BOOTSTRAP_SECRET_CREDENTIAL_ID?.trim()) {
|
||||
credentialBindings.add(string(credentialsId: params.BOOTSTRAP_SECRET_CREDENTIAL_ID.trim(), variable: 'GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET'))
|
||||
}
|
||||
|
||||
def importStep = {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
chmod +x scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
|
||||
input_path="${EFFECTIVE_INPUT_FILE}"
|
||||
if [[ "${input_path}" != /* ]]; then
|
||||
input_path="${WORKSPACE}/${input_path}"
|
||||
fi
|
||||
if [[ ! -s "${input_path}" ]]; then
|
||||
echo "[database-import] INPUT_FILE 不存在或为空: ${input_path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
backup_dir="${PRE_IMPORT_BACKUP_DIRECTORY}"
|
||||
backup_path="${backup_dir}/${EFFECTIVE_PRE_IMPORT_BACKUP_NAME}"
|
||||
mkdir -p "${backup_dir}"
|
||||
|
||||
completed=0
|
||||
on_exit() {
|
||||
local exit_code=$?
|
||||
if [[ "${exit_code}" -ne 0 && "${completed}" -ne 1 ]]; then
|
||||
echo "[database-import] 导入失败,保持维护模式。导入前备份如已生成,会保留在 ${backup_path}。" >&2
|
||||
fi
|
||||
exit "${exit_code}"
|
||||
}
|
||||
trap on_exit EXIT
|
||||
|
||||
scripts/deploy/maintenance-on.sh "database import ${DATABASE}"
|
||||
|
||||
backup_args=(scripts/spacetime-export-migration-json.mjs --out "${backup_path}" --database "${DATABASE}")
|
||||
import_args=(scripts/spacetime-import-migration-json.mjs --in "${input_path}" --database "${DATABASE}")
|
||||
for args_name in backup_args import_args; do
|
||||
declare -n current_args="${args_name}"
|
||||
# server-url 明确指向目标实例时,不再同时透传默认 alias,避免 CLI 授权与 HTTP 导入落到不同目标。
|
||||
if [[ -n "${SPACETIME_SERVER_URL}" ]]; then
|
||||
current_args+=(--server-url "${SPACETIME_SERVER_URL}")
|
||||
elif [[ -n "${SPACETIME_SERVER}" ]]; then
|
||||
current_args+=(--server "${SPACETIME_SERVER}")
|
||||
fi
|
||||
if [[ -n "${SPACETIME_ROOT_DIR}" ]]; then
|
||||
current_args+=(--root-dir "${SPACETIME_ROOT_DIR}")
|
||||
fi
|
||||
done
|
||||
|
||||
backup_args+=(--note "jenkins pre-import backup ${BUILD_TAG}")
|
||||
node "${backup_args[@]}"
|
||||
test -s "${backup_path}"
|
||||
sha256sum "${backup_path}" >"${backup_path}.sha256"
|
||||
|
||||
if [[ -n "${SERVER_BACKUP_DIRECTORY}" ]]; then
|
||||
mkdir -p "${SERVER_BACKUP_DIRECTORY}"
|
||||
install -m 0640 "${backup_path}" "${SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_PRE_IMPORT_BACKUP_NAME}"
|
||||
install -m 0640 "${backup_path}.sha256" "${SERVER_BACKUP_DIRECTORY}/${EFFECTIVE_PRE_IMPORT_BACKUP_NAME}.sha256"
|
||||
fi
|
||||
|
||||
if [[ -n "${INCLUDE_TABLES}" ]]; then
|
||||
import_args+=(--include "${INCLUDE_TABLES}")
|
||||
fi
|
||||
if [[ -n "${CHUNK_SIZE}" ]]; then
|
||||
import_args+=(--chunk-size "${CHUNK_SIZE}")
|
||||
fi
|
||||
if [[ "${DRY_RUN}" == "true" ]]; then
|
||||
import_args+=(--dry-run)
|
||||
fi
|
||||
if [[ "${INCREMENTAL}" == "true" ]]; then
|
||||
import_args+=(--incremental)
|
||||
fi
|
||||
if [[ "${REPLACE_EXISTING}" == "true" ]]; then
|
||||
import_args+=(--replace-existing)
|
||||
fi
|
||||
import_args+=(--note "jenkins database import ${BUILD_TAG}")
|
||||
|
||||
node "${import_args[@]}"
|
||||
|
||||
# 导入成功后只做本机健康检查;业务级数据核验仍以迁移脚本的表级统计为准。
|
||||
if [[ "${RUN_SMOKE_TEST}" == "true" && -n "${SMOKE_HEALTH_URL}" ]]; then
|
||||
curl -fsS --max-time 10 "${SMOKE_HEALTH_URL}" >/dev/null
|
||||
fi
|
||||
|
||||
scripts/deploy/maintenance-off.sh
|
||||
completed=1
|
||||
echo "[database-import] 完成: dry_run=${DRY_RUN}, database=${DATABASE}, source_commit=$(cat .jenkins-source-commit)"
|
||||
'
|
||||
'''
|
||||
}
|
||||
|
||||
if (credentialBindings) {
|
||||
withCredentials(credentialBindings) {
|
||||
importStep()
|
||||
}
|
||||
} else {
|
||||
importStep()
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
archiveArtifacts artifacts: "${env.PRE_IMPORT_BACKUP_DIRECTORY}/${env.EFFECTIVE_PRE_IMPORT_BACKUP_NAME},${env.PRE_IMPORT_BACKUP_DIRECTORY}/${env.EFFECTIVE_PRE_IMPORT_BACKUP_NAME}.sha256", allowEmptyArchive: true, fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "数据库导入流水线完成: target=${params.DEPLOY_TARGET}, database=${params.DATABASE}, dryRun=${params.DRY_RUN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
triggers {
|
||||
cron('0 4 * * *')
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
string(name: 'WEB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Web-Build', description: 'Web 构建流水线作业名')
|
||||
string(name: 'API_BUILD_JOB_NAME', defaultValue: 'Genarrative-Api-Build', description: 'API 构建流水线作业名')
|
||||
string(name: 'STDB_BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb 构建流水线作业名')
|
||||
string(name: 'WEB_DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Web-Deploy', description: 'Web 发布流水线作业名')
|
||||
string(name: 'API_DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Api-Deploy', description: 'API 发布流水线作业名')
|
||||
string(name: 'STDB_PUBLISH_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Publish', description: 'Stdb 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Resolve Source') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Components') {
|
||||
parallel {
|
||||
stage('Web') {
|
||||
steps {
|
||||
script {
|
||||
def webRun = build job: params.WEB_BUILD_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
]
|
||||
env.WEB_BUILD_NUMBER = webRun.number.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Api') {
|
||||
steps {
|
||||
script {
|
||||
def apiRun = build job: params.API_BUILD_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
]
|
||||
env.API_BUILD_NUMBER = apiRun.number.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Stdb') {
|
||||
steps {
|
||||
script {
|
||||
def stdbRun = build job: params.STDB_BUILD_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DATABASE', value: params.DATABASE),
|
||||
]
|
||||
env.STDB_BUILD_NUMBER = stdbRun.number.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Stdb') {
|
||||
steps {
|
||||
build job: params.STDB_PUBLISH_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DATABASE', value: params.DATABASE),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: params.STDB_BUILD_JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.STDB_BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Api') {
|
||||
steps {
|
||||
build job: params.API_DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: params.API_BUILD_JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.API_BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Web') {
|
||||
steps {
|
||||
build job: params.WEB_DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: params.WEB_BUILD_JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.WEB_BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Full Build-And-Deploy 完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent')
|
||||
booleanParam(name: 'CONFIRM_PROVISION', defaultValue: false, description: '确认执行服务器初始化;未勾选时只允许 dry-run')
|
||||
booleanParam(name: 'DRY_RUN', defaultValue: true, description: '只打印将执行的服务器初始化命令,不写入系统配置')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit')
|
||||
string(name: 'SERVER_NAME', defaultValue: 'genarrative.example.com', description: 'Nginx server_name 与证书域名')
|
||||
string(name: 'SPACETIME_BIN_SOURCE', defaultValue: '/usr/local/bin/spacetime', description: '服务器上已有 spacetime CLI 路径')
|
||||
string(name: 'SPACETIME_ROOT', defaultValue: '/stdb', description: 'SpacetimeDB root-dir')
|
||||
string(name: 'RELEASE_ROOT', defaultValue: '/opt/genarrative/releases', description: 'release 根目录')
|
||||
string(name: 'CURRENT_LINK', defaultValue: '/opt/genarrative/current', description: '当前版本软链接')
|
||||
string(name: 'WEB_LINK', defaultValue: '/srv/genarrative/web', description: 'Nginx 静态站点目录或软链接')
|
||||
string(name: 'API_ENV_FILE', defaultValue: '/etc/genarrative/api-server.env', description: 'api-server 环境文件')
|
||||
string(name: 'API_PORT', defaultValue: '8082', description: 'api-server 本机监听端口')
|
||||
booleanParam(name: 'INSTALL_NGINX_CONFIG', defaultValue: true, description: '安装 Nginx 配置并执行 nginx -t')
|
||||
booleanParam(name: 'ENABLE_SERVICES', defaultValue: true, description: '启用并启动 spacetimedb 与 api-server systemd 服务')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release provision 需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。')
|
||||
}
|
||||
if (!params.DRY_RUN && !params.CONFIRM_PROVISION) {
|
||||
error('执行服务器初始化前必须勾选 CONFIRM_PROVISION;否则请保持 DRY_RUN=true。')
|
||||
}
|
||||
if (!params.SERVER_NAME?.trim()) {
|
||||
error('SERVER_NAME 不能为空。')
|
||||
}
|
||||
if (!params.SPACETIME_BIN_SOURCE?.trim()) {
|
||||
error('SPACETIME_BIN_SOURCE 不能为空。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout Provision Files') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Provision Server') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
require_path() {
|
||||
local path="$1"
|
||||
if [[ ! -e "${path}" ]]; then
|
||||
echo "[server-provision] 缺少必要文件: ${path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
echo "+ $*"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_file() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
local mode="$3"
|
||||
echo "+ install -m ${mode} ${source} ${target}"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m "${mode}" "${source}" "${target}"
|
||||
fi
|
||||
}
|
||||
|
||||
render_nginx_config() {
|
||||
sed "s/genarrative.example.com/${SERVER_NAME}/g" deploy/nginx/genarrative.conf
|
||||
}
|
||||
|
||||
render_api_env_example() {
|
||||
sed \
|
||||
-e "s|^GENARRATIVE_API_PORT=.*|GENARRATIVE_API_PORT=${API_PORT}|" \
|
||||
-e "s|^GENARRATIVE_SPACETIME_SERVER_URL=.*|GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3000|" \
|
||||
deploy/env/api-server.env.example
|
||||
}
|
||||
|
||||
escape_sed_replacement() {
|
||||
printf "%s" "$1" | sed "s/[&|]/\\\\&/g"
|
||||
}
|
||||
|
||||
render_spacetimedb_service() {
|
||||
local root_escaped
|
||||
root_escaped="$(escape_sed_replacement "${SPACETIME_ROOT}")"
|
||||
sed \
|
||||
-e "s|/stdb|${root_escaped}|g" \
|
||||
deploy/systemd/spacetimedb.service
|
||||
}
|
||||
|
||||
render_api_service() {
|
||||
local current_escaped env_escaped
|
||||
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
||||
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
||||
sed \
|
||||
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
||||
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
|
||||
deploy/systemd/genarrative-api.service
|
||||
}
|
||||
|
||||
require_path deploy/systemd/spacetimedb.service
|
||||
require_path deploy/systemd/genarrative-api.service
|
||||
require_path deploy/nginx/genarrative.conf
|
||||
require_path deploy/nginx/snippets/genarrative-maintenance.conf
|
||||
require_path deploy/env/api-server.env.example
|
||||
require_path scripts/deploy/maintenance-on.sh
|
||||
require_path scripts/deploy/maintenance-off.sh
|
||||
require_path scripts/deploy/maintenance-status.sh
|
||||
|
||||
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, source_commit=$(cat .jenkins-source-commit)"
|
||||
|
||||
run_cmd id
|
||||
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /var/lib/genarrative/maintenance /var/lib/genarrative/auth
|
||||
|
||||
if ! id spacetimedb >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir "${SPACETIME_ROOT}" --shell /usr/sbin/nologin spacetimedb
|
||||
else
|
||||
echo "[server-provision] 用户已存在: spacetimedb"
|
||||
fi
|
||||
|
||||
if ! id genarrative >/dev/null 2>&1; then
|
||||
run_cmd useradd --system --home-dir /opt/genarrative --shell /usr/sbin/nologin genarrative
|
||||
else
|
||||
echo "[server-provision] 用户已存在: genarrative"
|
||||
fi
|
||||
|
||||
run_cmd chown -R spacetimedb:spacetimedb "${SPACETIME_ROOT}"
|
||||
run_cmd chown -R genarrative:genarrative /opt/genarrative /var/lib/genarrative /srv/genarrative
|
||||
|
||||
if [[ ! -x "${SPACETIME_BIN_SOURCE}" ]]; then
|
||||
echo "[server-provision] spacetime CLI 不存在或不可执行: ${SPACETIME_BIN_SOURCE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "+ install -m 0755 ${SPACETIME_BIN_SOURCE} ${SPACETIME_ROOT}/spacetime"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
install -m 0755 "${SPACETIME_BIN_SOURCE}" "${SPACETIME_ROOT}/spacetime"
|
||||
chown spacetimedb:spacetimedb "${SPACETIME_ROOT}/spacetime"
|
||||
fi
|
||||
|
||||
spacetimedb_service="$(mktemp)"
|
||||
api_service="$(mktemp)"
|
||||
render_spacetimedb_service >"${spacetimedb_service}"
|
||||
render_api_service >"${api_service}"
|
||||
install_file "${spacetimedb_service}" /etc/systemd/system/spacetimedb.service 0644
|
||||
install_file "${api_service}" /etc/systemd/system/genarrative-api.service 0644
|
||||
rm -f "${spacetimedb_service}" "${api_service}"
|
||||
|
||||
if [[ ! -f "${API_ENV_FILE}" ]]; then
|
||||
echo "+ create ${API_ENV_FILE} from example"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
render_api_env_example >"${API_ENV_FILE}"
|
||||
chmod 0600 "${API_ENV_FILE}"
|
||||
chown root:root "${API_ENV_FILE}"
|
||||
fi
|
||||
else
|
||||
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
|
||||
fi
|
||||
|
||||
if [[ "${INSTALL_NGINX_CONFIG}" == "true" ]]; then
|
||||
run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d
|
||||
echo "+ render deploy/nginx/genarrative.conf -> /etc/nginx/conf.d/genarrative.conf"
|
||||
if [[ "${DRY_RUN}" != "true" ]]; then
|
||||
render_nginx_config >/etc/nginx/conf.d/genarrative.conf
|
||||
chmod 0644 /etc/nginx/conf.d/genarrative.conf
|
||||
fi
|
||||
install_file deploy/nginx/snippets/genarrative-maintenance.conf /etc/nginx/snippets/genarrative-maintenance.conf 0644
|
||||
run_cmd nginx -t
|
||||
fi
|
||||
|
||||
run_cmd systemctl daemon-reload
|
||||
if [[ "${ENABLE_SERVICES}" == "true" ]]; then
|
||||
run_cmd systemctl enable spacetimedb.service genarrative-api.service
|
||||
run_cmd systemctl restart spacetimedb.service
|
||||
if [[ -x "${CURRENT_LINK}/api-server" ]]; then
|
||||
run_cmd systemctl restart genarrative-api.service
|
||||
else
|
||||
echo "[server-provision] 尚未发现 ${CURRENT_LINK}/api-server,跳过 api-server 首次启动。后续 API deploy 会重启服务。"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[server-provision] 完成。若是首次初始化,请补齐 ${API_ENV_FILE} 的真实密钥后再启动 api-server。"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Server provision 完成: target=${params.DEPLOY_TARGET}, dryRun=${params.DRY_RUN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
CARGO_HOME = '/var/cache/genarrative-build/cargo-home'
|
||||
CARGO_TARGET_DIR = '/var/cache/genarrative-build/cargo-target/prod-release'
|
||||
CARGO_INCREMENTAL = '0'
|
||||
RUSTC_WRAPPER = 'sccache'
|
||||
SCCACHE_DIR = '/var/cache/genarrative-build/sccache'
|
||||
SCCACHE_CACHE_SIZE = '30G'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 Stdb module 发布')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Publish', description: 'Stdb module 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: 'PUBLISH_AFTER_BUILD=true 且目标为 release 时必须确认已有独立 release 部署 agent')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Stdb Module') {
|
||||
steps {
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
mkdir -p "${CARGO_HOME}" "${CARGO_TARGET_DIR}" "${SCCACHE_DIR}"
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
npm run build:production-release -- --component spacetime-module --name "${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
steps {
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm,build/${env.EFFECTIVE_BUILD_VERSION}/spacetime_module.wasm.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json", fingerprint: true
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
when {
|
||||
expression { return params.PUBLISH_AFTER_BUILD }
|
||||
}
|
||||
steps {
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.BUILD_NUMBER),
|
||||
string(name: 'DATABASE', value: params.DATABASE),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Stdb module 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
pipeline {
|
||||
agent none
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: '逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: '确认 release 目标已有独立 release 部署 agent;当前 Linux 开发/构建/开发部署 agent 不可冒充 release 部署机')
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '部署脚本来源分支')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '部署脚本来源 commit;上游触发时传实际构建 commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '待发布版本号')
|
||||
string(name: 'BUILD_JOB_NAME', defaultValue: 'Genarrative-Stdb-Module-Build', description: 'Stdb module 构建流水线作业名')
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', defaultValue: '', description: '要复制归档产物的上游构建号')
|
||||
string(name: 'DATABASE', defaultValue: 'genarrative-prod', description: '生产 SpacetimeDB database')
|
||||
string(name: 'SPACETIME_SERVER', defaultValue: 'local', description: 'SpacetimeDB server alias')
|
||||
booleanParam(name: 'CLEAR_DATABASE', defaultValue: false, description: '是否清空数据库后发布')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
if (params.DEPLOY_TARGET == 'release' && !params.CONFIRM_RELEASE_DEPLOY_AGENT) {
|
||||
error('release 部署需要先配置独立 release 部署 agent,并勾选 CONFIRM_RELEASE_DEPLOY_AGENT。当前 Linux 开发/构建/开发部署 agent 不能执行 release 部署。')
|
||||
}
|
||||
if (!params.BUILD_VERSION?.trim()) {
|
||||
error('BUILD_VERSION 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_JOB_NAME?.trim()) {
|
||||
error('BUILD_JOB_NAME 不能为空。')
|
||||
}
|
||||
if (!params.BUILD_NUMBER_TO_DEPLOY?.trim()) {
|
||||
error('BUILD_NUMBER_TO_DEPLOY 不能为空。')
|
||||
}
|
||||
if (!params.DATABASE?.trim()) {
|
||||
error('DATABASE 不能为空。')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout Publish Scripts') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Fetch Artifact') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
copyArtifacts(
|
||||
projectName: params.BUILD_JOB_NAME,
|
||||
selector: specific(params.BUILD_NUMBER_TO_DEPLOY),
|
||||
filter: "build/${params.BUILD_VERSION}/spacetime_module.wasm,build/${params.BUILD_VERSION}/spacetime_module.wasm.sha256,build/${params.BUILD_VERSION}/release-manifest.json",
|
||||
target: '.',
|
||||
fingerprintArtifacts: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish Stdb Module') {
|
||||
agent {
|
||||
label "${params.DEPLOY_TARGET == 'development' ? 'linux && genarrative-build' : 'linux && genarrative-release-deploy'}"
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
def clearArg = params.CLEAR_DATABASE ? '--clear-database' : ''
|
||||
sh """
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/deploy/production-stdb-publish.sh scripts/deploy/maintenance-on.sh scripts/deploy/maintenance-off.sh
|
||||
scripts/deploy/production-stdb-publish.sh \\
|
||||
--source-dir "build/${params.BUILD_VERSION}" \\
|
||||
--database "${params.DATABASE}" \\
|
||||
--server "${params.SPACETIME_SERVER}" \\
|
||||
${clearArg}
|
||||
'
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Stdb module 发布完成: version=${params.BUILD_VERSION}, database=${params.DATABASE}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
pipeline {
|
||||
agent {
|
||||
label 'linux && genarrative-build'
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
GIT_REMOTE_URL = 'http://127.0.0.1:3000/GenarrativeAI/Genarrative.git'
|
||||
}
|
||||
|
||||
parameters {
|
||||
string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: '源码分支,默认 master 最新提交')
|
||||
string(name: 'COMMIT_HASH', defaultValue: '', description: '可选,指定属于 SOURCE_BRANCH 的 Git commit')
|
||||
string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号,留空则使用 Jenkins BUILD_NUMBER')
|
||||
booleanParam(name: 'RUN_NPM_CI', defaultValue: false, description: '构建前是否执行 npm ci')
|
||||
booleanParam(name: 'PUBLISH_AFTER_BUILD', defaultValue: false, description: '构建成功后是否触发 Web 发布')
|
||||
string(name: 'DEPLOY_JOB_NAME', defaultValue: 'Genarrative-Web-Deploy', description: 'Web 发布流水线作业名')
|
||||
choice(name: 'DEPLOY_TARGET', choices: ['development', 'release'], description: 'PUBLISH_AFTER_BUILD=true 时的逻辑部署目标;development 使用当前 Linux 开发/构建/开发部署 agent')
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', defaultValue: false, description: 'PUBLISH_AFTER_BUILD=true 且目标为 release 时必须确认已有独立 release 部署 agent')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.SOURCE_BRANCH}"]],
|
||||
doGenerateSubmoduleConfigurations: false,
|
||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||
userRemoteConfigs: [[url: "${GIT_REMOTE_URL}"]],
|
||||
])
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
chmod +x scripts/jenkins-checkout-source.sh
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" \
|
||||
COMMIT_HASH="${COMMIT_HASH}" \
|
||||
GIT_REMOTE_URL="${GIT_REMOTE_URL}" \
|
||||
SOURCE_COMMIT_FILE=".jenkins-source-commit" \
|
||||
scripts/jenkins-checkout-source.sh
|
||||
'
|
||||
'''
|
||||
script {
|
||||
env.SOURCE_COMMIT = readFile('.jenkins-source-commit').trim()
|
||||
env.EFFECTIVE_BUILD_VERSION = params.BUILD_VERSION?.trim() ? params.BUILD_VERSION.trim() : env.BUILD_NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build Web') {
|
||||
steps {
|
||||
script {
|
||||
if (params.RUN_NPM_CI) {
|
||||
sh 'bash -lc "npm ci"'
|
||||
}
|
||||
}
|
||||
sh '''
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
SOURCE_BRANCH="${SOURCE_BRANCH}" SOURCE_COMMIT="${SOURCE_COMMIT}" \
|
||||
npm run build:production-release -- --component web --name "${EFFECTIVE_BUILD_VERSION}"
|
||||
'
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Archive') {
|
||||
steps {
|
||||
archiveArtifacts artifacts: "build/${env.EFFECTIVE_BUILD_VERSION}/web.tar.gz,build/${env.EFFECTIVE_BUILD_VERSION}/web.tar.gz.sha256,build/${env.EFFECTIVE_BUILD_VERSION}/release-manifest.json", fingerprint: true
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
when {
|
||||
expression { return params.PUBLISH_AFTER_BUILD }
|
||||
}
|
||||
steps {
|
||||
build job: params.DEPLOY_JOB_NAME,
|
||||
wait: true,
|
||||
propagate: true,
|
||||
parameters: [
|
||||
string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH),
|
||||
string(name: 'COMMIT_HASH', value: env.SOURCE_COMMIT),
|
||||
string(name: 'BUILD_VERSION', value: env.EFFECTIVE_BUILD_VERSION),
|
||||
string(name: 'DEPLOY_TARGET', value: params.DEPLOY_TARGET),
|
||||
booleanParam(name: 'CONFIRM_RELEASE_DEPLOY_AGENT', value: params.CONFIRM_RELEASE_DEPLOY_AGENT),
|
||||
string(name: 'BUILD_JOB_NAME', value: env.JOB_NAME),
|
||||
string(name: 'BUILD_NUMBER_TO_DEPLOY', value: env.BUILD_NUMBER),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Web 构建完成: version=${env.EFFECTIVE_BUILD_VERSION}, commit=${env.SOURCE_COMMIT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user