add git-based chatapp testbox deploy

This commit is contained in:
z 2026-05-05 19:51:04 +08:00
parent 78d27a45b3
commit 10174681a5
22 changed files with 1425 additions and 0 deletions

243
testbox/.env.example Normal file
View File

@ -0,0 +1,243 @@
# Host and public entry
TESTBOX_DOMAIN=jvapi-test.haiyihy.com
TESTBOX_PUBLIC_BASE_URL=https://jvapi-test.haiyihy.com
TESTBOX_ADMIN_DOMAIN=yumi-test-admi.haiyihy.com
TESTBOX_ADMIN_PUBLIC_BASE_URL=https://yumi-test-admi.haiyihy.com
TESTBOX_ADMIN_API_BASE_URL=https://yumi-test-admi.haiyihy.com/console
TESTBOX_ADMIN_OSS_URL=
# Infra credentials. Change these before first production-like test.
MYSQL_DATABASE=likei
MYSQL_USER=likei
MYSQL_PASSWORD=change-me-likei
MYSQL_ROOT_PASSWORD=change-me-root
REDIS_PASSWORD=
NACOS_USERNAME=nacos
NACOS_PASSWORD=nacos
NACOS_NAMESPACE_ID=RedCircleServiceTest
NACOS_NAMESPACE_NAME=RedCircleServiceTest
# Java runtime. Jar is built with Maven profile "test" for Nacos namespace,
# but runtime profile stays prod so existing profile groups still load.
SERVER_PROFILE_ACTIVE=prod
JAVA_XMS=128m
JAVA_XMX_SMALL=384m
JAVA_XMX_MEDIUM=512m
JAVA_METASPACE=192m
# Nacos-resolved placeholders used by chatapp3-java/nacos_config.
LIKEI_MYSQL_JDBC_URL=jdbc:p6spy:mysql://mysql:3306/likei?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
LIKEI_MYSQL_USERNAME=likei
LIKEI_MYSQL_PASSWORD=change-me-likei
LIKEI_REDIS_HOST=redis
LIKEI_REDIS_PORT=6379
LIKEI_REDIS_DATABASE=0
LIKEI_MONGO_URI=mongodb://mongo:27017/tarab_all
# RocketMQ is deployed locally. Keep Java producer disabled first so service
# startup is stable; flip to false when you want to test MQ paths.
LIKEI_ROCKETMQ_ACCESS_KEY=test
LIKEI_ROCKETMQ_SECRET_KEY=test
LIKEI_ROCKETMQ_ENDPOINT=rocketmq-proxy:8081
LIKEI_ROCKETMQ_LOCAL_DISABLE=true
# Payment provider placeholders used by rc-service-order. These test defaults
# only keep the service bootable; replace them when testing real payment flows.
LIKEI_PAY_WEB_PAY_BASE_URL=https://jvapi-test.haiyihy.com
LIKEI_PAY_WEB_PAY_RESULT_PAGE=/pay/result
LIKEI_PAY_WEB_PAY_RECHARGE_PAGE=/pay/recharge
LIKEI_PAY_WEB_PAY_COLLECTION_PAGE=/pay/collection
LIKEI_GOOGLE_PAY_GLOBAL_SERVER_ACCOUNT=test
LIKEI_GOOGLE_PAY_GLOBAL_PATCH_JSON=payfile/likei_google_pay.json
LIKEI_GOOGLE_PAY_TAXAB_SERVER_ACCOUNT=test
LIKEI_GOOGLE_PAY_TAXAB_PUBLIC_KEY=test
LIKEI_GOOGLE_PAY_TAXAB_PATCH_JSON=payfile/likei_google_pay.json
LIKEI_PAYPAL_MERCHANT_ID=test
LIKEI_PAYPAL_SECRET_KEY=test
LIKEI_PAYPAL_DEV_MERCHANT_ID=test
LIKEI_PAYPAL_DEV_SECRET_KEY=test
LIKEI_HUAWEI_GRANT_TYPE=client_credentials
LIKEI_HUAWEI_CLIENT_SECRET=test
LIKEI_HUAWEI_CLIENT_ID=test
LIKEI_HUAWEI_OAUTH_LOGIN_URL=https://oauth-login.cloud.huawei.com/oauth2/v3/token
LIKEI_HUAWEI_IAP_PUBLIC_KEY=test
LIKEI_PAYERMAX_SECRET_KEY=test
LIKEI_PAYERMAX_MERCHANT_ID=test
LIKEI_PAYERMAX_RSA_PRIVATE_KEY=test
LIKEI_PAYERMAX_RSA_PUBLIC_KEY=test
LIKEI_PAYERMAX_PLATFORM_RSA_PUBLIC_KEY=test
LIKEI_PAYERMAX_APP_ID=test
LIKEI_PAYERMAX_GATEWAY_BASE_URL=https://pay-gate-uat.payermax.com
LIKEI_PAYERMAX_ENABLE_REAL_TIME_EXCHANGE_RATE=false
LIKEI_AIRWALLEX_CLIENT_ID=test
LIKEI_AIRWALLEX_API_KEY=test
LIKEI_AIRWALLEX_BASE_URL=https://api-demo.airwallex.com
LIKEI_PAYNICORN_APP_KEY=test
LIKEI_PAYNICORN_MERCHANT_SECRET=test
LIKEI_CLIPSPAY_MERCHANT_NO=test
LIKEI_CLIPSPAY_MD5_SECRET_KEY=test
LIKEI_CLIPSPAY_TERMINAL_NO=test
LIKEI_CLIPSPAY_TERMINAL_KEY=test
LIKEI_CLIPSPAY_NOTIFY_URL=https://jvapi-test.haiyihy.com/play-server-notice/clipspay
LIKEI_CLIPSPAY_PLACE_ORDER_API=https://example.invalid
LIKEI_CLIPSPAY_REDIRECT_RESULT=https://jvapi-test.haiyihy.com/pay/result
LIKEI_ORDER_TELEGRAM_BOT_TOKEN=test
LIKEI_ORDER_TELEGRAM_WEBHOOK_URL=https://jvapi-test.haiyihy.com
# Generic Java placeholder defaults for single-node smoke testing.
LIKEI_FRAMEWORK_NACOS_STOP_KEY=test
GAME_INTERNAL_CALLBACK_SECRET=test-game-callback-secret
GAME_LUCKY_GIFT_GO_URL=http://golang:2900
LUCKY_GIFT_GO_ENABLED=false
LUCKY_GIFT_GO_TIMEOUT_MILLIS=3000
LUCKY_GIFT_API_ENABLED=false
LUCKY_GIFT_API_DEFAULT_URL=https://example.invalid
LUCKY_GIFT_API_CONFIG_1_STANDARD_ID=1
LUCKY_GIFT_API_CONFIG_1_URL=https://example.invalid
LUCKY_GIFT_API_CONFIG_2_STANDARD_ID=2
LUCKY_GIFT_API_CONFIG_2_URL=https://example.invalid
INVITE_CAMPAIGN_GO_ENABLED=false
INVITE_CAMPAIGN_GO_TIMEOUT_MILLIS=3000
INVITE_CAMPAIGN_GO_URL=http://golang:2900
LIKEI_WALLET_MYSQL_JDBC_URL=jdbc:p6spy:mysql://mysql:3306/likei?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
LIKEI_PAY_FACTORY_CONFIG_CACHE_TTL_SECONDS=300
LIKEI_RTC_APP_ID=test
LIKEI_RTC_AUTH_KEY=test
LIKEI_RTC_CERTIFICATE=test
LIKEI_RTC_KILL_ROOM_URL=https://example.invalid
LIKEI_AGORA_APP_CERTIFICATE=test
LIKEI_AGORA_AUTH_KEY=test
LIKEI_AGORA_TOKEN_EXPIRE_SECOND=3600
LIKEI_AGORA_PRIVILEGE_EXPIRE_SECOND=3600
LIKEI_IM_APP_ID=1400000000
LIKEI_IM_KEY=test
LIKEI_IM_IDENTIFIER=administrator
LIKEI_IM_BASE_ENDPOINT=https://console.tim.qq.com
LIKEI_IM_NOTICE_ACCOUNT=notice
LIKEI_IM_NEWSLETTER_ACCOUNT=newsletter
LIKEI_IM_BROADCAST_GROUP_LOTFUN=test-lotfun
LIKEI_IM_BROADCAST_GROUP_LIKEI=test-likei
LIKEI_TRTC_SECRET_ID=test
LIKEI_TRTC_SECRET_KEY=test
LIKEI_TRTC_ENDPOINT=https://trtc.tencentcloudapi.com
LIKEI_TRTC_SDK_APP_ID=1400000000
LIKEI_TENCENT_COS_ENABLED=false
LIKEI_TENCENT_COS_SECRET_ID=test
LIKEI_TENCENT_COS_SECRET_KEY=test
LIKEI_TENCENT_COS_BUCKET=test
LIKEI_TENCENT_COS_REGION=ap-jakarta
LIKEI_TENCENT_COS_ACCESS_URL=https://example.invalid
LIKEI_TENCENT_CLS_SECRET_ID=test
LIKEI_TENCENT_CLS_SECRET_KEY=test
LIKEI_TENCENT_CLS_ENDPOINT=https://cls.tencentcloudapi.com
LIKEI_TENCENT_CLS_REGION=ap-jakarta
LIKEI_TENCENT_CLOUD_IMS_SECRET_ID=test
LIKEI_TENCENT_CLOUD_IMS_SECRET_KEY=test
LIKEI_TENCENT_CLOUD_IMS_ENDPOINT=https://ims.tencentcloudapi.com
LIKEI_TENCENT_CLOUD_IMS_REGION=ap-jakarta
LIKEI_ALIYUN_OSS_ENDPOINT=oss-ap-southeast-1.aliyuncs.com
LIKEI_ALIYUN_OSS_BUCKET_NAME=test
LIKEI_ALIYUN_OSS_ACCESS_URL=https://example.invalid
LIKEI_ALIYUN_OSS_ACCESS_KEY_ID=test
LIKEI_ALIYUN_OSS_ACCESS_KEY_SECRET=test
LIKEI_ALIYUN_STS_ENDPOINT=sts.aliyuncs.com
LIKEI_ALIYUN_STS_ROLE_ARN=test
LIKEI_SMS_ALIYUN_ENABLED=false
LIKEI_SMS_ALIYUN_SIGN_NAME=test
LIKEI_SMS_ALIYUN_ACCESS_KEY_ID=test
LIKEI_SMS_ALIYUN_ACCESS_KEY_SECRET=test
LIKEI_SMS_ALIYUN_ENDPOINT=dysmsapi.aliyuncs.com
LIKEI_CENSOR_ACTIVE=qcloud
LIKEI_CENSOR_TUPU_ENABLED=false
LIKEI_AI_MINIMAX_URL=https://example.invalid
LIKEI_AI_MINIMAX_AUTHORIZATION=test
LIKEI_AI_MINIMAX_MODEL=test
LIKEI_AI_QIANFAN_URL=https://example.invalid
LIKEI_AI_QIANFAN_APP_ID=test
LIKEI_AI_QIANFAN_AUTHORIZATION=test
LIKEI_AI_QIANFAN_MODEL=test
LIKEI_AI_TTS_URL=https://example.invalid
LIKEI_AI_TTS_MODEL=test
LIKEI_TELEGRAM_BOT_TOKEN=test
LIKEI_TELEGRAM_MONITOR_CHAT_ID_1=0
LIKEI_TELEGRAM_MONITOR_ALERT_LIMIT_SECONDS=300
LIKEI_TELEGRAM_MONITOR_PROXY_HOST=
LIKEI_TELEGRAM_MONITOR_PROXY_PORT=0
LIKEI_VISUAL_MONITOR_USERNAME=admin
LIKEI_VISUAL_MONITOR_PASSWORD=admin
LIKEI_GAME_SUD_APP_ID=test
LIKEI_GAME_SUD_APP_KEY=test
LIKEI_GAME_SUD_APP_SECRET=test
LIKEI_GAME_SUD_APP_CHANNEL=test
LIKEI_GAME_BAISHUN_APP_ID=test
LIKEI_GAME_BAISHUN_APP_KEY=test
LIKEI_GAME_BAISHUN_APP_CHANNEL=test
LIKEI_GAME_BAISHUN2FUN_APP_ID=test
LIKEI_GAME_BAISHUN2FUN_APP_KEY=test
LIKEI_GAME_BAISHUN2FUN_APP_CHANNEL=test
LIKEI_GAME_HKYS_SIGN_KEY=test
LIKEI_GAME_HOTGAME_SIGN_KEY=test
LIKEI_YOMI_AES_KEY=test-yomi-aes-key
# Gateway route targets inside the compose network.
LIKEI_GATEWAY_ROUTE_AUTH_URI=http://auth:1000
LIKEI_GATEWAY_ROUTE_EXTERNAL_URI=http://external:3000
LIKEI_GATEWAY_ROUTE_ORDER_URI=http://order:2600
LIKEI_GATEWAY_ROUTE_WALLET_URI=http://wallet:2000
LIKEI_GATEWAY_ROUTE_LIVE_URI=http://live:2500
LIKEI_GATEWAY_ROUTE_OTHER_URI=http://other:2400
LIKEI_GATEWAY_ROUTE_GO_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_INVITE_PUBLIC_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_INVITE_APP_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_REGISTER_REWARD_APP_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_INVITE_INTERNAL_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_GAME_APP_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_GAME_INTERNAL_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_GAME_BAISHUN_URI=http://golang:2900
LIKEI_GATEWAY_ROUTE_FORWARD_URI=http://other:2400
LIKEI_GATEWAY_FORWARD_ROUTE_AES_KEY=test-forward-route-aes-key
LIKEI_GATEWAY_FORWARD_AES_KEY=test-forward-aes-key
# Go API / cron.
CHATAPP_HTTP_LISTEN_ADDR=:2900
CHATAPP_HTTP_INTERNAL_CALLBACK_SECRET=test-internal-callback-secret
CHATAPP_STORE_MYSQL_DSN=likei:change-me-likei@tcp(mysql:3306)/likei?charset=utf8mb4&parseTime=True&loc=Local
CHATAPP_STORE_REDIS_ADDR=redis:6379
CHATAPP_STORE_REDIS_PASSWORD=
CHATAPP_STORE_REDIS_DB=0
CHATAPP_JAVA_AUTH_BASE_URL=http://auth:1000
CHATAPP_JAVA_CONSOLE_BASE_URL=http://console:2700/console
CHATAPP_JAVA_DEVICE_BASE_URL=http://other:2400
CHATAPP_JAVA_APP_BASE_URL=http://other:2400
CHATAPP_JAVA_BRIDGE_BASE_URL=http://console:2700/console
CHATAPP_JAVA_EXTERNAL_BASE_URL=http://external:3000
CHATAPP_JAVA_OTHER_BASE_URL=http://other:2400
CHATAPP_JAVA_ORDER_BASE_URL=http://order:2600
CHATAPP_JAVA_WALLET_BASE_URL=http://wallet:2000
CHATAPP_INVITE_PUBLIC_BASE_URL=https://jvapi-test.haiyihy.com
CHATAPP_GAME_OPEN_PUBLIC_BASE_URL=https://jvapi-test.haiyihy.com
CHATAPP_WEEK_STAR_TIMEZONE=Asia/Riyadh
CHATAPP_ROOM_TURNOVER_REWARD_TIMEZONE=Asia/Riyadh
CHATAPP_WEEK_STAR_SETTLE_INTERVAL_SECONDS=60
CHATAPP_ROOM_TURNOVER_REWARD_SETTLE_INTERVAL_SECONDS=60
CHATAPP_WEEK_STAR_ROCKETMQ_ENABLED=false
CHATAPP_WEEK_STAR_ROCKETMQ_ENDPOINT=rocketmq-proxy:8081
CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_KEY=test
CHATAPP_WEEK_STAR_ROCKETMQ_ACCESS_SECRET=test
# Source locations on the testbox. sync-to-testbox.sh populates these.
TESTBOX_MONITOR_REPO_URL=git@gitea.haiyihy.com:hy/hy-app-monitor.git
TESTBOX_MONITOR_BRANCH=main
JAVA_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-java.git
JAVA_BRANCH=main
GOLANG_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-golang.git
GOLANG_BRANCH=main
CRON_REPO_URL=git@gitea.haiyihy.com:hy/chatapp-cron.git
CRON_BRANCH=main
ADMIN_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-admin.git
ADMIN_BRANCH=main
TESTBOX_SRC_ROOT=/opt/chatapp-test/sources
JAVA_SOURCE_DIR=/opt/chatapp-test/sources/chatapp3-java
GOLANG_SOURCE_DIR=/opt/chatapp-test/sources/chatapp3-golang
CRON_SOURCE_DIR=/opt/chatapp-test/sources/chatapp-cron
ADMIN_SOURCE_DIR=/opt/chatapp-test/sources/chatapp3-admin

4
testbox/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.env
mysql/baseline/*.sql
!mysql/baseline/README.md
!mysql/baseline/.gitkeep

17
testbox/Caddyfile Normal file
View File

@ -0,0 +1,17 @@
{
email admin@haiyihy.com
}
{$TESTBOX_DOMAIN} {
encode zstd gzip
reverse_proxy gateway:8080
}
{$TESTBOX_ADMIN_DOMAIN} {
encode zstd gzip
@console path /console /console/*
reverse_proxy @console gateway:8080
reverse_proxy admin:8080
}

98
testbox/README.md Normal file
View File

@ -0,0 +1,98 @@
# Chatapp testbox
Single-node test environment for chatapp Java services, Go API, cron worker,
and local infra.
Public API entry:
```text
https://jvapi-test.haiyihy.com
```
## What runs here
- MySQL 8.0
- Redis 7
- MongoDB 7
- Nacos standalone, namespace `RedCircleServiceTest`
- RocketMQ namesrv/broker/proxy
- Java: auth, gateway, external, console, other, live, wallet, order
- Go: chatapp3-golang API
- Cron: chatapp-cron week-star worker
- Admin: chatapp3-admin at `https://yumi-test-admi.haiyihy.com`
- Caddy for HTTPS reverse proxy to gateway
## First deployment
Make sure SSH works first:
```bash
ssh root@43.165.195.39 'hostname'
```
Then trigger a git-based deploy:
```bash
cd /Users/hy/Documents/haiyi/hy-app-monitor
bash testbox/scripts/deploy-from-git.sh root@43.165.195.39
```
The script only SSHes to the testbox. The server pulls these git repositories,
installs Docker on the testbox, starts infra, imports Nacos configs, applies
MySQL baseline files, builds images, and starts apps:
```text
git@gitea.haiyihy.com:hy/hy-app-monitor.git
git@gitea.haiyihy.com:hy/chatapp3-java.git
git@gitea.haiyihy.com:hy/chatapp3-golang.git
git@gitea.haiyihy.com:hy/chatapp-cron.git
git@gitea.haiyihy.com:hy/chatapp3-admin.git
```
After first deployment, you can run the deploy directly on the server:
```bash
ssh root@43.165.195.39 'bash /opt/chatapp-test/scripts/deploy-from-git-remote.sh'
```
The runtime `.env` stays on the server at `/opt/chatapp-test/.env`; git deploys
do not overwrite it.
## MySQL baseline
Java services need real schema and base config rows. Put sanitized SQL files in:
```text
testbox/mysql/baseline/
```
They are applied in lexical order. Recommended:
```text
10-schema.sql
20-base-data.sql
```
The deploy script then applies Go migrations from `chatapp3-golang/migrations`
and Java incremental SQL from `chatapp3-java/sql`.
## Runtime commands on the server
```bash
cd /opt/chatapp-test
docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml ps
docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml logs -f gateway
docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml restart other
```
Local-only infra ports on the server:
```text
127.0.0.1:3306 MySQL
127.0.0.1:6379 Redis
127.0.0.1:27017 Mongo
127.0.0.1:8848 Nacos
127.0.0.1:8081 RocketMQ proxy
```
Only `80/443` are intended to be public.

View File

@ -0,0 +1,11 @@
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0
deleteWhen=04
fileReservedTime=48
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
autoCreateTopicEnable=true
autoCreateSubscriptionGroup=true
listenPort=10911
namesrvAddr=rocketmq-namesrv:9876

View File

@ -0,0 +1,302 @@
x-java-env: &java-env
NACOS_HOST: nacos
NACOS_PORT: "8848"
NACOS_USERNAME: ${NACOS_USERNAME}
NACOS_PASSWORD: ${NACOS_PASSWORD}
SERVER_PROFILE_ACTIVE: ${SERVER_PROFILE_ACTIVE}
LIKEI_MYSQL_JDBC_URL: ${LIKEI_MYSQL_JDBC_URL}
LIKEI_MYSQL_USERNAME: ${LIKEI_MYSQL_USERNAME}
LIKEI_MYSQL_PASSWORD: ${LIKEI_MYSQL_PASSWORD}
LIKEI_REDIS_HOST: ${LIKEI_REDIS_HOST}
LIKEI_REDIS_PORT: ${LIKEI_REDIS_PORT}
LIKEI_REDIS_DATABASE: ${LIKEI_REDIS_DATABASE}
LIKEI_MONGO_URI: ${LIKEI_MONGO_URI}
LIKEI_ROCKETMQ_ACCESS_KEY: ${LIKEI_ROCKETMQ_ACCESS_KEY}
LIKEI_ROCKETMQ_SECRET_KEY: ${LIKEI_ROCKETMQ_SECRET_KEY}
LIKEI_ROCKETMQ_ENDPOINT: ${LIKEI_ROCKETMQ_ENDPOINT}
LIKEI_ROCKETMQ_LOCAL_DISABLE: ${LIKEI_ROCKETMQ_LOCAL_DISABLE}
LIKEI_GATEWAY_ROUTE_AUTH_URI: ${LIKEI_GATEWAY_ROUTE_AUTH_URI}
LIKEI_GATEWAY_ROUTE_EXTERNAL_URI: ${LIKEI_GATEWAY_ROUTE_EXTERNAL_URI}
LIKEI_GATEWAY_ROUTE_ORDER_URI: ${LIKEI_GATEWAY_ROUTE_ORDER_URI}
LIKEI_GATEWAY_ROUTE_WALLET_URI: ${LIKEI_GATEWAY_ROUTE_WALLET_URI}
LIKEI_GATEWAY_ROUTE_LIVE_URI: ${LIKEI_GATEWAY_ROUTE_LIVE_URI}
LIKEI_GATEWAY_ROUTE_OTHER_URI: ${LIKEI_GATEWAY_ROUTE_OTHER_URI}
LIKEI_GATEWAY_ROUTE_GO_URI: ${LIKEI_GATEWAY_ROUTE_GO_URI}
LIKEI_GATEWAY_ROUTE_INVITE_PUBLIC_URI: ${LIKEI_GATEWAY_ROUTE_INVITE_PUBLIC_URI}
LIKEI_GATEWAY_ROUTE_INVITE_APP_URI: ${LIKEI_GATEWAY_ROUTE_INVITE_APP_URI}
LIKEI_GATEWAY_ROUTE_REGISTER_REWARD_APP_URI: ${LIKEI_GATEWAY_ROUTE_REGISTER_REWARD_APP_URI}
LIKEI_GATEWAY_ROUTE_INVITE_INTERNAL_URI: ${LIKEI_GATEWAY_ROUTE_INVITE_INTERNAL_URI}
LIKEI_GATEWAY_ROUTE_GAME_APP_URI: ${LIKEI_GATEWAY_ROUTE_GAME_APP_URI}
LIKEI_GATEWAY_ROUTE_GAME_INTERNAL_URI: ${LIKEI_GATEWAY_ROUTE_GAME_INTERNAL_URI}
LIKEI_GATEWAY_ROUTE_GAME_BAISHUN_URI: ${LIKEI_GATEWAY_ROUTE_GAME_BAISHUN_URI}
LIKEI_GATEWAY_ROUTE_FORWARD_URI: ${LIKEI_GATEWAY_ROUTE_FORWARD_URI}
LIKEI_GATEWAY_FORWARD_ROUTE_AES_KEY: ${LIKEI_GATEWAY_FORWARD_ROUTE_AES_KEY}
LIKEI_GATEWAY_FORWARD_AES_KEY: ${LIKEI_GATEWAY_FORWARD_AES_KEY}
x-java-small: &java-small
JVM_OPTION: "-Xms${JAVA_XMS} -Xmx${JAVA_XMX_SMALL} -XX:MaxMetaspaceSize=${JAVA_METASPACE} -Djava.security.egd=file:/dev/./urandom"
x-java-medium: &java-medium
JVM_OPTION: "-Xms${JAVA_XMS} -Xmx${JAVA_XMX_MEDIUM} -XX:MaxMetaspaceSize=${JAVA_METASPACE} -Djava.security.egd=file:/dev/./urandom"
x-app-depends: &app-depends
mysql:
condition: service_healthy
redis:
condition: service_healthy
mongo:
condition: service_healthy
nacos:
condition: service_healthy
services:
auth:
build:
context: ${JAVA_SOURCE_DIR}/rc-auth
args:
SERVICE_VERSION: testbox
image: chatapp-test/auth:testbox
container_name: chatapp-test-auth
restart: unless-stopped
depends_on: *app-depends
env_file:
- .env
environment:
<<: [*java-env, *java-small]
SERVER_PORT: "1000"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:1000/actuator/health | grep -q UP"]
interval: 15s
timeout: 5s
retries: 40
wallet:
build:
context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-wallet
args:
SERVICE_VERSION: testbox
image: chatapp-test/wallet:testbox
container_name: chatapp-test-wallet
restart: unless-stopped
depends_on: *app-depends
env_file:
- .env
environment:
<<: [*java-env, *java-medium]
SERVER_PORT: "2000"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2000/actuator/health | grep -q UP"]
interval: 15s
timeout: 5s
retries: 40
other:
build:
context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-other
args:
SERVICE_VERSION: testbox
image: chatapp-test/other:testbox
container_name: chatapp-test-other
restart: unless-stopped
depends_on: *app-depends
env_file:
- .env
environment:
<<: [*java-env, *java-medium]
SERVER_PORT: "2400"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2400/actuator/health | grep -q UP"]
interval: 15s
timeout: 5s
retries: 40
live:
build:
context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-live
args:
SERVICE_VERSION: testbox
image: chatapp-test/live:testbox
container_name: chatapp-test-live
restart: unless-stopped
depends_on: *app-depends
env_file:
- .env
environment:
<<: [*java-env, *java-small]
SERVER_PORT: "2500"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2500/actuator/health | grep -q UP"]
interval: 15s
timeout: 5s
retries: 40
order:
build:
context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-order
args:
SERVICE_VERSION: testbox
image: chatapp-test/order:testbox
container_name: chatapp-test-order
restart: unless-stopped
depends_on: *app-depends
env_file:
- .env
environment:
<<: [*java-env, *java-medium]
SERVER_PORT: "2600"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2600/actuator/health | grep -q UP"]
interval: 15s
timeout: 5s
retries: 40
console:
build:
context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-console
args:
SERVICE_VERSION: testbox
image: chatapp-test/console:testbox
container_name: chatapp-test-console
restart: unless-stopped
depends_on: *app-depends
env_file:
- .env
environment:
<<: [*java-env, *java-medium]
SERVER_PORT: "2700"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2700/console/actuator/health | grep -q UP"]
interval: 15s
timeout: 5s
retries: 40
external:
build:
context: ${JAVA_SOURCE_DIR}/rc-service/rc-service-external
args:
SERVICE_VERSION: testbox
image: chatapp-test/external:testbox
container_name: chatapp-test-external
restart: unless-stopped
depends_on: *app-depends
env_file:
- .env
environment:
<<: [*java-env, *java-small]
SERVER_PORT: "3000"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/actuator/health | grep -q UP"]
interval: 15s
timeout: 5s
retries: 40
golang:
build:
context: ${GOLANG_SOURCE_DIR}
args:
APP_VERSION: testbox
image: chatapp-test/golang:testbox
container_name: chatapp-test-golang
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
env_file:
- .env
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:2900/health | grep -q ok"]
interval: 15s
timeout: 5s
retries: 30
chatapp-cron:
build:
context: ${CRON_SOURCE_DIR}
args:
APP_VERSION: testbox
image: chatapp-test/chatapp-cron:testbox
container_name: chatapp-test-cron
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
other:
condition: service_started
wallet:
condition: service_started
env_file:
- .env
admin:
image: chatapp-test/admin:testbox
container_name: chatapp-test-admin
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ | grep -qi '<!doctype html'"]
interval: 15s
timeout: 5s
retries: 20
gateway:
build:
context: ${JAVA_SOURCE_DIR}/rc-gateway
args:
SERVICE_VERSION: testbox
image: chatapp-test/gateway:testbox
container_name: chatapp-test-gateway
restart: unless-stopped
depends_on:
auth:
condition: service_started
other:
condition: service_started
wallet:
condition: service_started
order:
condition: service_started
live:
condition: service_started
external:
condition: service_started
golang:
condition: service_started
env_file:
- .env
environment:
<<: [*java-env, *java-medium]
SERVER_PORT: "8080"
ports:
- "127.0.0.1:8080:8080"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/health || wget -qO- http://127.0.0.1:8080/actuator/health"]
interval: 15s
timeout: 5s
retries: 40
caddy:
image: caddy:2.8-alpine
container_name: chatapp-test-caddy
restart: unless-stopped
depends_on:
gateway:
condition: service_started
admin:
condition: service_started
environment:
TESTBOX_DOMAIN: ${TESTBOX_DOMAIN}
TESTBOX_ADMIN_DOMAIN: ${TESTBOX_ADMIN_DOMAIN}
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
volumes:
caddy-data:
caddy-config:

View File

@ -0,0 +1,153 @@
services:
mysql:
image: mysql:8.0
container_name: chatapp-test-mysql
restart: unless-stopped
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-time-zone=+08:00
- --max_connections=300
- --innodb_buffer_pool_size=2G
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
ports:
- "127.0.0.1:3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD} --silent"]
interval: 10s
timeout: 5s
retries: 30
redis:
image: redis:7.2-alpine
container_name: chatapp-test-redis
restart: unless-stopped
command: >
sh -lc 'if [ -n "$$REDIS_PASSWORD" ]; then
exec redis-server --appendonly yes --requirepass "$$REDIS_PASSWORD";
else exec redis-server --appendonly yes; fi'
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
TZ: Asia/Shanghai
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"]
interval: 10s
timeout: 5s
retries: 30
mongo:
image: mongo:7.0
container_name: chatapp-test-mongo
restart: unless-stopped
environment:
TZ: Asia/Shanghai
ports:
- "127.0.0.1:27017:27017"
volumes:
- mongo-data:/data/db
healthcheck:
test: ["CMD-SHELL", "mongosh --quiet --eval 'db.runCommand({ ping: 1 }).ok' | grep -q 1"]
interval: 10s
timeout: 5s
retries: 30
nacos:
image: nacos/nacos-server:v2.3.2
container_name: chatapp-test-nacos
restart: unless-stopped
environment:
MODE: standalone
PREFER_HOST_MODE: hostname
NACOS_AUTH_ENABLE: "false"
JVM_XMS: 512m
JVM_XMX: 512m
JVM_XMN: 256m
TZ: Asia/Shanghai
ports:
- "127.0.0.1:8848:8848"
- "127.0.0.1:9848:9848"
volumes:
- nacos-data:/home/nacos/data
- nacos-logs:/home/nacos/logs
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8848/nacos/v1/console/health/readiness | grep -Eq '^(UP|OK)$'"]
interval: 10s
timeout: 5s
retries: 60
rocketmq-namesrv:
image: apache/rocketmq:5.3.1
container_name: chatapp-test-rocketmq-namesrv
user: root
restart: unless-stopped
command: sh mqnamesrv
environment:
JAVA_OPT_EXT: "-Xms256m -Xmx256m -Xmn128m"
TZ: Asia/Shanghai
ports:
- "127.0.0.1:9876:9876"
volumes:
- rocketmq-namesrv-logs:/home/rocketmq/logs
healthcheck:
test: ["CMD-SHELL", "bash -lc '</dev/tcp/127.0.0.1/9876'"]
interval: 10s
timeout: 5s
retries: 30
rocketmq-broker:
image: apache/rocketmq:5.3.1
container_name: chatapp-test-rocketmq-broker
user: root
restart: unless-stopped
depends_on:
rocketmq-namesrv:
condition: service_started
command: sh mqbroker -n rocketmq-namesrv:9876 -c /home/rocketmq/conf/broker.conf
environment:
JAVA_OPT_EXT: "-Xms512m -Xmx512m -Xmn256m"
TZ: Asia/Shanghai
ports:
- "127.0.0.1:10909:10909"
- "127.0.0.1:10911:10911"
volumes:
- ./config/rocketmq/broker.conf:/home/rocketmq/conf/broker.conf:ro
- rocketmq-broker-store:/home/rocketmq/store
- rocketmq-broker-logs:/home/rocketmq/logs
rocketmq-proxy:
image: apache/rocketmq:5.3.1
container_name: chatapp-test-rocketmq-proxy
user: root
restart: unless-stopped
depends_on:
rocketmq-broker:
condition: service_started
command: sh mqproxy -n rocketmq-namesrv:9876
environment:
JAVA_OPT_EXT: "-Xms256m -Xmx256m -Xmn128m"
TZ: Asia/Shanghai
ports:
- "127.0.0.1:8081:8081"
- "127.0.0.1:8082:8080"
volumes:
mysql-data:
redis-data:
mongo-data:
nacos-data:
nacos-logs:
rocketmq-namesrv-logs:
rocketmq-broker-store:
rocketmq-broker-logs:

View File

@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1.7
FROM node:22-slim AS builder
ARG ADMIN_API_BASE_URL=/console
ARG ADMIN_OSS_URL=
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV NODE_OPTIONS="--max-old-space-size=8192"
ENV TZ="Asia/Shanghai"
ENV CI="true"
RUN npm i -g corepack && corepack enable
WORKDIR /app
COPY . /app
RUN sed -i "s#^VITE_GLOB_API_URL=.*#VITE_GLOB_API_URL=${ADMIN_API_BASE_URL}#" apps/.env.production \
&& sed -i "s#^VITE_GLOB_OSS_URL=.*#VITE_GLOB_OSS_URL=${ADMIN_OSS_URL}#" apps/.env.production
RUN pnpm install --frozen-lockfile
RUN pnpm run build:antdv-next
FROM nginx:stable-alpine
RUN echo "types { application/javascript js mjs; }" > /etc/nginx/conf.d/mjs.conf \
&& rm -rf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/apps/dist /usr/share/nginx/html
COPY --from=builder /app/scripts/deploy/nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

View File

View File

@ -0,0 +1,8 @@
Put repeatable test baseline SQL files here.
Suggested files:
- 10-schema.sql: schema-only dump from production.
- 20-base-data.sql: whitelisted config/base data only.
The deploy script applies files in lexical order before Go migrations.

View File

@ -0,0 +1 @@
CREATE DATABASE IF NOT EXISTS `likei` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
env_value() {
python3 "$ROOT_DIR/scripts/env-value.py" "$ROOT_DIR/.env" "$1" "${2:-}"
}
MYSQL_CONTAINER="${MYSQL_CONTAINER:-$(env_value MYSQL_CONTAINER chatapp-test-mysql)}"
MYSQL_DATABASE="${MYSQL_DATABASE:-$(env_value MYSQL_DATABASE likei)}"
MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-$(env_value MYSQL_ROOT_PASSWORD)}"
GOLANG_SOURCE_DIR="${GOLANG_SOURCE_DIR:-$(env_value GOLANG_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-golang")}"
JAVA_SOURCE_DIR="${JAVA_SOURCE_DIR:-$(env_value JAVA_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-java")}"
TESTBOX_APPLY_JAVA_SQL="${TESTBOX_APPLY_JAVA_SQL:-$(env_value TESTBOX_APPLY_JAVA_SQL 1)}"
if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
echo "MYSQL_ROOT_PASSWORD is required" >&2
exit 1
fi
mysql_exec() {
docker exec -i "$MYSQL_CONTAINER" mysql -uroot -p"$MYSQL_ROOT_PASSWORD" "$@"
}
echo "waiting mysql..."
for _ in $(seq 1 90); do
if docker exec "$MYSQL_CONTAINER" mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" --silent >/dev/null 2>&1; then
break
fi
sleep 2
done
mysql_exec -e "CREATE DATABASE IF NOT EXISTS \`$MYSQL_DATABASE\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
shopt -s nullglob
baseline_files=("$ROOT_DIR"/mysql/baseline/*.sql)
if [ "${#baseline_files[@]}" -eq 0 ]; then
echo "warn: no mysql baseline SQL found in mysql/baseline; Java services need schema/base data before full use" >&2
else
for sql in "${baseline_files[@]}"; do
echo "applying baseline $(basename "$sql")"
mysql_exec "$MYSQL_DATABASE" < "$sql"
done
fi
GO_MIGRATIONS_DIR="$GOLANG_SOURCE_DIR/migrations"
if [ -d "$GO_MIGRATIONS_DIR" ]; then
for sql in "$GO_MIGRATIONS_DIR"/*.sql; do
[ -f "$sql" ] || continue
echo "applying go migration $(basename "$sql")"
mysql_exec "$MYSQL_DATABASE" < "$sql" || {
echo "warn: go migration failed, continuing: $sql" >&2
}
done
fi
JAVA_SQL_DIR="$JAVA_SOURCE_DIR/sql"
if [ "${TESTBOX_APPLY_JAVA_SQL:-1}" = "1" ] && [ -d "$JAVA_SQL_DIR" ]; then
for sql in "$JAVA_SQL_DIR"/*.sql; do
[ -f "$sql" ] || continue
echo "applying java sql $(basename "$sql")"
mysql_exec "$MYSQL_DATABASE" < "$sql" || {
echo "warn: java sql failed, continuing: $sql" >&2
}
done
fi
echo "mysql baseline step done"

View File

@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [ "$(id -u)" -ne 0 ]; then
echo "bootstrap-server.sh must run as root" >&2
exit 1
fi
install_packages() {
if command -v dnf >/dev/null 2>&1; then
dnf -y install curl rsync git tar gzip python3 python3-pip dnf-plugins-core ca-certificates java-17-openjdk-devel maven >/dev/null
if ! command -v docker >/dev/null 2>&1; then
dnf -y install docker docker-compose-plugin >/dev/null 2>&1 || {
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo >/dev/null
dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null
}
fi
elif command -v apt-get >/dev/null 2>&1; then
apt-get update >/dev/null
apt-get install -y curl rsync git tar gzip python3 python3-pip ca-certificates openjdk-17-jdk maven docker.io docker-compose-plugin >/dev/null
else
echo "unsupported Linux distribution: missing dnf/apt-get" >&2
exit 1
fi
}
install_packages
systemctl enable --now docker >/dev/null
mkdir -p "$ROOT_DIR"/{sources,mysql/baseline,run,logs}
sysctl -w vm.max_map_count=262144 >/dev/null || true
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then
firewall-cmd --add-service=http --permanent >/dev/null || true
firewall-cmd --add-service=https --permanent >/dev/null || true
firewall-cmd --reload >/dev/null || true
fi
if [ ! -f "$ROOT_DIR/.env" ]; then
cp "$ROOT_DIR/.env.example" "$ROOT_DIR/.env"
echo "created $ROOT_DIR/.env from example; please review passwords before real use" >&2
fi
docker compose version

43
testbox/scripts/build-images.sh Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
env_value() {
python3 "$ROOT_DIR/scripts/env-value.py" "$ROOT_DIR/.env" "$1" "${2:-}"
}
JAVA_SOURCE_DIR="${JAVA_SOURCE_DIR:-$(env_value JAVA_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-java")}"
ADMIN_SOURCE_DIR="${ADMIN_SOURCE_DIR:-$(env_value ADMIN_SOURCE_DIR "$ROOT_DIR/sources/chatapp3-admin")}"
TESTBOX_ADMIN_API_BASE_URL="${TESTBOX_ADMIN_API_BASE_URL:-$(env_value TESTBOX_ADMIN_API_BASE_URL "https://yumi-test-admi.haiyihy.com/console")}"
TESTBOX_ADMIN_OSS_URL="${TESTBOX_ADMIN_OSS_URL:-$(env_value TESTBOX_ADMIN_OSS_URL "")}"
if [ ! -d "$JAVA_SOURCE_DIR" ]; then
echo "missing JAVA_SOURCE_DIR: $JAVA_SOURCE_DIR" >&2
exit 1
fi
if [ ! -d "$ADMIN_SOURCE_DIR" ]; then
echo "missing ADMIN_SOURCE_DIR: $ADMIN_SOURCE_DIR" >&2
exit 1
fi
cd "$JAVA_SOURCE_DIR"
if [ -x ./ci/install-private-maven.sh ]; then
./ci/install-private-maven.sh
fi
JAVA_MODULES="rc-auth,rc-gateway,rc-service/rc-service-external/external-start,rc-service/rc-service-console/console-start,rc-service/rc-service-other/other-start,rc-service/rc-service-live/live-start,rc-service/rc-service-wallet/wallet-start,rc-service/rc-service-order/order-start"
mvn -Ptest -T 1C -pl "$JAVA_MODULES" -am -Dmaven.test.skip=true package
cd "$ROOT_DIR"
docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml build \
auth gateway external console other live wallet order golang chatapp-cron
docker build \
-f "$ROOT_DIR/docker/chatapp3-admin.Dockerfile" \
--build-arg "ADMIN_API_BASE_URL=$TESTBOX_ADMIN_API_BASE_URL" \
--build-arg "ADMIN_OSS_URL=$TESTBOX_ADMIN_OSS_URL" \
-t chatapp-test/admin:testbox \
"$ADMIN_SOURCE_DIR"

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
NAMESRV="${ROCKETMQ_NAMESRV_ADDR:-rocketmq-namesrv:9876}"
CLUSTER="${ROCKETMQ_CLUSTER:-DefaultCluster}"
BROKER_CONTAINER="${ROCKETMQ_BROKER_CONTAINER:-chatapp-test-rocketmq-broker}"
MQADMIN="/home/rocketmq/rocketmq-5.3.1/bin/mqadmin"
topics=(
RC_DEFAULT_APP_ORDINARY
RC_DEFAULT_APP_DELAYED
RC_DEFAULT_APP_ORDER
)
for topic in "${topics[@]}"; do
docker exec "$BROKER_CONTAINER" sh -lc \
"$MQADMIN updateTopic -n '$NAMESRV' -c '$CLUSTER' -t '$topic' >/dev/null"
echo "rocketmq topic ok: $topic"
done

30
testbox/scripts/deploy-all.sh Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
if [ ! -f .env ]; then
cp .env.example .env
echo "created .env from .env.example; edit credentials and rerun" >&2
exit 2
fi
scripts/merge-env-defaults.sh .env .env.example
COMPOSE=(docker compose --env-file .env -f docker-compose.infra.yml -f docker-compose.apps.yml)
"${COMPOSE[@]}" up -d mysql redis mongo nacos rocketmq-namesrv rocketmq-broker rocketmq-proxy
python3 scripts/import-nacos-configs.py
scripts/create-rocketmq-topics.sh
scripts/apply-mysql-baseline.sh
scripts/build-images.sh
"${COMPOSE[@]}" up -d auth wallet other live order console external golang gateway chatapp-cron admin caddy
echo
echo "testbox status:"
"${COMPOSE[@]}" ps
echo
echo "public url: ${TESTBOX_PUBLIC_BASE_URL:-https://jvapi-test.haiyihy.com}"

View File

@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
REMOTE_DIR="${REMOTE_DIR:-/opt/chatapp-test}"
MONITOR_CHECKOUT_DIR="${MONITOR_CHECKOUT_DIR:-$REMOTE_DIR/git/hy-app-monitor}"
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o StrictHostKeyChecking=accept-new}"
env_file_value() {
local key="$1"
local file="${2:-$REMOTE_DIR/.env}"
[ -f "$file" ] || return 0
awk -v key="$key" '
BEGIN { FS = "=" }
$1 == key {
sub(/^[^=]*=/, "")
gsub(/^[ \t"'"'"']+|[ \t"'"'"']+$/, "")
print
exit
}
' "$file"
}
MONITOR_REPO_URL="${MONITOR_REPO_URL:-$(env_file_value TESTBOX_MONITOR_REPO_URL)}"
MONITOR_REPO_URL="${MONITOR_REPO_URL:-git@gitea.haiyihy.com:hy/hy-app-monitor.git}"
MONITOR_BRANCH="${MONITOR_BRANCH:-$(env_file_value TESTBOX_MONITOR_BRANCH)}"
MONITOR_BRANCH="${MONITOR_BRANCH:-main}"
sync_repo() {
local dir="$1"
local url="$2"
local branch="$3"
local stamp
mkdir -p "$(dirname "$dir")"
if [ -e "$dir" ] && [ ! -d "$dir/.git" ]; then
stamp="$(date +%Y%m%d%H%M%S)"
mv "$dir" "$dir.pre-git-$stamp"
echo "moved non-git directory to $dir.pre-git-$stamp"
fi
if [ ! -d "$dir/.git" ]; then
git clone --branch "$branch" "$url" "$dir"
fi
git -C "$dir" remote set-url origin "$url"
git -C "$dir" fetch --prune origin "$branch"
git -C "$dir" checkout -B "$branch" "origin/$branch"
git -C "$dir" reset --hard "origin/$branch"
}
sync_repo "$MONITOR_CHECKOUT_DIR" "$MONITOR_REPO_URL" "$MONITOR_BRANCH"
latest_script="$MONITOR_CHECKOUT_DIR/testbox/scripts/deploy-from-git-remote.sh"
current_script="${BASH_SOURCE[0]}"
if [ "${TESTBOX_REMOTE_DEPLOY_BOOTSTRAPPED:-0}" != "1" ] && [ "$current_script" != "$latest_script" ]; then
export TESTBOX_REMOTE_DEPLOY_BOOTSTRAPPED=1
exec bash "$latest_script"
fi
mkdir -p "$REMOTE_DIR"/{git,sources,run,logs}
rsync --archive --delete \
--exclude ".env" \
--exclude "git" \
--exclude "sources" \
--exclude "run" \
--exclude "logs" \
--exclude "mysql/baseline/*.sql" \
"$MONITOR_CHECKOUT_DIR/testbox/" "$REMOTE_DIR/"
bash "$REMOTE_DIR/scripts/bootstrap-server.sh"
bash "$REMOTE_DIR/scripts/merge-env-defaults.sh" "$REMOTE_DIR/.env" "$REMOTE_DIR/.env.example"
env_value() {
python3 "$REMOTE_DIR/scripts/env-value.py" "$REMOTE_DIR/.env" "$1" "${2:-}"
}
JAVA_REPO_URL="$(env_value JAVA_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-java.git)"
JAVA_BRANCH="$(env_value JAVA_BRANCH main)"
GOLANG_REPO_URL="$(env_value GOLANG_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-golang.git)"
GOLANG_BRANCH="$(env_value GOLANG_BRANCH main)"
CRON_REPO_URL="$(env_value CRON_REPO_URL git@gitea.haiyihy.com:hy/chatapp-cron.git)"
CRON_BRANCH="$(env_value CRON_BRANCH main)"
ADMIN_REPO_URL="$(env_value ADMIN_REPO_URL git@gitea.haiyihy.com:hy/chatapp3-admin.git)"
ADMIN_BRANCH="$(env_value ADMIN_BRANCH main)"
JAVA_SOURCE_DIR="$(env_value JAVA_SOURCE_DIR "$REMOTE_DIR/sources/chatapp3-java")"
GOLANG_SOURCE_DIR="$(env_value GOLANG_SOURCE_DIR "$REMOTE_DIR/sources/chatapp3-golang")"
CRON_SOURCE_DIR="$(env_value CRON_SOURCE_DIR "$REMOTE_DIR/sources/chatapp-cron")"
ADMIN_SOURCE_DIR="$(env_value ADMIN_SOURCE_DIR "$REMOTE_DIR/sources/chatapp3-admin")"
echo "updating source repos from git..."
sync_repo "$JAVA_SOURCE_DIR" "$JAVA_REPO_URL" "$JAVA_BRANCH"
sync_repo "$GOLANG_SOURCE_DIR" "$GOLANG_REPO_URL" "$GOLANG_BRANCH"
sync_repo "$CRON_SOURCE_DIR" "$CRON_REPO_URL" "$CRON_BRANCH"
sync_repo "$ADMIN_SOURCE_DIR" "$ADMIN_REPO_URL" "$ADMIN_BRANCH"
cd "$REMOTE_DIR"
bash scripts/deploy-all.sh

View File

@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
REMOTE="${1:-root@43.165.195.39}"
REMOTE_DIR="${REMOTE_DIR:-/opt/chatapp-test}"
MONITOR_REPO_URL="${MONITOR_REPO_URL:-git@gitea.haiyihy.com:hy/hy-app-monitor.git}"
MONITOR_BRANCH="${MONITOR_BRANCH:-main}"
MONITOR_CHECKOUT_DIR="${MONITOR_CHECKOUT_DIR:-$REMOTE_DIR/git/hy-app-monitor}"
ssh "$REMOTE" \
"REMOTE_DIR='$REMOTE_DIR' MONITOR_REPO_URL='$MONITOR_REPO_URL' MONITOR_BRANCH='$MONITOR_BRANCH' MONITOR_CHECKOUT_DIR='$MONITOR_CHECKOUT_DIR' bash -s" <<'REMOTE_BOOTSTRAP'
set -euo pipefail
REMOTE_DIR="${REMOTE_DIR:-/opt/chatapp-test}"
MONITOR_REPO_URL="${MONITOR_REPO_URL:-git@gitea.haiyihy.com:hy/hy-app-monitor.git}"
MONITOR_BRANCH="${MONITOR_BRANCH:-main}"
MONITOR_CHECKOUT_DIR="${MONITOR_CHECKOUT_DIR:-$REMOTE_DIR/git/hy-app-monitor}"
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o StrictHostKeyChecking=accept-new}"
install_minimal_packages() {
if command -v git >/dev/null 2>&1 && command -v rsync >/dev/null 2>&1; then
return
fi
if command -v dnf >/dev/null 2>&1; then
dnf -y install git rsync ca-certificates >/dev/null
elif command -v apt-get >/dev/null 2>&1; then
apt-get update >/dev/null
apt-get install -y git rsync ca-certificates >/dev/null
else
echo "unsupported Linux distribution: missing dnf/apt-get" >&2
exit 1
fi
}
sync_repo() {
local dir="$1"
local url="$2"
local branch="$3"
local stamp
mkdir -p "$(dirname "$dir")"
if [ -e "$dir" ] && [ ! -d "$dir/.git" ]; then
stamp="$(date +%Y%m%d%H%M%S)"
mv "$dir" "$dir.pre-git-$stamp"
echo "moved non-git directory to $dir.pre-git-$stamp"
fi
if [ ! -d "$dir/.git" ]; then
git clone --branch "$branch" "$url" "$dir"
fi
git -C "$dir" remote set-url origin "$url"
git -C "$dir" fetch --prune origin "$branch"
git -C "$dir" checkout -B "$branch" "origin/$branch"
git -C "$dir" reset --hard "origin/$branch"
}
install_minimal_packages
sync_repo "$MONITOR_CHECKOUT_DIR" "$MONITOR_REPO_URL" "$MONITOR_BRANCH"
exec bash "$MONITOR_CHECKOUT_DIR/testbox/scripts/deploy-from-git-remote.sh"
REMOTE_BOOTSTRAP

31
testbox/scripts/env-value.py Executable file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from pathlib import Path
def read_value(path: Path, key: str, default: str) -> str:
if not path.exists():
return default
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
name, value = line.split("=", 1)
if name.strip() == key:
return value.strip().strip('"').strip("'")
return default
def main() -> int:
if len(sys.argv) < 3:
print("usage: env-value.py ENV_FILE KEY [DEFAULT]", file=sys.stderr)
return 2
default = sys.argv[3] if len(sys.argv) > 3 else ""
print(read_value(Path(sys.argv[1]), sys.argv[2], default))
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
from pathlib import Path
import sys
import time
import urllib.parse
import urllib.request
ROOT = Path(__file__).resolve().parents[1]
ENV_PATH = ROOT / ".env"
def load_env(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
if not path.exists():
return values
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
ENV = {**load_env(ENV_PATH), **os.environ}
NACOS_BASE_URL = ENV.get("NACOS_BASE_URL", "http://127.0.0.1:8848").rstrip("/")
NAMESPACE_ID = ENV.get("NACOS_NAMESPACE_ID", "RedCircleServiceTest")
NAMESPACE_NAME = ENV.get("NACOS_NAMESPACE_NAME", NAMESPACE_ID)
JAVA_SOURCE_DIR = Path(ENV.get("JAVA_SOURCE_DIR", str(ROOT / "sources" / "chatapp3-java")))
def request(method: str, path: str, data: dict[str, str] | None = None) -> str:
url = f"{NACOS_BASE_URL}{path}"
body = None
headers = {"User-Agent": "chatapp-testbox/1.0"}
if data is not None:
body = urllib.parse.urlencode(data).encode("utf-8")
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
req = urllib.request.Request(url, data=body, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.read().decode("utf-8", errors="replace")
def wait_nacos() -> None:
for _ in range(90):
try:
text = request("GET", "/nacos/v1/console/health/readiness")
if "UP" in text or "OK" in text:
return
except Exception:
pass
time.sleep(2)
raise RuntimeError("nacos readiness timeout")
def ensure_namespace() -> None:
data = {
"customNamespaceId": NAMESPACE_ID,
"namespaceName": NAMESPACE_NAME,
"namespaceDesc": "single-node chatapp testbox",
}
try:
request("POST", "/nacos/v1/console/namespaces", data)
except Exception as exc:
print(f"warn: namespace create skipped: {exc}", file=sys.stderr)
def publish_config(group: str, data_id: str, content: str) -> None:
payload = {
"tenant": NAMESPACE_ID,
"group": group,
"dataId": data_id,
"type": "yaml" if data_id.endswith((".yml", ".yaml")) else "text",
"content": content,
}
result = request("POST", "/nacos/v1/cs/configs", payload).strip()
if result.lower() == "false":
raise RuntimeError(f"nacos rejected config {group}/{data_id}")
def main() -> int:
config_root = JAVA_SOURCE_DIR / "nacos_config"
if not config_root.is_dir():
raise RuntimeError(f"missing nacos_config: {config_root}")
wait_nacos()
ensure_namespace()
count = 0
for path in sorted(config_root.glob("*/*")):
if not path.is_file():
continue
group = path.parent.name
data_id = path.name
publish_config(group, data_id, path.read_text(encoding="utf-8"))
count += 1
print(f"published {group}/{data_id}")
print(f"nacos import ok: namespace={NAMESPACE_ID} configs={count}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
env_file="${1:-.env}"
defaults_file="${2:-.env.example}"
if [ ! -f "$env_file" ] || [ ! -f "$defaults_file" ]; then
exit 0
fi
added=0
while IFS= read -r line || [ -n "$line" ]; do
trimmed="${line#"${line%%[![:space:]]*}"}"
if [ -z "$trimmed" ] || [[ "$trimmed" == \#* ]] || [[ "$trimmed" != *=* ]]; then
continue
fi
key="${trimmed%%=*}"
if ! grep -qE "^${key}=" "$env_file"; then
if [ "$added" -eq 0 ]; then
{
echo
echo "# Added from .env.example on $(date +%F)"
} >> "$env_file"
fi
echo "$trimmed" >> "$env_file"
added=$((added + 1))
fi
done < "$defaults_file"
if [ "$added" -gt 0 ]; then
echo "merged $added missing env defaults into $env_file"
fi

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REMOTE="${1:-root@43.165.195.39}"
echo "sync-to-testbox.sh is now a git-based deploy wrapper."
echo "No local source files or local .env will be copied."
exec "$ROOT_DIR/scripts/deploy-from-git.sh" "$REMOTE"