fix: Dockerfile 构建上下文修复 + 部署脚本 SSH 别名
This commit is contained in:
parent
e63601b737
commit
2682232d85
|
|
@ -6,8 +6,8 @@ WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci --production=false
|
RUN npm ci --production=false
|
||||||
|
|
||||||
# 2. 复制共享库
|
# 2. 复制共享库(构建前由部署脚本创建 symlink: shared -> ../shared)
|
||||||
COPY ../shared/ ./shared/
|
COPY shared/ ./shared/
|
||||||
|
|
||||||
# 3. 复制源码并构建
|
# 3. 复制源码并构建
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
# monitor-ai 部署脚本
|
# monitor-ai 部署脚本
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# bash deploy-monitor.sh # 本地模式(默认)
|
# bash deploy-monitor.sh # 部署到 txjp 服务器(默认)
|
||||||
# bash deploy-monitor.sh --dev # 开发模式(本地构建)
|
# bash deploy-monitor.sh --dev # 开发模式(本地构建)
|
||||||
# bash deploy-monitor.sh --remote --host <IP> # 远程部署到指定服务器
|
|
||||||
#
|
#
|
||||||
# 首次部署自动:
|
# 首次部署自动:
|
||||||
# 1. 生成 OIDC_CLIENT_SECRET (openssl rand -hex 32)
|
# 1. 生成 OIDC_CLIENT_SECRET (openssl rand -hex 32)
|
||||||
|
|
@ -42,20 +41,9 @@ fi
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 解析参数
|
# 解析参数
|
||||||
# ============================================================
|
# ============================================================
|
||||||
MODE="local"
|
MODE="remote"
|
||||||
HOST=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--remote)
|
|
||||||
MODE="remote"
|
|
||||||
info "模式: 远程部署"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--host)
|
|
||||||
HOST="$2"
|
|
||||||
info "目标主机: $HOST"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--dev)
|
--dev)
|
||||||
MODE="dev"
|
MODE="dev"
|
||||||
info "模式: 开发构建"
|
info "模式: 开发构建"
|
||||||
|
|
@ -69,7 +57,7 @@ while [[ $# -gt 0 ]]; do
|
||||||
done
|
done
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 路径配置
|
# 路径配置(对齐 deploy-ai.sh,使用 SSH 别名 txjp)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
LOCAL_DIR="/Users/niuniu/programs/docker"
|
LOCAL_DIR="/Users/niuniu/programs/docker"
|
||||||
MONITOR_DIR="$LOCAL_DIR/monitor-ai"
|
MONITOR_DIR="$LOCAL_DIR/monitor-ai"
|
||||||
|
|
@ -79,13 +67,6 @@ CONTAINER="monitor-ai"
|
||||||
SSH_TARGET="txjp"
|
SSH_TARGET="txjp"
|
||||||
SNAPSHOT_FILE="/tmp/.snapshot.monitor.md5"
|
SNAPSHOT_FILE="/tmp/.snapshot.monitor.md5"
|
||||||
|
|
||||||
# 远程模式需要 host 参数
|
|
||||||
if [[ "$MODE" == "remote" && -z "$HOST" ]]; then
|
|
||||||
error "远程部署需要指定 --host <IP>"
|
|
||||||
echo " 用法: bash deploy-monitor.sh --remote --host 43.133.38.210"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 打包源码
|
# 打包源码
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -116,28 +97,10 @@ package_source() {
|
||||||
# 首次部署:生成密钥和配置
|
# 首次部署:生成密钥和配置
|
||||||
# ============================================================
|
# ============================================================
|
||||||
first_deploy_setup() {
|
first_deploy_setup() {
|
||||||
local target="$1" # SSH target or "local"
|
|
||||||
local ssh_cmd=""
|
|
||||||
if [[ "$target" != "local" ]]; then
|
|
||||||
ssh_cmd="ssh $target"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local run
|
|
||||||
if [[ -n "$ssh_cmd" ]]; then
|
|
||||||
run="$ssh_cmd"
|
|
||||||
else
|
|
||||||
run=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "首次部署检查..."
|
info "首次部署检查..."
|
||||||
|
|
||||||
# 检查 .env 是否存在
|
|
||||||
local env_exists
|
local env_exists
|
||||||
if [[ -n "$run" ]]; then
|
env_exists=$(ssh txjp "test -f $REMOTE_DIR/.env && echo yes || echo no" 2>/dev/null)
|
||||||
env_exists=$($run "test -f $REMOTE_DIR/.env && echo yes || echo no" 2>/dev/null)
|
|
||||||
else
|
|
||||||
env_exists=$(test -f "$MONITOR_DIR/.env" && echo yes || echo no)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$env_exists" == "yes" ]]; then
|
if [[ "$env_exists" == "yes" ]]; then
|
||||||
info "已存在 .env,跳过密钥生成"
|
info "已存在 .env,跳过密钥生成"
|
||||||
|
|
@ -151,10 +114,8 @@ first_deploy_setup() {
|
||||||
LOCALADMIN_PASS=$(openssl rand -hex 16)
|
LOCALADMIN_PASS=$(openssl rand -hex 16)
|
||||||
JWT_SECRET=$(openssl rand -hex 32)
|
JWT_SECRET=$(openssl rand -hex 32)
|
||||||
|
|
||||||
# 生成 pbkdf2 密码哈希(通过 Authelia 容器或本地 openssl)
|
# 生成 pbkdf2 密码哈希(通过 Authelia 容器)
|
||||||
if [[ -n "$run" ]]; then
|
PBKDF2_HASH=$(ssh txjp "docker exec authelia authelia crypto hash generate pbkdf2 --password '$LOCALADMIN_PASS' 2>/dev/null | grep 'Digest:' | awk '{print \$2}'" 2>/dev/null)
|
||||||
PBKDF2_HASH=$($run "docker exec authelia authelia crypto hash generate pbkdf2 --password '$LOCALADMIN_PASS' 2>/dev/null | grep 'Digest:' | awk '{print \$2}'" 2>/dev/null)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 如果 Authelia 不可用,使用 openssl 生成备用哈希
|
# 如果 Authelia 不可用,使用 openssl 生成备用哈希
|
||||||
if [[ -z "$PBKDF2_HASH" ]]; then
|
if [[ -z "$PBKDF2_HASH" ]]; then
|
||||||
|
|
@ -164,8 +125,8 @@ first_deploy_setup() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 写入 .env 文件
|
# 写入 .env 文件
|
||||||
local env_content
|
ssh txjp "cat > $REMOTE_DIR/.env << 'ENVEOF'
|
||||||
env_content="DATABASE_PATH=/app/data/monitor.db
|
DATABASE_PATH=/app/data/monitor.db
|
||||||
MONITOR_MODE=local
|
MONITOR_MODE=local
|
||||||
AUTHELIA_URL=https://sso.tlyq.ai
|
AUTHELIA_URL=https://sso.tlyq.ai
|
||||||
OIDC_CLIENT_ID=monitor-oidc
|
OIDC_CLIENT_ID=monitor-oidc
|
||||||
|
|
@ -176,15 +137,8 @@ COOKIE_DOMAIN=.tlyq.ai
|
||||||
LDAP_URL=ldap://ldap-ai:3890
|
LDAP_URL=ldap://ldap-ai:3890
|
||||||
LOCALADMIN_PASSWORD=$LOCALADMIN_PASS
|
LOCALADMIN_PASSWORD=$LOCALADMIN_PASS
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED=0"
|
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
|
||||||
if [[ -n "$run" ]]; then
|
|
||||||
$run "cat > $REMOTE_DIR/.env << 'ENVEOF'
|
|
||||||
$env_content
|
|
||||||
ENVEOF"
|
ENVEOF"
|
||||||
else
|
|
||||||
echo "$env_content" > "$MONITOR_DIR/.env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "密钥已生成"
|
log "密钥已生成"
|
||||||
info " OIDC_CLIENT_SECRET: $OIDC_SECRET"
|
info " OIDC_CLIENT_SECRET: $OIDC_SECRET"
|
||||||
|
|
@ -196,7 +150,6 @@ ENVEOF"
|
||||||
# 更新 nginx 配置
|
# 更新 nginx 配置
|
||||||
# ============================================================
|
# ============================================================
|
||||||
update_nginx() {
|
update_nginx() {
|
||||||
local target="$1"
|
|
||||||
local nginx_conf="$LOCAL_DIR/nginx-proxy-ai/conf.d/monitor-ai.conf"
|
local nginx_conf="$LOCAL_DIR/nginx-proxy-ai/conf.d/monitor-ai.conf"
|
||||||
|
|
||||||
if [[ ! -f "$nginx_conf" ]]; then
|
if [[ ! -f "$nginx_conf" ]]; then
|
||||||
|
|
@ -204,21 +157,15 @@ update_nginx() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$target" == "local" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "更新 nginx 配置..."
|
log "更新 nginx 配置..."
|
||||||
scp "$nginx_conf" "$target:$REMOTE_DIR/../nginx-proxy-ai/conf.d/monitor-ai.conf" 2>/dev/null || true
|
scp "$nginx_conf" txjp:/root/docker/nginx-proxy-ai/conf.d/monitor-ai.conf 2>/dev/null || true
|
||||||
ssh "$target" "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" && log "nginx 已重载" || warn "nginx 重载失败"
|
ssh txjp "docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" && log "nginx 已重载" || warn "nginx 重载失败"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 服务器构建
|
# 服务器构建
|
||||||
# ============================================================
|
# ============================================================
|
||||||
build_on_server() {
|
build_on_server() {
|
||||||
local target="$1"
|
|
||||||
|
|
||||||
# 1. 计算源码快照
|
# 1. 计算源码快照
|
||||||
log "检查源码是否有变化..."
|
log "检查源码是否有变化..."
|
||||||
local local_md5
|
local local_md5
|
||||||
|
|
@ -238,13 +185,13 @@ build_on_server() {
|
||||||
echo " 源码快照: ${local_md5}"
|
echo " 源码快照: ${local_md5}"
|
||||||
|
|
||||||
local prev_md5
|
local prev_md5
|
||||||
prev_md5=$(ssh "$target" "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "")
|
prev_md5=$(ssh txjp "cat ${SNAPSHOT_FILE} 2>/dev/null" 2>/dev/null || echo "")
|
||||||
|
|
||||||
if [[ -z "$prev_md5" ]]; then
|
if [[ -z "$prev_md5" ]]; then
|
||||||
info "首次部署,执行完整构建"
|
info "首次部署,执行完整构建"
|
||||||
elif [[ "$prev_md5" == "$local_md5" ]]; then
|
elif [[ "$prev_md5" == "$local_md5" ]]; then
|
||||||
info "源码无变化,跳过构建,仅重启容器"
|
info "源码无变化,跳过构建,仅重启容器"
|
||||||
ssh "$target" "cd $REMOTE_DIR && docker compose down && docker compose up -d"
|
ssh txjp "cd $REMOTE_DIR && docker compose down && docker compose up -d"
|
||||||
log "容器已重建"
|
log "容器已重建"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
|
|
@ -256,14 +203,14 @@ build_on_server() {
|
||||||
|
|
||||||
# 3. 上传
|
# 3. 上传
|
||||||
log "上传源码包..."
|
log "上传源码包..."
|
||||||
scp /tmp/monitor-ai.tar.gz "$target:/tmp/monitor-ai.tar.gz" || {
|
scp /tmp/monitor-ai.tar.gz txjp:/tmp/monitor-ai.tar.gz || {
|
||||||
error "上传失败"
|
error "上传失败"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 4. 解压源码到 /tmp,然后 rsync 到目标目录
|
# 4. 解压源码到 /tmp,然后 rsync 到目标目录
|
||||||
log "解压源码并准备依赖..."
|
log "解压源码并准备依赖..."
|
||||||
ssh "$target" "\
|
ssh txjp "\
|
||||||
rm -rf /tmp/deploy_monitor && mkdir -p /tmp/deploy_monitor && \
|
rm -rf /tmp/deploy_monitor && mkdir -p /tmp/deploy_monitor && \
|
||||||
tar xzf /tmp/monitor-ai.tar.gz -C /tmp/deploy_monitor 2>/dev/null || true && \
|
tar xzf /tmp/monitor-ai.tar.gz -C /tmp/deploy_monitor 2>/dev/null || true && \
|
||||||
rsync -a --delete \
|
rsync -a --delete \
|
||||||
|
|
@ -274,13 +221,14 @@ build_on_server() {
|
||||||
rsync -a --delete \
|
rsync -a --delete \
|
||||||
--exclude='node_modules' --exclude='.git' \
|
--exclude='node_modules' --exclude='.git' \
|
||||||
/tmp/deploy_monitor/shared/ /root/docker/shared/ && \
|
/tmp/deploy_monitor/shared/ /root/docker/shared/ && \
|
||||||
|
ln -sf /root/docker/shared $REMOTE_DIR/shared && \
|
||||||
rm -rf /tmp/deploy_monitor"
|
rm -rf /tmp/deploy_monitor"
|
||||||
|
|
||||||
# 5. 首次部署:上传 docker-compose.yml 和 Dockerfile
|
# 5. 首次部署:上传 docker-compose.yml 和 Dockerfile
|
||||||
ssh "$target" "cd $REMOTE_DIR && docker compose up -d"
|
ssh txjp "cd $REMOTE_DIR && docker compose up -d"
|
||||||
|
|
||||||
# 如果 host 上没有 node_modules,从容器内复制一份
|
# 如果 host 上没有 node_modules,从容器内复制一份
|
||||||
ssh "$target" "if [ ! -d '${REMOTE_DIR}/node_modules' ]; then
|
ssh txjp "if [ ! -d '${REMOTE_DIR}/node_modules' ]; then
|
||||||
echo ' 首次:复制容器内 node_modules 到主机...'
|
echo ' 首次:复制容器内 node_modules 到主机...'
|
||||||
docker cp ${CONTAINER}:/app/node_modules ${REMOTE_DIR}/node_modules
|
docker cp ${CONTAINER}:/app/node_modules ${REMOTE_DIR}/node_modules
|
||||||
echo ' 完成'
|
echo ' 完成'
|
||||||
|
|
@ -288,16 +236,16 @@ build_on_server() {
|
||||||
|
|
||||||
# 6. 安装可能新增的依赖
|
# 6. 安装可能新增的依赖
|
||||||
log "安装新增依赖..."
|
log "安装新增依赖..."
|
||||||
ssh "$target" "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5 || true"
|
ssh txjp "cd ${REMOTE_DIR} && npm install --prefer-offline 2>&1 | tail -5 || true"
|
||||||
|
|
||||||
# 7. 清理可能被上传的本地环境变量文件
|
# 7. 清理可能被上传的本地环境变量文件
|
||||||
ssh "$target" "rm -f $REMOTE_DIR/.env.local $REMOTE_DIR/.env.development $REMOTE_DIR/.env.production 2>/dev/null; echo '已清理本地环境文件'"
|
ssh txjp "rm -f $REMOTE_DIR/.env.local $REMOTE_DIR/.env.development $REMOTE_DIR/.env.production 2>/dev/null; echo '已清理本地环境文件'"
|
||||||
|
|
||||||
# 8. 服务器上执行 npm run build
|
# 8. 服务器上执行 npm run build
|
||||||
log "服务器上执行 npm run build..."
|
log "服务器上执行 npm run build..."
|
||||||
local build_start build_end build_dur
|
local build_start build_end build_dur
|
||||||
build_start=$(date +%s)
|
build_start=$(date +%s)
|
||||||
ssh "$target" "cd ${REMOTE_DIR} && npm run build 2>&1" | \
|
ssh txjp "cd ${REMOTE_DIR} && npm run build 2>&1" | \
|
||||||
grep -vE "^(info|warn|npm warn|audited|packages|funding|vulnerability|npm notice|New major)" | \
|
grep -vE "^(info|warn|npm warn|audited|packages|funding|vulnerability|npm notice|New major)" | \
|
||||||
tail -15
|
tail -15
|
||||||
local build_exit_code=${PIPESTATUS[0]}
|
local build_exit_code=${PIPESTATUS[0]}
|
||||||
|
|
@ -311,30 +259,30 @@ build_on_server() {
|
||||||
|
|
||||||
# 9. 重建容器
|
# 9. 重建容器
|
||||||
log "重建容器..."
|
log "重建容器..."
|
||||||
ssh "$target" "cd $REMOTE_DIR && docker compose up -d && docker compose restart"
|
ssh txjp "cd $REMOTE_DIR && docker compose up -d && docker compose restart"
|
||||||
|
|
||||||
# 10. 清理构建缓存
|
# 10. 清理构建缓存
|
||||||
log "清理构建缓存..."
|
log "清理构建缓存..."
|
||||||
ssh "$target" "docker image prune -f 2>/dev/null || true"
|
ssh txjp "docker image prune -f 2>/dev/null || true"
|
||||||
|
|
||||||
local disk_usage
|
local disk_usage
|
||||||
disk_usage=$(ssh "$target" "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50")
|
disk_usage=$(ssh txjp "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50")
|
||||||
if [ "$disk_usage" -gt 95 ]; then
|
if [ "$disk_usage" -gt 95 ]; then
|
||||||
warn "磁盘使用率 ${disk_usage}%,执行紧急清理"
|
warn "磁盘使用率 ${disk_usage}%,执行紧急清理"
|
||||||
ssh "$target" "docker builder prune -f 2>/dev/null || true"
|
ssh txjp "docker builder prune -f 2>/dev/null || true"
|
||||||
elif [ "$disk_usage" -gt 85 ]; then
|
elif [ "$disk_usage" -gt 85 ]; then
|
||||||
info "磁盘使用率 ${disk_usage}%,执行激进清理(6小时)"
|
info "磁盘使用率 ${disk_usage}%,执行激进清理(6小时)"
|
||||||
ssh "$target" "docker builder prune -f --filter 'until=6h' 2>/dev/null || true"
|
ssh txjp "docker builder prune -f --filter 'until=6h' 2>/dev/null || true"
|
||||||
elif [ "$disk_usage" -gt 70 ]; then
|
elif [ "$disk_usage" -gt 70 ]; then
|
||||||
info "磁盘使用率 ${disk_usage}%,执行正常清理(24小时)"
|
info "磁盘使用率 ${disk_usage}%,执行正常清理(24小时)"
|
||||||
ssh "$target" "docker builder prune -f --filter 'until=24h' 2>/dev/null || true"
|
ssh txjp "docker builder prune -f --filter 'until=24h' 2>/dev/null || true"
|
||||||
else
|
else
|
||||||
info "磁盘使用率 ${disk_usage}%,执行保守清理(48小时)"
|
info "磁盘使用率 ${disk_usage}%,执行保守清理(48小时)"
|
||||||
ssh "$target" "docker builder prune -f --filter 'until=48h' 2>/dev/null || true"
|
ssh txjp "docker builder prune -f --filter 'until=48h' 2>/dev/null || true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 11. 保存快照
|
# 11. 保存快照
|
||||||
ssh "$target" "echo '$local_md5' > ${SNAPSHOT_FILE}"
|
ssh txjp "echo '$local_md5' > ${SNAPSHOT_FILE}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
@ -372,34 +320,19 @@ echo ""
|
||||||
BUILD_START=$(date +%s)
|
BUILD_START=$(date +%s)
|
||||||
|
|
||||||
case "$MODE" in
|
case "$MODE" in
|
||||||
local)
|
remote)
|
||||||
# 本地模式:部署到 txjp 服务器(默认行为)
|
# 远程模式:部署到 txjp 服务器
|
||||||
log "部署目标: $MONITOR_DIR → txjp:$REMOTE_DIR"
|
log "部署目标: $MONITOR_DIR → txjp:$REMOTE_DIR"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# 首次部署检查
|
# 首次部署检查
|
||||||
first_deploy_setup "$SSH_TARGET"
|
first_deploy_setup
|
||||||
|
|
||||||
# 构建
|
# 构建
|
||||||
build_on_server "$SSH_TARGET"
|
build_on_server
|
||||||
|
|
||||||
# 更新 nginx
|
# 更新 nginx
|
||||||
update_nginx "$SSH_TARGET"
|
update_nginx
|
||||||
;;
|
|
||||||
|
|
||||||
remote)
|
|
||||||
# 远程模式:部署到指定服务器
|
|
||||||
log "部署目标: $MONITOR_DIR → $HOST:$REMOTE_DIR"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 首次部署检查
|
|
||||||
first_deploy_setup "$HOST"
|
|
||||||
|
|
||||||
# 构建
|
|
||||||
build_on_server "$HOST"
|
|
||||||
|
|
||||||
# 更新 nginx
|
|
||||||
update_nginx "$HOST"
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
dev)
|
dev)
|
||||||
|
|
@ -421,7 +354,7 @@ if [[ "$MODE" == "dev" ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
STATUS=$(ssh "$SSH_TARGET" "curl -s -o /dev/null -w '%{http_code}' -k 'https://monitor.tlyq.ai/api/health' 2>/dev/null" 2>/dev/null || echo "???")
|
STATUS=$(ssh txjp "curl -s -o /dev/null -w '%{http_code}' -k 'https://monitor.tlyq.ai/api/health' 2>/dev/null" 2>/dev/null || echo "???")
|
||||||
if [[ "$STATUS" == "200" ]]; then
|
if [[ "$STATUS" == "200" ]]; then
|
||||||
log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 https://monitor.tlyq.ai"
|
log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 https://monitor.tlyq.ai"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue