Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8386d7b02 | |||
| 5fe2e2c75c | |||
| 3e1789e124 |
@@ -0,0 +1,6 @@
|
||||
ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api
|
||||
ROLE_USER_SESSION_COOKIE=role_user_session_develop
|
||||
APP_ENV=develop
|
||||
APP_ENV_LABEL=测试环境
|
||||
PORT=3211
|
||||
HOSTNAME=0.0.0.0
|
||||
+5
-1
@@ -1,2 +1,6 @@
|
||||
ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api
|
||||
ROLE_USER_SESSION_COOKIE=role_user_session
|
||||
ROLE_USER_SESSION_COOKIE=role_user_session_local
|
||||
APP_ENV=local
|
||||
APP_ENV_LABEL=本地环境
|
||||
PORT=3210
|
||||
HOSTNAME=0.0.0.0
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api
|
||||
ROLE_USER_SESSION_COOKIE=role_user_session_test
|
||||
APP_ENV=test
|
||||
ROLE_USER_SESSION_COOKIE=role_user_session_develop
|
||||
APP_ENV=develop
|
||||
APP_ENV_LABEL=测试环境
|
||||
PORT=3211
|
||||
HOSTNAME=0.0.0.0
|
||||
|
||||
Vendored
-138
@@ -1,138 +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 = "role-user"
|
||||
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
|
||||
pnpm lint
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage("Build") {
|
||||
steps {
|
||||
script {
|
||||
sh isProductionDeploy() ? "pnpm build:prod" : "pnpm build:test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage("Package standalone") {
|
||||
steps {
|
||||
sh '''
|
||||
set -euo pipefail
|
||||
rm -rf .deploy/role-user
|
||||
mkdir -p .deploy/role-user/.next
|
||||
cp -R .next/standalone/. .deploy/role-user/
|
||||
cp -R .next/static .deploy/role-user/.next/static
|
||||
cp -R public .deploy/role-user/public
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage("Deploy test") {
|
||||
when {
|
||||
expression { isTestDeploy() }
|
||||
}
|
||||
steps {
|
||||
sh "bash deploy/jenkins/deploy-standalone.sh test .deploy/role-user"
|
||||
}
|
||||
}
|
||||
|
||||
stage("Deploy production") {
|
||||
when {
|
||||
expression { isProductionDeploy() && !params.SKIP_DEPLOY }
|
||||
}
|
||||
steps {
|
||||
sh "bash deploy/jenkins/deploy-standalone.sh production .deploy/role-user"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
parameters {
|
||||
gitParameter(
|
||||
name: 'RELEASE_TAG',
|
||||
type: 'PT_TAG',
|
||||
tagFilter: 'v*',
|
||||
branchFilter: 'origin/(.*)',
|
||||
sortMode: 'DESCENDING_SMART',
|
||||
selectedValue: 'TOP',
|
||||
useRepository: 'http://127.0.0.1:3001/my-project/role-user.git',
|
||||
quickFilterEnabled: true,
|
||||
listSize: '10',
|
||||
requiredParameter: true,
|
||||
description: '请选择要部署到生产环境的 Git Tag。列表自动来自当前项目仓库,生产只能从 Tag 发布。'
|
||||
)
|
||||
}
|
||||
|
||||
environment {
|
||||
CI = 'true'
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout Tag') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: '*/master']],
|
||||
userRemoteConfigs: [[
|
||||
url: 'http://127.0.0.1:3001/my-project/role-user.git'
|
||||
]]
|
||||
])
|
||||
sh '''
|
||||
set -eu
|
||||
test -n "$RELEASE_TAG"
|
||||
NORMALIZED_RELEASE_TAG="$(printf '%s' "$RELEASE_TAG" | sed 's/\\^{}$//')"
|
||||
echo "Deploying production tag: $NORMALIZED_RELEASE_TAG"
|
||||
git fetch --tags --force
|
||||
git checkout -f "refs/tags/$NORMALIZED_RELEASE_TAG"
|
||||
git describe --tags --exact-match HEAD
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Env') {
|
||||
steps {
|
||||
sh '''
|
||||
git --version
|
||||
node -v
|
||||
corepack --version
|
||||
corepack enable
|
||||
corepack prepare pnpm@11.5.0 --activate
|
||||
pnpm -v
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install') {
|
||||
steps {
|
||||
sh '''
|
||||
pnpm install --frozen-lockfile
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Check') {
|
||||
steps {
|
||||
sh '''
|
||||
pnpm typecheck
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh '''
|
||||
pnpm build:prod
|
||||
test -f .next/standalone/server.js
|
||||
test -d .next/static
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy Production') {
|
||||
steps {
|
||||
sh 'sudo /usr/local/bin/deploy-role-user-from-jenkins "$WORKSPACE"'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,23 @@ pnpm dev
|
||||
|
||||
```bash
|
||||
ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api
|
||||
ROLE_USER_SESSION_COOKIE=role_user_session
|
||||
ROLE_USER_SESSION_COOKIE=role_user_session_local
|
||||
APP_ENV=local
|
||||
APP_ENV_LABEL=本地环境
|
||||
PORT=3210
|
||||
HOSTNAME=0.0.0.0
|
||||
```
|
||||
|
||||
`ACCESS_MANAGE_API_BASE_URL` 指向 `access-manage` 服务端 API 根路径。员工登录会调用后端 `POST /api/auth/employee/login`,登录成功后只把 JWT 写入 Next.js 服务端 HttpOnly Cookie,不写入 `localStorage`。
|
||||
|
||||
环境标识按 `APP_ENV` 区分:
|
||||
|
||||
- `local`: 本地环境,默认使用 `.env.example` 复制出的 `.env.local`。
|
||||
- `develop`: 测试环境,参考 `.env.develop.example`;`.env.test.example` 保留为兼容入口。
|
||||
- `production`: 生产环境,参考 `.env.production.example`。
|
||||
|
||||
`APP_ENV_LABEL` 可覆盖页面右上角显示文案;未配置时会按 `APP_ENV` 自动显示“本地环境”“测试环境”或“生产环境”。Cookie 只在 `APP_ENV=production` 时设置 `Secure`,避免测试环境 HTTP 访问时登录后被浏览器丢弃 Cookie。
|
||||
|
||||
## 可用命令
|
||||
|
||||
```bash
|
||||
@@ -38,6 +50,10 @@ pnpm dev
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
pnpm build
|
||||
pnpm build:develop
|
||||
pnpm build:prod
|
||||
pnpm start:develop
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
## 当前实现范围
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
deploy_env="${1:?Usage: deploy-standalone.sh test|production [artifact_dir]}"
|
||||
artifact_dir="${2:-.deploy/role-user}"
|
||||
|
||||
case "${deploy_env}" in
|
||||
test)
|
||||
env_file=".env.test"
|
||||
;;
|
||||
production)
|
||||
env_file=".env.production"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown deploy environment: ${deploy_env}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "${artifact_dir}/server.js" ]]; then
|
||||
echo "Standalone server.js not found in ${artifact_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
project_name="${PROJECT_NAME:-role-user}"
|
||||
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)"
|
||||
|
||||
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 "${artifact_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 "Start with: set -a; source ${target_dir}/shared/${env_file}; set +a; node ${target_dir}/current/server.js"
|
||||
fi
|
||||
@@ -1,91 +0,0 @@
|
||||
# 环境拆分与流水线规则
|
||||
|
||||
`role-user` 是 Next.js standalone 服务。测试环境和生产环境通过独立部署目录、独立运行时环境变量和独立后端 API 地址拆分。
|
||||
|
||||
## 环境约定
|
||||
|
||||
| 环境 | 触发方式 | 代码依据 | 构建命令 | 默认部署目录 | 默认端口 | 后端 API |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 测试环境 | `develop` 合并后自动触发 Jenkins | `develop` 最新提交 | `pnpm build:test` | `/srv/www/test/role-user/current` | `3211` | `http://127.0.0.1:3501/api` |
|
||||
| 生产环境 | Jenkins 手动触发 | Gitea 项目 Tag | `pnpm build:prod` | `/srv/www/production/role-user/current` | `3210` | `http://127.0.0.1:3500/api` |
|
||||
|
||||
生产环境禁止因代码合并自动部署。生产部署时必须在 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 对应的提交,再部署。
|
||||
|
||||
## 运行时环境变量
|
||||
|
||||
真实环境变量必须留在服务器,不提交到仓库。
|
||||
|
||||
部署脚本默认读取:
|
||||
|
||||
```text
|
||||
/srv/www/test/role-user/shared/.env.test
|
||||
/srv/www/production/role-user/shared/.env.production
|
||||
```
|
||||
|
||||
示例文件:
|
||||
|
||||
```text
|
||||
.env.test.example
|
||||
.env.production.example
|
||||
```
|
||||
|
||||
`ACCESS_MANAGE_API_BASE_URL` 是服务端 BFF 使用的运行时变量,不加 `NEXT_PUBLIC_`,不会暴露到浏览器 bundle。Cookie 名测试和生产应分开,避免同域下会话互相覆盖。
|
||||
|
||||
## standalone 产物
|
||||
|
||||
`next.config.ts` 已启用 `output: "standalone"`。Jenkins 构建后会打包:
|
||||
|
||||
```text
|
||||
.next/standalone
|
||||
.next/static
|
||||
public
|
||||
```
|
||||
|
||||
部署脚本会发布到:
|
||||
|
||||
```text
|
||||
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/role-user/releases/<build>-<commit>
|
||||
```
|
||||
|
||||
并更新:
|
||||
|
||||
```text
|
||||
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/role-user/current
|
||||
```
|
||||
|
||||
如果 Jenkins 不在目标服务器上运行,可以配置:
|
||||
|
||||
```text
|
||||
DEPLOY_REMOTE=user@server
|
||||
```
|
||||
|
||||
## 生产发布流程
|
||||
|
||||
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 "role-user production release 2026-06-05"
|
||||
git push origin v2026.06.05-1
|
||||
```
|
||||
+6
-3
@@ -3,11 +3,14 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3210",
|
||||
"dev": "APP_ENV=local APP_ENV_LABEL=本地环境 ACCESS_MANAGE_API_BASE_URL=http://localhost:3500/api ROLE_USER_SESSION_COOKIE=role_user_session_local next dev -p 3210",
|
||||
"build": "next build",
|
||||
"build:test": "next build",
|
||||
"build:prod": "next build",
|
||||
"build:develop": "APP_ENV=develop APP_ENV_LABEL=测试环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api ROLE_USER_SESSION_COOKIE=role_user_session_develop next build",
|
||||
"build:test": "pnpm build:develop",
|
||||
"build:prod": "APP_ENV=production APP_ENV_LABEL=生产环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3500/api ROLE_USER_SESSION_COOKIE=role_user_session next build",
|
||||
"start": "next start",
|
||||
"start:develop": "APP_ENV=develop APP_ENV_LABEL=测试环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api ROLE_USER_SESSION_COOKIE=role_user_session_develop next start -p 3211",
|
||||
"start:prod": "APP_ENV=production APP_ENV_LABEL=生产环境 ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3500/api ROLE_USER_SESSION_COOKIE=role_user_session next start -p 3210",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit --incremental false"
|
||||
},
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { getAppEnvLabel } from "@/lib/environment";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -28,7 +29,7 @@ export const viewport: Viewport = {
|
||||
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
await connection();
|
||||
|
||||
const environmentLabel = process.env.APP_ENV_LABEL || "生产环境";
|
||||
const environmentLabel = getAppEnvLabel();
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import "server-only";
|
||||
|
||||
export type AppEnv = "local" | "develop" | "production";
|
||||
|
||||
export function getAppEnv(): AppEnv {
|
||||
const appEnv = process.env.APP_ENV;
|
||||
|
||||
if (appEnv === "production") {
|
||||
return "production";
|
||||
}
|
||||
|
||||
if (appEnv === "develop" || appEnv === "test") {
|
||||
return "develop";
|
||||
}
|
||||
|
||||
if (appEnv === "local") {
|
||||
return "local";
|
||||
}
|
||||
|
||||
return process.env.NODE_ENV === "production" ? "production" : "local";
|
||||
}
|
||||
|
||||
export function getAppEnvLabel() {
|
||||
if (process.env.APP_ENV_LABEL) {
|
||||
return process.env.APP_ENV_LABEL;
|
||||
}
|
||||
|
||||
const labels: Record<AppEnv, string> = {
|
||||
local: "本地环境",
|
||||
develop: "测试环境",
|
||||
production: "生产环境"
|
||||
};
|
||||
|
||||
return labels[getAppEnv()];
|
||||
}
|
||||
+3
-1
@@ -2,6 +2,8 @@ import "server-only";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
import { getAppEnv } from "@/lib/environment";
|
||||
|
||||
const DEFAULT_COOKIE_NAME = "role_user_session";
|
||||
|
||||
export function getSessionCookieName() {
|
||||
@@ -17,7 +19,7 @@ export async function setSessionToken(token: string) {
|
||||
|
||||
cookieStore.set(getSessionCookieName(), token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
secure: getAppEnv() === "production",
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 8
|
||||
|
||||
Reference in New Issue
Block a user