Python(uv)+ Vue 项目基于 Jenkins + Gitea 的 CI/CD 配置

折腾 CI/CD,搞半天还用了自建 Git 服务

  • Jenkins
  • Ref: Jenkins Installation

为什么自己搭 Git

起因是因为要用 WebHook,给 Jenkins,如果用 Github Action 先不说 Private repo 的限制,Webhook 请求到国内网络的问题,另一个则是相反的,拉 Github 上 repo 的网络问题,因此还是决定自己搭内网 Git,而且 Gitea 蛮简单的

安装 Jenkins

Jenkins 基于 Java 运行,所以需要安装 Java 环境,按照官方的就行

1
2
3
sudo apt update
sudo apt install fontconfig openjdk-21-jre
java -version

这个从官方源下可能会慢一点

1
2
3
4
5
6
7
sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key
echo "deb [signed-by=/etc/apt/keyrings/jenkins-keyring.asc]" \
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt update
sudo apt install jenkins

我遇到的一个主要问题是 Jenkins 的默认 plugins 安装慢的没边,后来是先换了个 update-center.json 的源,也因为这个源后来装不上 node

参考 gh: lework/jenkins-update-center

注意这些最好在你第一次访问 Web 界面并开始安装 plugins 之前就配置好,否则需要中断安装,不过影响不大,我是中断恢复也没有问题

安装证书

1
2
3
[ ! -d /var/lib/jenkins/update-center-rootCAs ] && mkdir /var/lib/jenkins/update-center-rootCAs
wget https://cdn.jsdelivr.net/gh/lework/jenkins-update-center/rootCA/update-center.crt -O /var/lib/jenkins/update-center-rootCAs/update-center.crt
chown jenkins.jenkins -R /var/lib/jenkins/update-center-rootCAs

换源

1
2
3
4
sed -i 's#https://updates.jenkins.io/update-center.json#https://cdn.jsdelivr.net/gh/lework/jenkins-update-center/updates/tsinghua/update-center.json#' /var/lib/jenkins/hudson.model.UpdateCenter.xml
rm -f /var/lib/jenkins/updates/default.json

systemctl restart jenkins

我当时换的 ustc

安装必要插件

Manage Jenkins → Plugins → Available plugins → 搜索 NodeJS、Gitea、Generic Webhook Trigger、SSH Agent → Install

这里在 download 页面会提示你勾一个在闲时重启的框

配置 Node

Manage Jenkins → Tools → NodeJS installations

node installation with mirror

这里我配了国内源,然后和 G 老师说的有点出入,实际上要配置版本号,而不是下拉菜单,然后我在测试构建时遇到了问题

problem

后来是我把 update-center.json 换回官方源,才解决了这个问题

安装 uv

uv 与 Jenkins 无关,但是为了 Jenkins 用户的访问,需要全局安装

1
2
3
4
5
6
7
8
9
10
curl -LsSf https://astral.sh/uv/install.sh \
-o /tmp/uv-installer.sh

sudo env UV_UNMANAGED_INSTALL=/usr/local/bin \
sh /tmp/uv-installer.sh

sudo -u jenkins -H sh -c '
cd "$HOME"
/usr/local/bin/uv --no-config python install 3.13
'

测试 Pipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
pipeline {
agent any

tools {
nodejs 'node-24'
}

options {
timestamps()
disableConcurrentBuilds()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(
numToKeepStr: '20',
artifactNumToKeepStr: '10'
))
}

environment {
UV_PYTHON = '3.13'
UV_CACHE_DIR = "${WORKSPACE}/.cache/uv"
}

stages {
stage('Checkout') {
steps {
dir('example_frontend') {
git branch: 'main',
CredentialsId: 'gitea-http',
url: 'https://git.example.com/example/example_frontend.git'
}

dir('example_backend') {
git branch: 'main',
CredentialsId: 'gitea-http',
url: 'https://git.example.com/example/example_backend.git'
}
}
}

stage('Verify Source') {
steps {
sh '''
set -eux

test -f example_frontend/package.json
test -f example_frontend/pnpm-lock.yaml

test -f example_backend/pyproject.toml
test -f example_backend/uv.lock
test -f example_backend/main.py
'''
}
}

stage('Check Toolchains') {
steps {
sh '''
set -eux

node --version
npm --version
pnpm --version

uv --version
uv python find 3.13
uv run \
--no-project \
--python 3.13 \
python --version
'''
}
}

stage('Build Frontend') {
steps {
dir('example_frontend') {
sh '''
set -eux

pnpm install --frozen-lockfile
pnpm build

test -d dist
test -f dist/index.html
'''
}
}
}

stage('Check Backend') {
steps {
dir('example_backend') {
sh '''
set -eux

uv sync --locked
uv run --locked python --version
uv run --locked python -m compileall -q main.py
uv run --locked python -c "from main import app; print(app.title)"
uv run --locked python -c "import fastapi; print(fastapi.__version__)"
'''
}
}
}
}

post {
success {
archiveArtifacts(
artifacts: 'example_frontend/dist/**',
fingerprint: true,
allowEmptyArchive: false
)

echo 'Frontend and backend checks completed successfully.'
}

failure {
echo 'Pipeline failed. Check the first failed stage in Console Output.'
}

always {
echo "Build result: ${currentBuild.currentResult}"
}
}
}

这里需要配置 Gitea 的 credential(Access Token),只需要勾一个 repository 的 read

Gitea 右上角头像 → Settings / 设置 → Applications / 应用 → Manage Access Tokens / 管理访问令牌

gitea token

然后配置 Credentials,选 Username with password

gitea token credential

配置 WebHooks

添加 webhook 到 Credentials

先生成一个随机字符串

1
openssl rand -hex 32

存入 Credentials,选 Secret text

webhook token credential

这里其实可以先测一下 webhook,事实上需要在 app.ini 配置然后重启 gitea 服务才能生效

/etc/gitea/app.ini
1
2
[security]
ALLOWED_HOST_LIST = external,loopback

配置 repo 的 Webhook

webhook configuration

建议先不配置 pipeline

然后可以往下滚一点,点击下面的测试推送事件按钮做测试发送

webhook test

这里虽然是失败,但这是因为没有 pipeline 配置,所以是 ok 的

配置生产服务器的 deploy

因为我想的是简单部署,通过 service 注册后端,然后restart,而不是 docker,因此我需要一个 deploy 用户,从而需要密钥对登陆

创建与配置 deploy 用户

在 deploy 服务器上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sudo adduser \
--disabled-password \
--gecos "" \
deploy

sudo install -d \
-o deploy \
-g deploy \
-m 0755 \
/srv/jenkins-test

sudo install -d \
-o deploy \
-g deploy \
-m 0755 \
/srv/jenkins-test/releases

sudo install -d \
-o deploy \
-g deploy \
-m 0700 \
/home/deploy/.ssh

sudo touch /home/deploy/.ssh/authorized_keys
sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys
sudo chmod 0600 /home/deploy/.ssh/authorized_keys

生成密钥对,建议直接在 deploy 服务器上做

1
2
3
4
5
ssh-keygen \
-t ed25519 \
-a 100 \
-C "jenkins-production-deploy" \
-f jenkins-production-deploy

随后将公钥写入 deploy 服务器的 /home/deploy/.ssh/authorized_keys

在 Jenkins 中添加凭据,选 SSH Username with private key

Manage Jenkins → Credentials → System → Global credentials → Add Credentials

ssh credential

在 jenkins 服务器做这些操作,其中 TARGET_SERVER_IP 是 deploy 服务器的 IP 地址,用于先获取 fingerprint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ssh-keyscan \
-p 22 \
-t ed25519 \
TARGET_SERVER_IP \
> /tmp/jenkins-test-hostkey

sudo install \
-d \
-o jenkins \
-g jenkins \
-m 0700 \
/var/lib/jenkins/.ssh

sudo sh -c \
'cat /tmp/jenkins-test-hostkey >> /var/lib/jenkins/.ssh/known_hosts'

sudo chown jenkins:jenkins \
/var/lib/jenkins/.ssh/known_hosts

sudo chmod 0600 \
/var/lib/jenkins/.ssh/known_hosts

注册服务与配置后端环境

同样的,装 uv

1
2
3
4
5
6
7
curl -LsSf \
https://astral.sh/uv/install.sh \
-o /tmp/uv-installer.sh

sudo env \
UV_UNMANAGED_INSTALL=/usr/local/bin \
sh /tmp/uv-installer.sh

创建后端 env 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sudo install \
-d \
-o root \
-g deploy \
-m 0750 \
/etc/jenkins-test

sudo touch /etc/jenkins-test/backend.env

sudo chown root:deploy \
/etc/jenkins-test/backend.env

sudo chmod 0640 \
/etc/jenkins-test/backend.env

注册 systemd service

/etc/systemd/system/jenkins-test-backend.service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[Unit]
Description=Jenkins Test FastAPI Backend
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=deploy
Group=deploy

WorkingDirectory=/srv/jenkins-test/backend-current
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=-/etc/jenkins-test/backend.env

ExecStart=/srv/jenkins-test/backend-current/.venv/bin/uvicorn \
main:app \
--host 127.0.0.1 \
--port 8000 \
--proxy-headers \
--forwarded-allow-ips=127.0.0.1

Restart=on-failure
RestartSec=3
TimeoutStopSec=20
KillSignal=SIGINT

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

加载服务并设置开机自启动

1
2
sudo systemctl daemon-reload
sudo systemctl enable jenkins-test-backend

限制 deploy 用户权限,只允许 deploy 重启指定服务

1
sudo visudo -f /etc/sudoers.d/jenkins-test-deploy
1
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart jenkins-test-backend

配置 nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
server {
listen 80;
listen [::]:80;

server_name app.example.com;

root /srv/jenkins-test/frontend-current;
index index.html;

client_max_body_size 10m;

location ^~ /api/ {
proxy_pass http://127.0.0.1:8000;
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_connect_timeout 5s;
proxy_read_timeout 60s;
}

location / {
try_files $uri $uri/ /index.html;
}

location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2)$ {
try_files $uri =404;
expires 7d;
add_header Cache-Control "public";
}
}

Pipeline 配置

frontend-pipeline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
pipeline {
agent any

tools {
nodejs 'node-24'
}

triggers {
GenericTrigger(
genericVariables: [
[key: 'GITEA_REF', value: '$.ref'],
[key: 'GITEA_COMMIT', value: '$.after'],
[key: 'GITEA_REPOSITORY', value: '$.repository.full_name'],
[key: 'GITEA_PUSHER', value: '$.pusher.username']
],

causeString: 'Frontend push: $GITEA_COMMIT by $GITEA_PUSHER',

tokenCredentialId: 'gitea-webhook-token',

printContributedVariables: false,
printPostContent: false,
silentResponse: false,
shouldNotFlatten: false,

regexpFilterText: '$GITEA_REPOSITORY:$GITEA_REF',
regexpFilterExpression:
'^iy88/jenkins_test_frontend:refs/heads/main$'
)
}

options {
timestamps()
disableConcurrentBuilds()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(
numToKeepStr: '20',
artifactNumToKeepStr: '10'
))
}

environment {
DEPLOY_HOST = '替换为目标服务器地址'
DEPLOY_PORT = '22'
DEPLOY_USER = 'deploy'
DEPLOY_ROOT = '/srv/jenkins-test'
}

stages {
stage('Checkout') {
steps {
deleteDir()

git branch: 'main',
credentialsId: 'gitea-http',
url: 'https://git.example.com/example/jenkins_test_frontend.git'

script {
String requestedCommit = env.GITEA_COMMIT?.trim()

if (requestedCommit ==~ /[0-9a-fA-F]{40}/) {
sh "git checkout --detach ${requestedCommit}"
}

env.DEPLOY_REVISION = sh(
script: 'git rev-parse HEAD',
returnStdout: true
).trim()

env.DEPLOY_RELEASE =
"${env.DEPLOY_REVISION}-${env.BUILD_NUMBER}"
}
}
}

stage('Build') {
steps {
sh '''
set -eux

test -f package.json
test -f pnpm-lock.yaml

node --version
pnpm --version

pnpm install --frozen-lockfile
pnpm build

test -f dist/index.html
'''
}
}

stage('Package') {
steps {
sh '''
set -eux

ARCHIVE="frontend-${DEPLOY_RELEASE}.tar.gz"

tar -C dist -czf "${ARCHIVE}" .

test -s "${ARCHIVE}"
'''

archiveArtifacts(
artifacts: 'frontend-*.tar.gz',
fingerprint: true
)
}
}

stage('Deploy') {
steps {
sshagent(
credentials: ['production-deploy-ssh'],
ignoreMissing: false
) {
sh '''
set -eux

ARCHIVE="frontend-${DEPLOY_RELEASE}.tar.gz"

scp \
-P "${DEPLOY_PORT}" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
"${ARCHIVE}" \
"${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/${ARCHIVE}"

ssh \
-p "${DEPLOY_PORT}" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
"${DEPLOY_USER}@${DEPLOY_HOST}" \
bash -s -- \
"${DEPLOY_ROOT}" \
"${DEPLOY_RELEASE}" \
"${ARCHIVE}" <<'REMOTE'
set -eux

DEPLOY_ROOT="$1"
DEPLOY_RELEASE="$2"
ARCHIVE="$3"

RELEASE_DIR="${DEPLOY_ROOT}/releases/frontend-${DEPLOY_RELEASE}"
NEXT_LINK="${DEPLOY_ROOT}/.frontend-current-${DEPLOY_RELEASE}"
CURRENT_LINK="${DEPLOY_ROOT}/frontend-current"

mkdir -p "${RELEASE_DIR}"
tar -xzf "/tmp/${ARCHIVE}" -C "${RELEASE_DIR}"
rm -f "/tmp/${ARCHIVE}"

test -f "${RELEASE_DIR}/index.html"

ln -s "${RELEASE_DIR}" "${NEXT_LINK}"
mv -Tf "${NEXT_LINK}" "${CURRENT_LINK}"

echo "Frontend deployed to ${RELEASE_DIR}"
REMOTE
'''
}
}
}
}

post {
success {
echo "Frontend deployed successfully: ${env.DEPLOY_REVISION}"
}

failure {
echo 'Frontend build or deployment failed.'
}

always {
echo "Build result: ${currentBuild.currentResult}"
}
}
}
backend-pipeline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
pipeline {
agent any

triggers {
GenericTrigger(
genericVariables: [
[key: 'GITEA_REF', value: '$.ref'],
[key: 'GITEA_COMMIT', value: '$.after'],
[key: 'GITEA_REPOSITORY', value: '$.repository.full_name'],
[key: 'GITEA_PUSHER', value: '$.pusher.username']
],

causeString: 'Backend push: $GITEA_COMMIT by $GITEA_PUSHER',

tokenCredentialId: 'gitea-webhook-token',

printContributedVariables: false,
printPostContent: false,
silentResponse: false,
shouldNotFlatten: false,

regexpFilterText: '$GITEA_REPOSITORY:$GITEA_REF',
regexpFilterExpression:
'^iy88/jenkins_test_backend:refs/heads/main$'
)
}

options {
timestamps()
disableConcurrentBuilds()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(
numToKeepStr: '20',
artifactNumToKeepStr: '10'
))
}

environment {
UV_PYTHON = '3.13'
UV_CACHE_DIR = "${WORKSPACE}/.cache/uv"

DEPLOY_HOST = '替换为目标服务器地址'
DEPLOY_PORT = '22'
DEPLOY_USER = 'deploy'
DEPLOY_ROOT = '/srv/jenkins-test'

BACKEND_SERVICE = 'jenkins-test-backend'
BACKEND_HEALTH_URL = 'http://127.0.0.1:8000/api/hello'
}

stages {
stage('Checkout') {
steps {
deleteDir()

git branch: 'main',
credentialsId: 'gitea-http',
url: 'https://git.example.com/example/jenkins_test_backend.git'

script {
String requestedCommit = env.GITEA_COMMIT?.trim()

if (requestedCommit ==~ /[0-9a-fA-F]{40}/) {
sh "git checkout --detach ${requestedCommit}"
}

env.DEPLOY_REVISION = sh(
script: 'git rev-parse HEAD',
returnStdout: true
).trim()

env.DEPLOY_RELEASE =
"${env.DEPLOY_REVISION}-${env.BUILD_NUMBER}"
}
}
}

stage('Check Backend') {
steps {
sh '''
set -eux

test -f pyproject.toml
test -f uv.lock
test -f main.py

uv --version
uv python find 3.13

uv sync --locked

uv run --locked python --version
uv run --locked python -m compileall -q main.py
uv run --locked python -c \
"from main import app; print(app.title)"
'''
}
}

stage('Package') {
steps {
sh '''
set -eux

ARCHIVE="backend-${DEPLOY_RELEASE}.tar.gz"

git ls-files -z |
tar --null -T - -czf "${ARCHIVE}"

test -s "${ARCHIVE}"
'''

archiveArtifacts(
artifacts: 'backend-*.tar.gz',
fingerprint: true
)
}
}

stage('Deploy') {
steps {
sshagent(
credentials: ['production-deploy-ssh'],
ignoreMissing: false
) {
sh '''
set -eux

ARCHIVE="backend-${DEPLOY_RELEASE}.tar.gz"

scp \
-P "${DEPLOY_PORT}" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
"${ARCHIVE}" \
"${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/${ARCHIVE}"

ssh \
-p "${DEPLOY_PORT}" \
-o BatchMode=yes \
-o StrictHostKeyChecking=yes \
"${DEPLOY_USER}@${DEPLOY_HOST}" \
bash -s -- \
"${DEPLOY_ROOT}" \
"${DEPLOY_RELEASE}" \
"${ARCHIVE}" \
"${BACKEND_SERVICE}" \
"${BACKEND_HEALTH_URL}" <<'REMOTE'
set -u

DEPLOY_ROOT="$1"
DEPLOY_RELEASE="$2"
ARCHIVE="$3"
BACKEND_SERVICE="$4"
HEALTH_URL="$5"

RELEASE_DIR="${DEPLOY_ROOT}/releases/backend-${DEPLOY_RELEASE}"
CURRENT_LINK="${DEPLOY_ROOT}/backend-current"
NEXT_LINK="${DEPLOY_ROOT}/.backend-current-${DEPLOY_RELEASE}"

PREVIOUS_RELEASE=""
if [ -L "${CURRENT_LINK}" ]; then
PREVIOUS_RELEASE="$(readlink -f "${CURRENT_LINK}")"
fi

set -e

mkdir -p "${RELEASE_DIR}"
tar -xzf "/tmp/${ARCHIVE}" -C "${RELEASE_DIR}"
rm -f "/tmp/${ARCHIVE}"

cd "${RELEASE_DIR}"

/usr/local/bin/uv sync \
--locked \
--no-dev \
--python 3.13

test -x "${RELEASE_DIR}/.venv/bin/uvicorn"

ln -s "${RELEASE_DIR}" "${NEXT_LINK}"
mv -Tf "${NEXT_LINK}" "${CURRENT_LINK}"

set +e

sudo systemctl restart "${BACKEND_SERVICE}"
RESTART_RESULT=$?

HEALTHY=0

if [ "${RESTART_RESULT}" -eq 0 ]; then
for ATTEMPT in $(seq 1 20); do
if curl -fsS "${HEALTH_URL}" >/dev/null; then
HEALTHY=1
break
fi

sleep 1
done
fi

if [ "${HEALTHY}" -ne 1 ]; then
echo "Backend health check failed."

if [ -n "${PREVIOUS_RELEASE}" ] &&
[ -d "${PREVIOUS_RELEASE}" ]; then
ROLLBACK_LINK="${DEPLOY_ROOT}/.backend-rollback-${DEPLOY_RELEASE}"

ln -s "${PREVIOUS_RELEASE}" "${ROLLBACK_LINK}"
mv -Tf "${ROLLBACK_LINK}" "${CURRENT_LINK}"

sudo systemctl restart "${BACKEND_SERVICE}"

echo "Rolled back to ${PREVIOUS_RELEASE}"
fi

exit 1
fi

echo "Backend deployed to ${RELEASE_DIR}"
REMOTE
'''
}
}
}
}

post {
success {
echo "Backend deployed successfully: ${env.DEPLOY_REVISION}"
}

failure {
echo 'Backend build or deployment failed.'
}

always {
echo "Build result: ${currentBuild.currentResult}"
}
}
}

随后可以先 build backend 再 build frontend,并观察配置结果,我这儿是没问题的。

jenkins-panel