Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efa71ede6d | |||
| c0b561b6cb | |||
| 1dbeaa7209 | |||
| f1e3976c95 | |||
| 998a44431d |
Vendored
-122
@@ -1,122 +0,0 @@
|
|||||||
def isProductionDeploy() {
|
|
||||||
return params.DEPLOY_ENV == "production"
|
|
||||||
}
|
|
||||||
|
|
||||||
def isTestDeploy() {
|
|
||||||
def branch = env.BRANCH_NAME ?: ""
|
|
||||||
return params.DEPLOY_ENV == "test" && !params.SKIP_DEPLOY && branch == env.TEST_BRANCH
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonCPS
|
|
||||||
boolean isUserTriggeredBuild() {
|
|
||||||
return currentBuild.rawBuild.getCauses().any { cause ->
|
|
||||||
cause.class.name == "hudson.model.Cause\$UserIdCause"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeline {
|
|
||||||
agent any
|
|
||||||
|
|
||||||
options {
|
|
||||||
timestamps()
|
|
||||||
disableConcurrentBuilds()
|
|
||||||
buildDiscarder(logRotator(numToKeepStr: "30", artifactNumToKeepStr: "10"))
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters {
|
|
||||||
choice(
|
|
||||||
name: "DEPLOY_ENV",
|
|
||||||
choices: ["test", "production"],
|
|
||||||
description: "test: develop 合并后自动部署测试环境;production: 仅允许手动输入 Tag 部署"
|
|
||||||
)
|
|
||||||
string(name: "RELEASE_TAG", defaultValue: "", description: "生产部署必填,必须是 Gitea 中已存在的 Tag")
|
|
||||||
booleanParam(name: "SKIP_DEPLOY", defaultValue: false, description: "只构建检查,不执行部署")
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
PROJECT_NAME = "access-manage"
|
|
||||||
TEST_BRANCH = "develop"
|
|
||||||
DEPLOY_BASE_DIR = "/srv/www"
|
|
||||||
}
|
|
||||||
|
|
||||||
stages {
|
|
||||||
stage("Validate deploy policy") {
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
if (isProductionDeploy()) {
|
|
||||||
if (!isUserTriggeredBuild()) {
|
|
||||||
error("生产环境禁止自动触发,只能在 Jenkins 手动 Build With Parameters。")
|
|
||||||
}
|
|
||||||
if (!params.RELEASE_TAG?.trim()) {
|
|
||||||
error("生产环境部署必须填写 RELEASE_TAG,且只能从项目 Tag 部署。")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.DEPLOY_ENV == "test" && env.BRANCH_NAME && env.BRANCH_NAME != env.TEST_BRANCH) {
|
|
||||||
echo "当前分支 ${env.BRANCH_NAME} 不是 ${env.TEST_BRANCH},本次只构建检查,不自动部署测试环境。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Checkout production tag") {
|
|
||||||
when {
|
|
||||||
expression { isProductionDeploy() }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
set -euo pipefail
|
|
||||||
git fetch --tags --force origin '+refs/tags/*:refs/tags/*'
|
|
||||||
tag_commit="$(git rev-parse -q --verify "refs/tags/${RELEASE_TAG}^{commit}")"
|
|
||||||
if [ -z "${tag_commit}" ]; then
|
|
||||||
echo "Tag not found: ${RELEASE_TAG}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
git checkout -f "${tag_commit}"
|
|
||||||
git log -1 --oneline
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Install") {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
corepack enable || true
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Verify") {
|
|
||||||
steps {
|
|
||||||
sh "pnpm typecheck"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Build") {
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
sh isProductionDeploy() ? "pnpm build:pro" : "pnpm build:test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Deploy test") {
|
|
||||||
when {
|
|
||||||
expression { isTestDeploy() }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh "bash deploy/jenkins/deploy-backend.sh test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Deploy production") {
|
|
||||||
when {
|
|
||||||
expression { isProductionDeploy() && !params.SKIP_DEPLOY }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh "bash deploy/jenkins/deploy-backend.sh production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,14 +44,12 @@
|
|||||||
├── AGENTS.md # Codex/Agent 入口指令,当前指向 RTK.md
|
├── AGENTS.md # Codex/Agent 入口指令,当前指向 RTK.md
|
||||||
├── RTK.md # 项目协作规则和开发约定
|
├── RTK.md # 项目协作规则和开发约定
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── API.md # 前端对接接口文档
|
│ └── API.md # 前端对接接口文档
|
||||||
│ └── ENVIRONMENT_DEPLOYMENT.md # 测试/生产环境拆分与 Jenkins 规则
|
|
||||||
├── deploy/
|
├── deploy/
|
||||||
│ ├── env/
|
│ ├── env/
|
||||||
│ │ ├── test.env.example # 测试环境变量示例,不包含真实密码
|
│ │ ├── local.env.example # 本地环境变量示例,不包含真实密码
|
||||||
|
│ │ ├── test.env.example # develop/测试环境变量示例,不包含真实密码
|
||||||
│ │ └── production.env.example # 生产环境变量示例,不包含真实密码
|
│ │ └── production.env.example # 生产环境变量示例,不包含真实密码
|
||||||
│ ├── jenkins/
|
|
||||||
│ │ └── deploy-backend.sh # Jenkins 后端部署脚本
|
|
||||||
│ └── server/
|
│ └── server/
|
||||||
│ ├── create-env.sh # 在服务器生成测试/生产真实环境变量
|
│ ├── create-env.sh # 在服务器生成测试/生产真实环境变量
|
||||||
│ ├── docker-compose.mysql.test.yml # 测试 MySQL Compose 模板
|
│ ├── docker-compose.mysql.test.yml # 测试 MySQL Compose 模板
|
||||||
@@ -85,7 +83,6 @@
|
|||||||
│ │ └── tasks/ # 任务后台管理和员工端任务模块
|
│ │ └── tasks/ # 任务后台管理和员工端任务模块
|
||||||
│ └── shared/ # 通用响应结构和业务错误
|
│ └── shared/ # 通用响应结构和业务错误
|
||||||
├── docker-compose.yml # 本地 MySQL
|
├── docker-compose.yml # 本地 MySQL
|
||||||
├── Jenkinsfile # Jenkins 测试自动部署、生产手动 Tag 部署规则
|
|
||||||
├── package.json
|
├── package.json
|
||||||
├── pnpm-lock.yaml
|
├── pnpm-lock.yaml
|
||||||
├── README.md
|
├── README.md
|
||||||
@@ -98,14 +95,12 @@
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `.agents/skills/readme-structure-sync/` | 项目内 skill。约定当目录、重要文件或 `package.json` 脚本变化时,同步更新 README。 |
|
| `.agents/skills/readme-structure-sync/` | 项目内 skill。约定当目录、重要文件或 `package.json` 脚本变化时,同步更新 README。 |
|
||||||
| `.env.development` | 当前 `package.json` 脚本默认读取的本地开发环境变量文件;该文件只保留在本机,不提交到仓库。 |
|
| `.env.development` | 当前 `package.json` 脚本默认读取的本地开发环境变量文件;该文件只保留在本机,不提交到仓库。 |
|
||||||
| `.env.local` / `.env.production` | 本机已有的其他环境变量文件;代码已允许 `NODE_ENV=local` 和 `NODE_ENV=production`,切换脚本时可以复用。 |
|
| `.env.local` / `.env.production` | 本机已有的其他环境变量文件;代码已允许 `APP_ENV=local/develop/production` 区分本地、测试和生产,切换脚本时可以复用。 |
|
||||||
| `.gitignore` | 忽略本地环境变量、依赖目录、编译产物和系统文件,避免把无关文件推到仓库。 |
|
| `.gitignore` | 忽略本地环境变量、依赖目录、编译产物和系统文件,避免把无关文件推到仓库。 |
|
||||||
| `AGENTS.md` | Agent 工具读取的入口文件,当前通过 `@RTK.md` 引入项目规则。 |
|
| `AGENTS.md` | Agent 工具读取的入口文件,当前通过 `@RTK.md` 引入项目规则。 |
|
||||||
| `RTK.md` | 本项目的协作规则,例如使用中文说明、保持分层、改目录时同步 README。 |
|
| `RTK.md` | 本项目的协作规则,例如使用中文说明、保持分层、改目录时同步 README。 |
|
||||||
| `docs/API.md` | 面向前端对接的完整接口文档,包含认证、权限、字段约束、示例请求响应和错误码。 |
|
| `docs/API.md` | 面向前端对接的完整接口文档,包含认证、权限、字段约束、示例请求响应和错误码。 |
|
||||||
| `docs/ENVIRONMENT_DEPLOYMENT.md` | 测试/生产环境拆分、Jenkins 参数、Tag 发布和服务器路径约定。 |
|
| `deploy/env/` | 本地、develop/测试、生产环境变量示例,只给字段结构,不提交真实密码。 |
|
||||||
| `deploy/env/` | 测试/生产环境变量示例,只给字段结构,不提交真实密码。 |
|
|
||||||
| `deploy/jenkins/` | Jenkins 部署脚本。当前 `deploy-backend.sh` 会按环境发布到独立目录。 |
|
|
||||||
| `deploy/server/` | 服务器部署辅助文件。`create-env.sh` 在服务器本地生成测试/生产真实环境变量,Compose 模板分别启动测试和生产 MySQL。 |
|
| `deploy/server/` | 服务器部署辅助文件。`create-env.sh` 在服务器本地生成测试/生产真实环境变量,Compose 模板分别启动测试和生产 MySQL。 |
|
||||||
| `migrations/` | 数据库迁移目录。所有建表、改表、初始化基础数据的 SQL 都放在这里。 |
|
| `migrations/` | 数据库迁移目录。所有建表、改表、初始化基础数据的 SQL 都放在这里。 |
|
||||||
| `src/app.ts` | 创建 Fastify 应用,注册路由,处理健康检查和全局错误。 |
|
| `src/app.ts` | 创建 Fastify 应用,注册路由,处理健康检查和全局错误。 |
|
||||||
@@ -124,7 +119,6 @@
|
|||||||
| `src/modules/tasks/` | 任务模块,负责后台任务管理、员工端任务处理和任务事件日志。 |
|
| `src/modules/tasks/` | 任务模块,负责后台任务管理、员工端任务处理和任务事件日志。 |
|
||||||
| `src/shared/` | 跨模块复用的响应结构和业务错误类型。 |
|
| `src/shared/` | 跨模块复用的响应结构和业务错误类型。 |
|
||||||
| `docker-compose.yml` | 本地开发用 MySQL 容器配置。 |
|
| `docker-compose.yml` | 本地开发用 MySQL 容器配置。 |
|
||||||
| `Jenkinsfile` | 流水线入口。`develop` 合并自动部署测试环境;生产环境只能手动选择 Tag 部署。 |
|
|
||||||
| `package.json` | 项目信息、依赖和常用脚本;脚本会读取现有 `.env.development`。 |
|
| `package.json` | 项目信息、依赖和常用脚本;脚本会读取现有 `.env.development`。 |
|
||||||
| `pnpm-lock.yaml` | pnpm 锁文件,保证依赖版本一致。 |
|
| `pnpm-lock.yaml` | pnpm 锁文件,保证依赖版本一致。 |
|
||||||
| `tsconfig.json` | TypeScript 编译配置。 |
|
| `tsconfig.json` | TypeScript 编译配置。 |
|
||||||
@@ -159,6 +153,8 @@ pnpm install
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
APP_ENV=local
|
||||||
|
APP_ENV_LABEL=本地环境
|
||||||
PORT=3500
|
PORT=3500
|
||||||
|
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
@@ -172,7 +168,7 @@ JWT_SECRET=请使用至少 32 位的随机字符串
|
|||||||
JWT_EXPIRES_IN=2h
|
JWT_EXPIRES_IN=2h
|
||||||
```
|
```
|
||||||
|
|
||||||
代码允许的 `NODE_ENV` 值是:`local`、`development`、`test`、`production`。如果改用 `.env.local` 或 `.env.production` 启动,也需要包含同样的 `JWT_SECRET` 和 `JWT_EXPIRES_IN`。
|
代码允许的 `APP_ENV` 值是:`local`、`develop`、`production`,其中历史 `test` 会兼容为 develop/测试环境。未显式配置 `APP_ENV_LABEL` 时,健康检查会按环境输出“本地环境”“测试环境”或“生产环境”。如果改用 `.env.local`、`.env.test` 或 `.env.production` 启动,也需要包含同样的 `JWT_SECRET` 和 `JWT_EXPIRES_IN`。
|
||||||
|
|
||||||
## 启动步骤
|
## 启动步骤
|
||||||
|
|
||||||
@@ -314,7 +310,7 @@ curl http://127.0.0.1:3500/health # 生产
|
|||||||
|
|
||||||
只有返回里出现 `database: "up"`,才代表后端服务和 MySQL 都连通。
|
只有返回里出现 `database: "up"`,才代表后端服务和 MySQL 都连通。
|
||||||
|
|
||||||
如果手动压缩部署,至少需要包含 `dist/`、`migrations/`、`deploy/`、`package.json` 和 `pnpm-lock.yaml`。不要把本机 `.env.*`、`node_modules/`、`src/`、`tsconfig.json` 和 `.git/` 打进部署包。
|
如果手动压缩部署,至少需要包含 `dist/`、`migrations/`、`package.json` 和 `pnpm-lock.yaml`。不要把本机 `.env.*`、`node_modules/`、`src/`、`tsconfig.json` 和 `.git/` 打进部署包。
|
||||||
|
|
||||||
可以在本机执行:
|
可以在本机执行:
|
||||||
|
|
||||||
@@ -322,15 +318,11 @@ curl http://127.0.0.1:3500/health # 生产
|
|||||||
pnpm build:dev
|
pnpm build:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
命令执行完成后会得到最新的 `dist/`。如果需要压缩上传,由部署者手动选择要打包的文件;测试构建可执行 `pnpm build:test`,正式生产构建时执行 `pnpm build:pro`。
|
命令执行完成后会得到最新的 `dist/`。如果需要压缩上传,由部署者手动选择要打包的文件;develop/测试构建可执行 `pnpm build:develop` 或兼容命令 `pnpm build:test`,正式生产构建时执行 `pnpm build:pro`。
|
||||||
|
|
||||||
## Jenkins 环境规则
|
## 环境构建约定
|
||||||
|
|
||||||
流水线规则见 [docs/ENVIRONMENT_DEPLOYMENT.md](./docs/ENVIRONMENT_DEPLOYMENT.md)。
|
仓库只维护项目自身的构建脚本和环境变量示例,不维护流水线文件。测试、生产的触发规则和部署路径由外部 CI/CD 平台配置。
|
||||||
|
|
||||||
- 测试环境:`develop` 合并后自动触发,部署到 `/srv/www/test/access-manage/current`。
|
|
||||||
- 生产环境:禁止代码合并自动触发,只能在 Jenkins 手动选择 `DEPLOY_ENV=production` 并填写 Gitea 项目 Tag。
|
|
||||||
- 生产部署会先 checkout 到 `RELEASE_TAG` 对应的提交,再构建和部署。
|
|
||||||
|
|
||||||
## package.json 脚本说明
|
## package.json 脚本说明
|
||||||
|
|
||||||
@@ -341,14 +333,17 @@ pnpm build:dev
|
|||||||
| `pnpm dev` | 使用现有 `.env.development` 启动开发服务,并通过 `tsx watch` 监听代码变化。 | 日常开发接口时使用。 |
|
| `pnpm dev` | 使用现有 `.env.development` 启动开发服务,并通过 `tsx watch` 监听代码变化。 | 日常开发接口时使用。 |
|
||||||
| `pnpm build` | 使用 `tsc` 编译 TypeScript,输出到 `dist/`。 | 准备运行编译产物或发布前验证时使用。 |
|
| `pnpm build` | 使用 `tsc` 编译 TypeScript,输出到 `dist/`。 | 准备运行编译产物或发布前验证时使用。 |
|
||||||
| `pnpm build:dev` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备开发/测试服务器部署产物时使用。 |
|
| `pnpm build:dev` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备开发/测试服务器部署产物时使用。 |
|
||||||
| `pnpm build:test` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | Jenkins 测试环境构建时使用。 |
|
| `pnpm build:develop` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备 develop/测试环境部署产物时使用。 |
|
||||||
| `pnpm build:pro` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备正式生产部署产物时使用。 |
|
| `pnpm build:test` | `pnpm build:develop` 的兼容命令。 | 旧测试环境脚本继续可用。 |
|
||||||
|
| `pnpm build:pro` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备生产环境部署产物时使用。 |
|
||||||
| `pnpm start` | 使用现有 `.env.development` 运行 `dist/server.js`。 | 已经执行过 `pnpm build` 后,用编译产物启动服务。 |
|
| `pnpm start` | 使用现有 `.env.development` 运行 `dist/server.js`。 | 已经执行过 `pnpm build` 后,用编译产物启动服务。 |
|
||||||
| `pnpm start:test` | 使用 `.env.test` 运行 `dist/server.js`。 | 服务器测试环境启动编译产物时使用。 |
|
| `pnpm start:develop` | 使用 `.env.test` 运行 `dist/server.js`,其中 `APP_ENV=develop`。 | 服务器 develop/测试环境启动编译产物时使用。 |
|
||||||
|
| `pnpm start:test` | `pnpm start:develop` 的兼容命令。 | 旧测试环境服务命令继续可用。 |
|
||||||
| `pnpm start:prod` | 使用 `.env.production` 运行 `dist/server.js`。 | 服务器生产环境启动编译产物时使用。 |
|
| `pnpm start:prod` | 使用 `.env.production` 运行 `dist/server.js`。 | 服务器生产环境启动编译产物时使用。 |
|
||||||
| `pnpm typecheck` | 执行 `tsc --noEmit`,只检查类型,不生成文件。 | 改 TypeScript 代码后快速确认类型是否正确。 |
|
| `pnpm typecheck` | 执行 `tsc --noEmit`,只检查类型,不生成文件。 | 改 TypeScript 代码后快速确认类型是否正确。 |
|
||||||
| `pnpm db:migrate` | 使用现有 `.env.development` 运行 `src/db/migrate.ts`,按顺序执行 `migrations/*.sql`。 | 第一次启动项目、拉到新迁移、改数据库结构后使用。 |
|
| `pnpm db:migrate` | 使用现有 `.env.development` 运行 `src/db/migrate.ts`,按顺序执行 `migrations/*.sql`。 | 第一次启动项目、拉到新迁移、改数据库结构后使用。 |
|
||||||
| `pnpm db:migrate:test` | 使用 `.env.test` 运行 `dist/db/migrate.js`,按顺序执行 `migrations/*.sql`。 | 服务器测试环境建表、升级表结构或初始化基础数据时使用。 |
|
| `pnpm db:migrate:develop` | 使用 `.env.test` 运行 `dist/db/migrate.js`,按顺序执行 `migrations/*.sql`。 | 服务器 develop/测试环境建表、升级表结构或初始化基础数据时使用。 |
|
||||||
|
| `pnpm db:migrate:test` | `pnpm db:migrate:develop` 的兼容命令。 | 旧测试环境迁移命令继续可用。 |
|
||||||
| `pnpm db:migrate:prod` | 使用 `.env.production` 运行 `dist/db/migrate.js`,按顺序执行 `migrations/*.sql`。 | 服务器生产环境建表、升级表结构或初始化基础数据时使用。 |
|
| `pnpm db:migrate:prod` | 使用 `.env.production` 运行 `dist/db/migrate.js`,按顺序执行 `migrations/*.sql`。 | 服务器生产环境建表、升级表结构或初始化基础数据时使用。 |
|
||||||
| `pnpm db:shell` | 进入 Docker 容器里的 MySQL 命令行。 | 需要手动查看表结构或查询数据时使用。 |
|
| `pnpm db:shell` | 进入 Docker 容器里的 MySQL 命令行。 | 需要手动查看表结构或查询数据时使用。 |
|
||||||
| `pnpm mysql:up` | 启动本地 MySQL 容器。 | 开发前先启动数据库。 |
|
| `pnpm mysql:up` | 启动本地 MySQL 容器。 | 开发前先启动数据库。 |
|
||||||
|
|||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
NODE_ENV=development
|
||||||
|
APP_ENV=local
|
||||||
|
APP_ENV_LABEL=本地环境
|
||||||
|
PORT=3500
|
||||||
|
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3307
|
||||||
|
DB_USER=access_user
|
||||||
|
DB_PASSWORD=replace-with-local-password
|
||||||
|
DB_NAME=access_manage
|
||||||
|
DB_CONNECTION_LIMIT=10
|
||||||
|
|
||||||
|
JWT_SECRET=replace-with-at-least-32-characters-local-secret
|
||||||
|
JWT_EXPIRES_IN=2h
|
||||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
NODE_ENV=test
|
NODE_ENV=test
|
||||||
APP_ENV=test
|
APP_ENV=develop
|
||||||
APP_ENV_LABEL=测试环境
|
APP_ENV_LABEL=测试环境
|
||||||
PORT=3501
|
PORT=3501
|
||||||
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
deploy_env="${1:?Usage: deploy-backend.sh test|production}"
|
|
||||||
|
|
||||||
case "${deploy_env}" in
|
|
||||||
test)
|
|
||||||
env_file=".env.test"
|
|
||||||
;;
|
|
||||||
production)
|
|
||||||
env_file=".env.production"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown deploy environment: ${deploy_env}" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
project_name="${PROJECT_NAME:-access-manage}"
|
|
||||||
base_dir="${DEPLOY_BASE_DIR:-/srv/www}"
|
|
||||||
target_dir="${DEPLOY_TARGET_DIR:-${base_dir}/${deploy_env}/${project_name}}"
|
|
||||||
deploy_remote="${DEPLOY_REMOTE:-}"
|
|
||||||
release_id="${BUILD_NUMBER:-manual}-$(git rev-parse --short=12 HEAD 2>/dev/null || date +%Y%m%d%H%M%S)"
|
|
||||||
package_dir=".deploy/${project_name}"
|
|
||||||
|
|
||||||
rm -rf "${package_dir}"
|
|
||||||
mkdir -p "${package_dir}"
|
|
||||||
|
|
||||||
cp -R dist "${package_dir}/dist"
|
|
||||||
cp -R migrations "${package_dir}/migrations"
|
|
||||||
cp -R deploy "${package_dir}/deploy"
|
|
||||||
cp package.json pnpm-lock.yaml "${package_dir}/"
|
|
||||||
|
|
||||||
remote_shell() {
|
|
||||||
if [[ -n "${deploy_remote}" ]]; then
|
|
||||||
ssh "${deploy_remote}" "$@"
|
|
||||||
else
|
|
||||||
bash -lc "$*"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
remote_copy() {
|
|
||||||
local source_dir="$1"
|
|
||||||
local dest_dir="$2"
|
|
||||||
|
|
||||||
if [[ -n "${deploy_remote}" ]]; then
|
|
||||||
rsync -az --delete "${source_dir}/" "${deploy_remote}:${dest_dir}/"
|
|
||||||
else
|
|
||||||
mkdir -p "${dest_dir}"
|
|
||||||
rsync -az --delete "${source_dir}/" "${dest_dir}/"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
release_dir="${target_dir}/releases/${release_id}"
|
|
||||||
shared_dir="${target_dir}/shared"
|
|
||||||
|
|
||||||
remote_shell "mkdir -p '${release_dir}' '${shared_dir}' '${target_dir}/logs'"
|
|
||||||
remote_copy "${package_dir}" "${release_dir}"
|
|
||||||
remote_shell "
|
|
||||||
set -euo pipefail
|
|
||||||
if [ -f '${shared_dir}/${env_file}' ]; then
|
|
||||||
ln -sfn '../../shared/${env_file}' '${release_dir}/${env_file}'
|
|
||||||
else
|
|
||||||
echo 'Missing ${shared_dir}/${env_file}; create it before starting the service.' >&2
|
|
||||||
fi
|
|
||||||
ln -sfn '${release_dir}' '${target_dir}/current'
|
|
||||||
"
|
|
||||||
|
|
||||||
if [[ -n "${DEPLOY_POST_DEPLOY_CMD:-}" ]]; then
|
|
||||||
remote_shell "cd '${target_dir}/current' && ${DEPLOY_POST_DEPLOY_CMD}"
|
|
||||||
else
|
|
||||||
echo "Deployed ${project_name} ${deploy_env} to ${target_dir}/current"
|
|
||||||
echo "Set DEPLOY_POST_DEPLOY_CMD in Jenkins to install production dependencies, run migrations, and restart the service."
|
|
||||||
fi
|
|
||||||
@@ -5,10 +5,12 @@ target_dir="${1:-$(pwd)}"
|
|||||||
deploy_env="${2:-production}"
|
deploy_env="${2:-production}"
|
||||||
|
|
||||||
case "${deploy_env}" in
|
case "${deploy_env}" in
|
||||||
test)
|
test|develop)
|
||||||
mysql_env="${target_dir}/.env.test.mysql"
|
mysql_env="${target_dir}/.env.test.mysql"
|
||||||
app_env="${target_dir}/.env.test"
|
app_env="${target_dir}/.env.test"
|
||||||
node_env="test"
|
node_env="test"
|
||||||
|
app_env_name="develop"
|
||||||
|
app_env_label="测试环境"
|
||||||
app_port="${ACCESS_MANAGE_TEST_PORT:-3501}"
|
app_port="${ACCESS_MANAGE_TEST_PORT:-3501}"
|
||||||
mysql_port="${ACCESS_MANAGE_TEST_DB_PORT:-3308}"
|
mysql_port="${ACCESS_MANAGE_TEST_DB_PORT:-3308}"
|
||||||
mysql_database="${ACCESS_MANAGE_TEST_DB_NAME:-access_manage_test}"
|
mysql_database="${ACCESS_MANAGE_TEST_DB_NAME:-access_manage_test}"
|
||||||
@@ -17,12 +19,14 @@ case "${deploy_env}" in
|
|||||||
mysql_env="${target_dir}/.env.production.mysql"
|
mysql_env="${target_dir}/.env.production.mysql"
|
||||||
app_env="${target_dir}/.env.production"
|
app_env="${target_dir}/.env.production"
|
||||||
node_env="production"
|
node_env="production"
|
||||||
|
app_env_name="production"
|
||||||
|
app_env_label="生产环境"
|
||||||
app_port="${ACCESS_MANAGE_PRODUCTION_PORT:-3500}"
|
app_port="${ACCESS_MANAGE_PRODUCTION_PORT:-3500}"
|
||||||
mysql_port="${ACCESS_MANAGE_PRODUCTION_DB_PORT:-3307}"
|
mysql_port="${ACCESS_MANAGE_PRODUCTION_DB_PORT:-3307}"
|
||||||
mysql_database="${ACCESS_MANAGE_PRODUCTION_DB_NAME:-access_manage}"
|
mysql_database="${ACCESS_MANAGE_PRODUCTION_DB_NAME:-access_manage}"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 [target_dir] [test|production]" >&2
|
echo "Usage: $0 [target_dir] [test|develop|production]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -74,8 +78,8 @@ jwt_secret="$(openssl rand -hex 48)"
|
|||||||
|
|
||||||
cat > "${app_env}" <<EOF
|
cat > "${app_env}" <<EOF
|
||||||
NODE_ENV=${node_env}
|
NODE_ENV=${node_env}
|
||||||
APP_ENV=${node_env}
|
APP_ENV=${app_env_name}
|
||||||
APP_ENV_LABEL=$([[ "${deploy_env}" == "test" ]] && echo "测试环境" || echo "生产环境")
|
APP_ENV_LABEL=${app_env_label}
|
||||||
PORT=${app_port}
|
PORT=${app_port}
|
||||||
|
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
# 环境拆分与流水线规则
|
|
||||||
|
|
||||||
本项目拆分为测试环境和生产环境两套独立环境。两套环境使用不同部署目录、环境变量文件、数据库端口和数据目录,避免测试迭代影响生产。
|
|
||||||
|
|
||||||
## 环境约定
|
|
||||||
|
|
||||||
| 环境 | 触发方式 | 代码依据 | 默认部署目录 | 默认应用端口 | 默认数据库端口 | 数据目录 |
|
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| 测试环境 | `develop` 合并后自动触发 Jenkins | `develop` 最新提交 | `/srv/www/test/access-manage/current` | `3501` | `3308` | `/srv/data/test/access-manage/mysql` |
|
|
||||||
| 生产环境 | Jenkins 手动触发 | Gitea 项目 Tag | `/srv/www/production/access-manage/current` | `3500` | `3307` | `/srv/data/production/access-manage/mysql` |
|
|
||||||
|
|
||||||
生产环境禁止因代码合并自动部署。生产部署时必须在 Jenkins 参数中选择 `DEPLOY_ENV=production` 并填写已存在的 `RELEASE_TAG`。
|
|
||||||
|
|
||||||
## Jenkins 参数
|
|
||||||
|
|
||||||
| 参数 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| `DEPLOY_ENV` | `test` 或 `production`。默认 `test`。 |
|
|
||||||
| `RELEASE_TAG` | 生产环境必填,必须是 Gitea 仓库中已存在的 Tag。 |
|
|
||||||
| `SKIP_DEPLOY` | 为 `true` 时只执行安装、检查、构建,不部署。 |
|
|
||||||
|
|
||||||
`Jenkinsfile` 会强制校验:
|
|
||||||
|
|
||||||
- 测试环境只在 `develop` 分支自动部署。
|
|
||||||
- 生产环境必须手动触发。
|
|
||||||
- 生产环境必须填写 `RELEASE_TAG`。
|
|
||||||
- 生产环境构建会先 checkout 到该 Tag 对应的提交,再部署。
|
|
||||||
|
|
||||||
## 服务器环境变量
|
|
||||||
|
|
||||||
测试和生产的真实环境变量必须留在服务器,不提交到仓库。
|
|
||||||
|
|
||||||
生成测试环境:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /srv/www/test/access-manage
|
|
||||||
bash deploy/server/create-env.sh /srv/www/test/access-manage test
|
|
||||||
docker-compose --env-file .env.test.mysql -f deploy/server/docker-compose.mysql.test.yml up -d mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
生成生产环境:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /srv/www/production/access-manage
|
|
||||||
bash deploy/server/create-env.sh /srv/www/production/access-manage production
|
|
||||||
docker-compose --env-file .env.production.mysql -f deploy/server/docker-compose.mysql.production.yml up -d mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
部署脚本默认会读取:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/srv/www/test/access-manage/shared/.env.test
|
|
||||||
/srv/www/production/access-manage/shared/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
如果使用 `create-env.sh` 在 `current` 外生成了 env 文件,请把对应文件放到 `shared/` 下,或者在服务器上建立等价软链。
|
|
||||||
|
|
||||||
## 生产发布流程
|
|
||||||
|
|
||||||
1. 在需要发布的提交上创建 Tag。
|
|
||||||
2. 推送 Tag 到 Gitea。
|
|
||||||
3. Jenkins 手动 Build With Parameters。
|
|
||||||
4. 选择 `DEPLOY_ENV=production`。
|
|
||||||
5. 填写 `RELEASE_TAG`。
|
|
||||||
6. 执行构建部署。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git tag -a v2026.06.05-1 -m "access-manage production release 2026-06-05"
|
|
||||||
git push origin v2026.06.05-1
|
|
||||||
```
|
|
||||||
|
|
||||||
## 部署脚本
|
|
||||||
|
|
||||||
`deploy/jenkins/deploy-backend.sh` 会把 `dist/`、`migrations/`、`deploy/`、`package.json` 和 `pnpm-lock.yaml` 发布到:
|
|
||||||
|
|
||||||
```text
|
|
||||||
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/access-manage/releases/<build>-<commit>
|
|
||||||
```
|
|
||||||
|
|
||||||
并更新:
|
|
||||||
|
|
||||||
```text
|
|
||||||
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/access-manage/current
|
|
||||||
```
|
|
||||||
|
|
||||||
如果 Jenkins 不在目标服务器上运行,可以配置:
|
|
||||||
|
|
||||||
```text
|
|
||||||
DEPLOY_REMOTE=user@server
|
|
||||||
```
|
|
||||||
|
|
||||||
如果需要部署后自动安装依赖、执行迁移和重启服务,在 Jenkins 中配置:
|
|
||||||
|
|
||||||
```text
|
|
||||||
DEPLOY_POST_DEPLOY_CMD=pnpm install --prod --frozen-lockfile && pnpm db:migrate:prod && systemctl restart access-manage-production
|
|
||||||
```
|
|
||||||
|
|
||||||
测试环境应配置独立命令,例如使用 `.env.test` 和独立服务名。
|
|
||||||
+6
-3
@@ -7,14 +7,17 @@
|
|||||||
"dev": "DOTENV_CONFIG_PATH=.env.development tsx watch src/server.ts",
|
"dev": "DOTENV_CONFIG_PATH=.env.development tsx watch src/server.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"build:dev": "rm -rf dist && pnpm build",
|
"build:dev": "rm -rf dist && pnpm build",
|
||||||
"build:test": "rm -rf dist && pnpm build",
|
"build:develop": "rm -rf dist && pnpm build",
|
||||||
|
"build:test": "pnpm build:develop",
|
||||||
"build:pro": "rm -rf dist && pnpm build",
|
"build:pro": "rm -rf dist && pnpm build",
|
||||||
"start": "DOTENV_CONFIG_PATH=.env.development node dist/server.js",
|
"start": "DOTENV_CONFIG_PATH=.env.development node dist/server.js",
|
||||||
"start:test": "DOTENV_CONFIG_PATH=.env.test node dist/server.js",
|
"start:develop": "DOTENV_CONFIG_PATH=.env.test node dist/server.js",
|
||||||
|
"start:test": "pnpm start:develop",
|
||||||
"start:prod": "DOTENV_CONFIG_PATH=.env.production node dist/server.js",
|
"start:prod": "DOTENV_CONFIG_PATH=.env.production node dist/server.js",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"db:migrate": "DOTENV_CONFIG_PATH=.env.development tsx src/db/migrate.ts",
|
"db:migrate": "DOTENV_CONFIG_PATH=.env.development tsx src/db/migrate.ts",
|
||||||
"db:migrate:test": "DOTENV_CONFIG_PATH=.env.test node dist/db/migrate.js",
|
"db:migrate:develop": "DOTENV_CONFIG_PATH=.env.test node dist/db/migrate.js",
|
||||||
|
"db:migrate:test": "pnpm db:migrate:develop",
|
||||||
"db:migrate:prod": "DOTENV_CONFIG_PATH=.env.production node dist/db/migrate.js",
|
"db:migrate:prod": "DOTENV_CONFIG_PATH=.env.production node dist/db/migrate.js",
|
||||||
"db:shell": "docker compose exec mysql mysql -uaccess_user -paccess_pass access_manage",
|
"db:shell": "docker compose exec mysql mysql -uaccess_user -paccess_pass access_manage",
|
||||||
"mysql:up": "docker compose up -d mysql",
|
"mysql:up": "docker compose up -d mysql",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
+18
-4
@@ -5,9 +5,9 @@ import { z } from "zod";
|
|||||||
// 这样数据库密码、端口等配置错误会在服务启动阶段暴露,而不是等到请求进来才失败。
|
// 这样数据库密码、端口等配置错误会在服务启动阶段暴露,而不是等到请求进来才失败。
|
||||||
const envSchema = z.object({
|
const envSchema = z.object({
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["local", "development", "test", "production"])
|
.enum(["local", "development", "test", "develop", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
APP_ENV: z.enum(["local", "development", "test", "production"]).optional(),
|
APP_ENV: z.enum(["local", "development", "test", "develop", "production"]).optional(),
|
||||||
APP_ENV_LABEL: z.string().min(1).optional(),
|
APP_ENV_LABEL: z.string().min(1).optional(),
|
||||||
PORT: z.coerce.number().int().positive().default(3000),
|
PORT: z.coerce.number().int().positive().default(3000),
|
||||||
|
|
||||||
@@ -31,9 +31,23 @@ if (!result.success) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawAppEnv = result.data.APP_ENV ?? result.data.NODE_ENV;
|
||||||
|
const resolvedAppEnv =
|
||||||
|
rawAppEnv === "production"
|
||||||
|
? "production"
|
||||||
|
: rawAppEnv === "test" || rawAppEnv === "develop"
|
||||||
|
? "develop"
|
||||||
|
: "local";
|
||||||
|
const defaultAppEnvLabel =
|
||||||
|
{
|
||||||
|
local: "本地环境",
|
||||||
|
develop: "测试环境",
|
||||||
|
production: "生产环境"
|
||||||
|
}[resolvedAppEnv] ?? resolvedAppEnv;
|
||||||
|
|
||||||
// 其他模块只从 env 读取已校验过的值,不再直接访问 process.env。
|
// 其他模块只从 env 读取已校验过的值,不再直接访问 process.env。
|
||||||
export const env = {
|
export const env = {
|
||||||
...result.data,
|
...result.data,
|
||||||
APP_ENV: result.data.APP_ENV ?? result.data.NODE_ENV,
|
APP_ENV: resolvedAppEnv,
|
||||||
APP_ENV_LABEL: result.data.APP_ENV_LABEL ?? result.data.NODE_ENV
|
APP_ENV_LABEL: result.data.APP_ENV_LABEL ?? defaultAppEnvLabel
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user