7 Commits

Author SHA1 Message Date
湛兮 a8d48ad745 测试 2 2026-06-03 22:50:11 +08:00
湛兮 93bc10a795 测试 2 2026-06-03 22:46:38 +08:00
湛兮 d3a79ee028 测试 2 2026-06-03 22:39:50 +08:00
湛兮 e228ec3973 测试推送 2026-06-03 16:54:48 +08:00
湛兮 9d5a92a4a2 测试推送 2026-06-03 16:52:38 +08:00
湛兮 3c75ea4775 测试推送 2026-06-03 16:44:09 +08:00
湛兮 925acb8a40 chore: update login hero title 2026-06-03 16:36:07 +08:00
8 changed files with 2 additions and 342 deletions
-6
View File
@@ -1,6 +0,0 @@
ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3500/api
ROLE_USER_SESSION_COOKIE=role_user_session
APP_ENV=production
APP_ENV_LABEL=生产环境
PORT=3210
HOSTNAME=0.0.0.0
-6
View File
@@ -1,6 +0,0 @@
ACCESS_MANAGE_API_BASE_URL=http://127.0.0.1:3501/api
ROLE_USER_SESSION_COOKIE=role_user_session_test
APP_ENV=test
APP_ENV_LABEL=测试环境
PORT=3211
HOSTNAME=0.0.0.0
Vendored
-138
View File
@@ -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"
}
}
}
}
-71
View File
@@ -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
-91
View File
@@ -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
```
-2
View File
@@ -5,8 +5,6 @@
"scripts": {
"dev": "next dev -p 3210",
"build": "next build",
"build:test": "next build",
"build:prod": "next build",
"start": "next start",
"lint": "eslint",
"typecheck": "tsc --noEmit --incremental false"
-16
View File
@@ -1029,22 +1029,6 @@ textarea:focus-visible {
color: var(--accent-ink);
}
.environment-badge {
background: rgba(20, 20, 24, 0.86);
border-radius: 999px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.16);
color: #ffffff;
font-size: 12px;
font-weight: 750;
letter-spacing: 0;
line-height: 1;
padding: 8px 10px;
position: fixed;
right: 12px;
top: 12px;
z-index: 60;
}
.skeleton {
animation: pulse 1.2s ease-in-out infinite;
background: linear-gradient(90deg, #ececef, #fafafa, #ececef);
+2 -12
View File
@@ -1,5 +1,4 @@
import type { Metadata, Viewport } from "next";
import { connection } from "next/server";
import "./globals.css";
@@ -25,19 +24,10 @@ export const viewport: Viewport = {
themeColor: "#ffffff"
};
export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
await connection();
const environmentLabel = process.env.APP_ENV_LABEL || "生产环境";
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-CN">
<body>
<div className="environment-badge" aria-label={`当前环境:${environmentLabel}`}>
{environmentLabel}
</div>
{children}
</body>
<body>{children}</body>
</html>
);
}