diff --git a/.env.example b/.env.example index 17122d1f8..a911ad3ce 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,24 @@ -# E2E 测试环境变量示例 +# 环境变量示例 # 复制此文件为 .env.local 并填入真实值 # .env.local 文件不会被提交到 git +# ==================== E2E 测试 ==================== + # 资金账号助记词 - 用于提供测试资金(24个中文词,空格分隔) E2E_TEST_MNEMONIC="词1 词2 词3 词4 词5 词6 词7 词8 词9 词10 词11 词12 词13 词14 词15 词16 词17 词18 词19 词20 词21 词22 词23 词24" # 钱包锁 E2E_TEST_PASSWORD="your-test-password" + +# ==================== DWEB 发布 ==================== + +# SFTP 服务器地址(默认: sftp://iweb.xin:22022) +# DWEB_SFTP_URL="sftp://iweb.xin:22022" + +# 正式版账号(用于 pnpm release 和 --stable 构建) +DWEB_SFTP_USER="keyapp" +DWEB_SFTP_PASS="your-password" + +# 开发版账号(用于日常 CI/CD 和 beta 构建) +DWEB_SFTP_USER_DEV="keyapp-dev" +DWEB_SFTP_PASS_DEV="your-dev-password" diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index fb47ab37f..e10db3ff9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -111,16 +111,16 @@ jobs: fi # 准备 webapp 目录 - mkdir -p docs/public/webapp docs/public/webapp-beta + mkdir -p docs/public/webapp docs/public/webapp-dev if [[ "$CHANNEL" == "stable" ]]; then cp -r dist-web/* docs/public/webapp/ if gh release download --pattern 'bfmpay-web-beta.zip' --dir /tmp -R ${{ github.repository }} 2>/dev/null; then - unzip -q /tmp/bfmpay-web-beta.zip -d docs/public/webapp-beta/ + unzip -q /tmp/bfmpay-web-beta.zip -d docs/public/webapp-dev/ else - cp -r dist-web/* docs/public/webapp-beta/ + cp -r dist-web/* docs/public/webapp-dev/ fi else - cp -r dist-web/* docs/public/webapp-beta/ + cp -r dist-web/* docs/public/webapp-dev/ if gh release download --pattern 'bfmpay-web.zip' --dir /tmp -R ${{ github.repository }} 2>/dev/null; then unzip -q /tmp/bfmpay-web.zip -d docs/public/webapp/ else @@ -168,6 +168,21 @@ jobs: cp release/bfmpay-dweb-beta.zip "release/bfmpay-dweb-${VERSION}-beta.zip" fi + # 上传 DWEB 到 SFTP 服务器 + - name: Upload DWEB to SFTP + env: + CHANNEL: ${{ steps.channel.outputs.channel }} + DWEB_SFTP_USER: ${{ secrets.DWEB_SFTP_USER }} + DWEB_SFTP_PASS: ${{ secrets.DWEB_SFTP_PASS }} + DWEB_SFTP_USER_DEV: ${{ secrets.DWEB_SFTP_USER_DEV }} + DWEB_SFTP_PASS_DEV: ${{ secrets.DWEB_SFTP_PASS_DEV }} + run: | + if [[ "$CHANNEL" == "stable" ]]; then + bun scripts/build.ts dweb --upload --stable --skip-typecheck --skip-test + else + bun scripts/build.ts dweb --upload --skip-typecheck --skip-test + fi + # 直接推送到 gh-pages 分支,避免使用 upload-pages-artifact(self-hosted 上容易卡住) - name: Deploy to GitHub Pages env: @@ -197,7 +212,7 @@ jobs: ### 在线访问 - Web 应用 (stable): https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/webapp/ - - Web 应用 (beta): https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/webapp-beta/ + - Web 应用 (beta): https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/webapp-dev/ - 文档首页: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/ ### DWEB 安装 @@ -250,6 +265,11 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - name: Install dependencies run: pnpm install --frozen-lockfile @@ -314,20 +334,20 @@ jobs: echo "Copied current build to docs/public/webapp/ (stable)" # 尝试下载最新的 beta 版本(从 latest prerelease) - mkdir -p docs/public/webapp-beta + mkdir -p docs/public/webapp-dev if gh release download --pattern 'bfmpay-web-beta.zip' --dir /tmp -R ${{ github.repository }} 2>/dev/null; then - unzip -q /tmp/bfmpay-web-beta.zip -d docs/public/webapp-beta/ + unzip -q /tmp/bfmpay-web-beta.zip -d docs/public/webapp-dev/ echo "Downloaded beta version from release" else # 如果没有 beta release,使用当前构建 - cp -r dist-web/* docs/public/webapp-beta/ - echo "No beta release found, using current build for webapp-beta" + cp -r dist-web/* docs/public/webapp-dev/ + echo "No beta release found, using current build for webapp-dev" fi else - # beta 构建:当前版本放 webapp-beta/,尝试下载 stable - mkdir -p docs/public/webapp-beta - cp -r dist-web/* docs/public/webapp-beta/ - echo "Copied current build to docs/public/webapp-beta/ (beta)" + # beta 构建:当前版本放 webapp-dev/,尝试下载 stable + mkdir -p docs/public/webapp-dev + cp -r dist-web/* docs/public/webapp-dev/ + echo "Copied current build to docs/public/webapp-dev/ (beta)" # 尝试下载最新的 stable 版本 mkdir -p docs/public/webapp @@ -344,8 +364,8 @@ jobs: # 显示准备好的目录 echo "=== webapp directory ===" ls -la docs/public/webapp/ | head -5 - echo "=== webapp-beta directory ===" - ls -la docs/public/webapp-beta/ | head -5 + echo "=== webapp-dev directory ===" + ls -la docs/public/webapp-dev/ | head -5 # ===== 构建 Storybook ===== - name: Build Storybook @@ -414,6 +434,21 @@ jobs: echo "=== Release artifacts ===" ls -la release/ + # ===== 上传 DWEB 到 SFTP 服务器 ===== + - name: Upload DWEB to SFTP + env: + CHANNEL: ${{ steps.channel.outputs.channel }} + DWEB_SFTP_USER: ${{ secrets.DWEB_SFTP_USER }} + DWEB_SFTP_PASS: ${{ secrets.DWEB_SFTP_PASS }} + DWEB_SFTP_USER_DEV: ${{ secrets.DWEB_SFTP_USER_DEV }} + DWEB_SFTP_PASS_DEV: ${{ secrets.DWEB_SFTP_PASS_DEV }} + run: | + if [[ "$CHANNEL" == "stable" ]]; then + bun scripts/build.ts dweb --upload --stable --skip-typecheck --skip-test + else + bun scripts/build.ts dweb --upload --skip-typecheck --skip-test + fi + # ===== 上传构建产物 ===== - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@v3 @@ -470,7 +505,7 @@ jobs: ### 在线访问 - Web 应用 (stable): https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/webapp/ - - Web 应用 (beta): https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/webapp-beta/ + - Web 应用 (beta): https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/webapp-dev/ - 文档首页: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/ ### DWEB 安装 diff --git a/.gitignore b/.gitignore index 923d15b25..c9afb0a12 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ storybook-static/ docs/.vitepress/dist docs/.vitepress/cache docs/public/webapp -docs/public/webapp-beta +docs/public/webapp-dev # dependencies node_modules diff --git a/CHAT.md b/CHAT.md index b2d846e17..1e9418cc6 100644 --- a/CHAT.md +++ b/CHAT.md @@ -28,7 +28,7 @@ PDR文件不该包含过多的软件工程技术细节吧,反而应该专注 我们的build.ts脚本所实现的持续集成功能,其实默认生成的是“beta”渠道版本: 1. 这是每次git-push到main分支就会自动触发的。 -2. beta版本的网页版链接应该是 `https://???.github.io/???/webapp-beta/` +2. beta版本的网页版链接应该是 `https://???.github.io/???/webapp-dev/` 1. 也就是说“stable”版本仍然在`https://???.github.io/???/webapp/`,这个我们可能需要从github-release找最近的一个stable版本 2. 如果要生成“stable”,那么需要本地执行`pnpm gen:stable`,这个脚本默认是在本地执行: 1. 它的目的是更新版本号,生成changelog,这是一个交互式的命令。 @@ -49,7 +49,7 @@ PDR文件不该包含过多的软件工程技术细节吧,反而应该专注 1. 我们需要vitepress作为我们页面的骨架 2. 不论是stable还是beta,默认都应该从github-release下载,如果下载不到,那么就使用本地构建的 -3. download页面可以下载各种渠道的版本,目前有4个:webapp-stable/webapp-beta/dwebapp-stable/dwebapp-beta +3. download页面可以下载各种渠道的版本,目前有4个:webapp-stable/webapp-dev/dwebapp-stable/dwebapp-dev --- diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a494fff87..ef9823831 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -24,7 +24,7 @@ export default defineConfig({ base, // webapp/storybook 目录由 CI 动态生成,忽略死链接检查 - ignoreDeadLinks: [/\.\/webapp/, /\.\/webapp-beta/, /\.\/storybook/], + ignoreDeadLinks: [/\.\/webapp/, /\.\/webapp-dev/, /\.\/storybook/], head: [ ['link', { rel: 'icon', type: 'image/webp', href: '/logos/logo-64.webp' }], diff --git a/docs/.vitepress/plugins/webapp-loader.ts b/docs/.vitepress/plugins/webapp-loader.ts index dd1cfb21e..9dd0f6b43 100644 --- a/docs/.vitepress/plugins/webapp-loader.ts +++ b/docs/.vitepress/plugins/webapp-loader.ts @@ -2,7 +2,7 @@ * VitePress 插件:在 dev/build 前准备 webapp 目录 * * 流程: - * 1. 尝试从 GitHub Release 下载 webapp/webapp-beta + * 1. 尝试从 GitHub Release 下载 webapp/webapp-dev * 2. 下载失败则检查本地 dist-web 是否存在 * 3. 都没有则运行本地构建 */ @@ -161,7 +161,7 @@ async function prepareAllWebapps() { // 并行准备两个版本 await Promise.all([ prepareWebapp('stable', join(DOCS_PUBLIC, 'webapp')), - prepareWebapp('beta', join(DOCS_PUBLIC, 'webapp-beta')), + prepareWebapp('beta', join(DOCS_PUBLIC, 'webapp-dev')), ]) log.success('webapp 目录准备完成') @@ -186,8 +186,8 @@ export function webappLoaderPlugin(): Plugin { server.middlewares.use((req, res, next) => { const url = req.url || '' - // 处理 /webapp/ 和 /webapp-beta/ 路径 - const match = url.match(/^\/(webapp|webapp-beta)(\/.*)?$/) + // 处理 /webapp/ 和 /webapp-dev/ 路径 + const match = url.match(/^\/(webapp|webapp-dev)(\/.*)?$/) if (match) { const webappDir = match[1] const subPath = match[2] || '/' diff --git a/docs/contributing.md b/docs/contributing.md index 0173276ff..4f3d32efa 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -102,7 +102,7 @@ CI/CD 会自动构建以下产物: |------|------| | `/` | VitePress 文档站点 | | `/webapp/` | Web 应用(稳定版) | -| `/webapp-beta/` | Web 应用(测试版) | +| `/webapp-dev/` | Web 应用(测试版) | | `/storybook/` | Storybook 组件文档 | ## 开发规范 diff --git a/docs/download.md b/docs/download.md index 8f52924cd..1ca50ce80 100644 --- a/docs/download.md +++ b/docs/download.md @@ -22,7 +22,7 @@
包含最新功能,每次代码更新自动发布。
- 打开 Beta 版 + 打开 Beta 版 diff --git a/docs/index.md b/docs/index.md index 5e2e7e757..e8ac0115e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -49,7 +49,7 @@ features: | 平台 | 稳定版 | 测试版 | |------|--------|--------| -| **Web** | webapp/ | webapp-beta/ | +| **Web** | webapp/ | webapp-dev/ | | **DWEB** | [下载页面](./download) | [下载页面](./download) | 查看 [下载页面](./download) 了解更多版本信息。 diff --git a/package.json b/package.json index a88be5338..93429b9b1 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs", "gen:stable": "bun scripts/gen-stable.ts", + "release": "bun scripts/release.ts", "gen:logo": "bun scripts/gen-logo.ts", "i18n:extract": "bun scripts/i18n-extract.ts", "i18n:check": "turbo run i18n:run --", @@ -161,6 +162,7 @@ "semver": "^7.7.3", "shadcn": "^3.6.1", "sharp": "^0.34.5", + "ssh2-sftp-client": "^12.0.1", "storybook": "^10.1.4", "tailwindcss": "^4.0.0", "turbo": "^2.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69921f728..0e598ea18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,6 +303,9 @@ importers: sharp: specifier: ^0.34.5 version: 0.34.5 + ssh2-sftp-client: + specifier: ^12.0.1 + version: 12.0.1 storybook: specifier: ^10.1.4 version: 10.1.10(@testing-library/dom@10.4.0)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -3989,6 +3992,9 @@ packages: bs58check@2.1.2: resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-more-ints@1.0.0: resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==} @@ -3998,6 +4004,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -4182,6 +4192,10 @@ packages: component-inherit@0.0.3: resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + conf@9.0.2: resolution: {integrity: sha512-rLSiilO85qHgaTBIIHQpsv8z+NnVfZq3cKuYNCXN1AOqPzced0GWZEe/A517VldRLyQYXUMyV+vszavE2jSAqw==} engines: {node: '>=10'} @@ -4252,6 +4266,10 @@ packages: typescript: optional: true + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -5670,6 +5688,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.24.0: + resolution: {integrity: sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6554,6 +6575,14 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssh2-sftp-client@12.0.1: + resolution: {integrity: sha512-ICJ1L2PmBel2Q2ctbyxzTFZCPKSHYYD6s2TFZv7NXmZDrDNGk8lHBb/SK2WgXLMXNANH78qoumeJzxlWZqSqWg==} + engines: {node: '>=18.20.4'} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + sshpk@1.18.0: resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} engines: {node: '>=0.10.0'} @@ -6925,6 +6954,9 @@ packages: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typeorm@0.3.20: resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} engines: {node: '>=16.13.0'} @@ -11329,6 +11361,8 @@ snapshots: create-hash: 1.2.0 safe-buffer: 5.2.1 + buffer-from@1.1.2: {} + buffer-more-ints@1.0.0: {} buffer-xor@1.0.3: {} @@ -11338,6 +11372,9 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buildcheck@0.0.7: + optional: true + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -11519,6 +11556,13 @@ snapshots: component-inherit@0.0.3: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + conf@9.0.2: dependencies: ajv: 7.2.4 @@ -11584,6 +11628,12 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.24.0 + optional: true + crc-32@1.2.2: {} create-hash@1.2.0: @@ -13074,6 +13124,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.24.0: + optional: true + nanoid@3.3.11: {} negotiator@1.0.0: {} @@ -14004,6 +14057,19 @@ snapshots: sprintf-js@1.0.3: {} + ssh2-sftp-client@12.0.1: + dependencies: + concat-stream: 2.0.0 + ssh2: 1.17.0 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.24.0 + sshpk@1.18.0: dependencies: asn1: 0.2.6 @@ -14373,6 +14439,8 @@ snapshots: es-errors: 1.3.0 is-typed-array: 1.1.15 + typedarray@0.0.6: {} + typeorm@0.3.20(redis@4.6.7): dependencies: '@sqltools/formatter': 1.2.5 diff --git a/scripts/build.ts b/scripts/build.ts index fd55fdddc..8918708e6 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -20,7 +20,7 @@ * --upload 上传 dweb 版本到服务器 * * 渠道说明: - * beta: 部署到 /webapp-beta/,每次 push main 自动触发 + * beta: 部署到 /webapp-dev/,每次 push main 自动触发 * stable: 部署到 /webapp/,通过 pnpm gen:stable 手动触发 */ @@ -28,6 +28,10 @@ import { execSync } from 'node:child_process' import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, cpSync } from 'node:fs' import { join, resolve } from 'node:path' import { createWriteStream } from 'node:fs' +import { uploadToSftp, getNextDevVersion, getTodayDateString } from './utils/sftp' + +// Dev 版本信息(在 buildDweb 时设置) +let devVersionInfo: { version: string; dateDir: string } | null = null // ==================== 配置 ==================== @@ -160,32 +164,87 @@ async function buildWeb() { } async function buildDweb() { - log.step('构建 DWEB 版本') + const channel = getChannel() + const isDev = channel === 'beta' + + log.step(`构建 DWEB 版本 (${channel})`) cleanDir(DIST_DWEB_DIR) cleanDir(DISTS_DIR) - // 使用 SERVICE_IMPL=dweb 构建 - exec('pnpm build:dweb', { - env: { - SERVICE_IMPL: 'dweb', - }, - }) + // 如果是 dev 版本,先获取版本号 + if (isDev && process.argv.includes('--upload')) { + const sftpUrl = process.env.DWEB_SFTP_URL || 'sftp://iweb.xin:22022' + const sftpUser = process.env.DWEB_SFTP_USER_DEV || process.env.DWEB_SFTP_USER + const sftpPass = process.env.DWEB_SFTP_PASS_DEV || process.env.DWEB_SFTP_PASS + + if (sftpUser && sftpPass) { + try { + const baseVersion = getVersion() + const info = await getNextDevVersion({ url: sftpUrl, username: sftpUser, password: sftpPass }, baseVersion) + devVersionInfo = { version: info.version, dateDir: info.dateDir } + log.info(`Dev 版本号: ${devVersionInfo.version}`) + } catch (error) { + log.warn(`获取 dev 版本号失败: ${error},使用默认版本号`) + } + } + } - // 移动到 dist-dweb - if (existsSync(DIST_DIR)) { - copyDir(DIST_DIR, DIST_DWEB_DIR) - rmSync(DIST_DIR, { recursive: true, force: true }) + // 如果是 dev 版本,修改 manifest.json + const manifestPath = join(ROOT, 'manifest.json') + const manifestBackupPath = join(ROOT, 'manifest.json.bak') + let manifestModified = false + + if (isDev && existsSync(manifestPath)) { + const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) + // 备份原始 manifest + writeFileSync(manifestBackupPath, JSON.stringify(manifest, null, 2)) + + // 修改 id 添加 dev 前缀 + const originalId = manifest.id + manifest.id = `dev.${originalId}` + + // 修改版本号 + if (devVersionInfo) { + manifest.version = devVersionInfo.version + } + + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)) + manifestModified = true + log.info(`manifest.json 已修改: id=${manifest.id}, version=${manifest.version}`) } - // 运行 plaoc bundle 打包 - log.step('运行 Plaoc 打包') try { - exec(`plaoc bundle "${DIST_DWEB_DIR}" -c ./ -o "${DISTS_DIR}"`) - log.success('Plaoc 打包完成') - } catch (error) { - log.warn('Plaoc 打包失败,可能未安装 plaoc CLI') - log.info('请安装: npm install -g @aspect/plaoc-cli') + // 使用 SERVICE_IMPL=dweb 构建,dev 模式添加水印标识 + exec('pnpm build:dweb', { + env: { + SERVICE_IMPL: 'dweb', + VITE_DEV_MODE: isDev ? 'true' : 'false', + }, + }) + + // 移动到 dist-dweb + if (existsSync(DIST_DIR)) { + copyDir(DIST_DIR, DIST_DWEB_DIR) + rmSync(DIST_DIR, { recursive: true, force: true }) + } + + // 运行 plaoc bundle 打包 + log.step('运行 Plaoc 打包') + try { + exec(`plaoc bundle "${DIST_DWEB_DIR}" -c ./ -o "${DISTS_DIR}"`) + log.success('Plaoc 打包完成') + } catch (error) { + log.warn('Plaoc 打包失败,可能未安装 plaoc CLI') + log.info('请安装: npm install -g @aspect/plaoc-cli') + } + } finally { + // 恢复原始 manifest.json + if (manifestModified && existsSync(manifestBackupPath)) { + cpSync(manifestBackupPath, manifestPath) + rmSync(manifestBackupPath) + log.info('manifest.json 已恢复') + } } log.success('DWEB 版本构建完成') @@ -204,7 +263,7 @@ async function prepareGhPages(webDir: string) { log.step('准备 GitHub Pages 部署') const channel = getChannel() - const webappDirName = channel === 'stable' ? 'webapp' : 'webapp-beta' + const webappDirName = channel === 'stable' ? 'webapp' : 'webapp-dev' // 将当前构建复制到 docs/public 目录,供 VitePress 使用 const docsPublicWebapp = join(ROOT, 'docs', 'public', webappDirName) @@ -282,21 +341,52 @@ async function uploadDweb() { return } - log.step('上传 DWEB 版本') + const channel = getChannel() + log.step(`上传 DWEB 版本 (${channel})`) + + // 检查环境变量(URL 有默认值) + const sftpUrl = process.env.DWEB_SFTP_URL || 'sftp://iweb.xin:22022' - // 检查环境变量 - const sftpUrl = process.env.DWEB_SFTP_URL - const sftpUser = process.env.DWEB_SFTP_USER - const sftpPass = process.env.DWEB_SFTP_PASS + // 根据渠道选择账号: + // - stable: DWEB_SFTP_USER / DWEB_SFTP_PASS (正式版账号) + // - beta: DWEB_SFTP_USER_DEV / DWEB_SFTP_PASS_DEV (开发版账号) + const sftpUser = channel === 'stable' ? process.env.DWEB_SFTP_USER : (process.env.DWEB_SFTP_USER_DEV || process.env.DWEB_SFTP_USER) + const sftpPass = channel === 'stable' ? process.env.DWEB_SFTP_PASS : (process.env.DWEB_SFTP_PASS_DEV || process.env.DWEB_SFTP_PASS) - if (!sftpUrl || !sftpUser || !sftpPass) { + if (!sftpUser || !sftpPass) { log.warn('未配置 SFTP 环境变量,跳过上传') - log.info('请设置: DWEB_SFTP_URL, DWEB_SFTP_USER, DWEB_SFTP_PASS') + log.info(channel === 'stable' ? '请设置: DWEB_SFTP_USER, DWEB_SFTP_PASS' : '请设置: DWEB_SFTP_USER_DEV, DWEB_SFTP_PASS_DEV') return } - // TODO: 实现 SFTP 上传 - log.warn('SFTP 上传功能待实现') + log.info(`SFTP 用户: ${sftpUser}`) + + // 确定上传目录:优先使用 plaoc 打包输出 (dists/),否则使用 dist-dweb + const uploadDir = existsSync(DISTS_DIR) && readdirSync(DISTS_DIR).length > 0 ? DISTS_DIR : DIST_DWEB_DIR + + if (!existsSync(uploadDir)) { + log.error(`上传目录不存在: ${uploadDir}`) + log.info('请先运行 dweb 构建') + return + } + + try { + // beta 渠道按日期分组上传 + const remoteSubDir = channel === 'beta' && devVersionInfo ? devVersionInfo.dateDir : undefined + + await uploadToSftp({ + url: sftpUrl, + username: sftpUser, + password: sftpPass, + sourceDir: uploadDir, + projectName: channel === 'beta' ? 'bfmpay-dweb-dev' : 'bfmpay-dweb', + remoteSubDir, + }) + log.success('DWEB 上传完成') + } catch (error) { + log.error(`DWEB 上传失败: ${error}`) + throw error + } } // ==================== 主程序 ==================== diff --git a/scripts/release.ts b/scripts/release.ts new file mode 100644 index 000000000..a3128eca8 --- /dev/null +++ b/scripts/release.ts @@ -0,0 +1,483 @@ +#!/usr/bin/env bun +/** + * BFM Pay 正式版发布脚本 + * + * 交互式脚本,完整发布流程: + * 1. 检查工作区状态 + * 2. 选择版本号(小版本/中版本/大版本/当前/自定义) + * 3. 运行类型检查和测试 + * 4. 构建 Web 和 DWEB 版本 + * 5. 上传 DWEB 到正式服务器 + * 6. 更新 package.json 和 manifest.json + * 7. 更新 CHANGELOG.md + * 8. 提交变更并打 tag + * 9. 推送触发 GitHub Pages 更新 + * + * Usage: + * pnpm release + */ + +import { execSync } from 'node:child_process' +import { existsSync, readFileSync, writeFileSync, cpSync, rmSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { confirm, select, input } from '@inquirer/prompts' +import semver from 'semver' + +// ==================== 配置 ==================== + +const ROOT = resolve(import.meta.dirname, '..') +const PACKAGE_JSON_PATH = join(ROOT, 'package.json') +const MANIFEST_JSON_PATH = join(ROOT, 'manifest.json') +const CHANGELOG_PATH = join(ROOT, 'CHANGELOG.md') + +// 颜色输出 +const colors = { + reset: '\x1b[0m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + bold: '\x1b[1m', +} + +const log = { + info: (msg: string) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`), + success: (msg: string) => console.log(`${colors.green}✓${colors.reset} ${msg}`), + warn: (msg: string) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), + error: (msg: string) => console.log(`${colors.red}✗${colors.reset} ${msg}`), + step: (msg: string) => console.log(`\n${colors.cyan}▸${colors.reset} ${colors.cyan}${msg}${colors.reset}`), +} + +// ==================== 工具函数 ==================== + +function exec(cmd: string, options?: { silent?: boolean; env?: Record