feat(devops): 发版拆分为 client/site/server 三条独立流水线
将单一 CI/CD 流水线拆成三条互不影响的发布流水线,各有 tag 前缀、独立版本
序列与独立 CHANGELOG:
- client(client-v*):Flutter 全平台 + version.yaml 自更新清单
- site(site-v*):web/ Eleventy 营销站,不含 web 版 app
- server(server-v*):backend Go 服务 + 共享基建 nginx/systemd
新增 3 个 workflow(deploy-client/site/server.yml)替换 deploy.yml;CI 脚本
按 part 拆分为 compile-/release-/deploy-{client,site,server}.sh,抽出公共函数
lib-forgejo.sh;compile-{macos,android,ios,windows}.sh 改去 client-v 前缀;
manual.yml 按前缀路由回滚。
跨流水线解耦:version.yaml 归 client,后端每请求实时读取(不重启、不触发
server 流水线);官网下载页的版本徽章/下载链接/更新日志时间线运行时经
/api/v1/public/release 动态拉取(API 不可达回退构建时静态内容)。为此一次性
扩展后端 changelog 字段(version.go/public.go)与 download.njk 动态渲染。
CHANGELOG.md 重命名为 CHANGELOG-client.md,新增 CHANGELOG-site/server.md;
重写 /release 命令为 /release <part> [version];同步更新 CLAUDE.md 与部署文档。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+46
-36
@@ -1,17 +1,26 @@
|
||||
---
|
||||
description: 执行本地发版流程(build → test → CHANGELOG → commit → tag → push)
|
||||
argument-hint: <version> 如 1.0.22
|
||||
description: 执行某一条流水线的本地发版(part ∈ client|site|server)
|
||||
argument-hint: <part> [version] 如 `client 1.0.55` 或 `server`
|
||||
model: claude-sonnet-4-6
|
||||
allowed-tools: Bash, Read, Edit, Write
|
||||
---
|
||||
|
||||
执行本地发版流程。
|
||||
执行三条独立流水线之一的本地发版流程。`$ARGUMENTS` 形如 `<part> [version]`:
|
||||
|
||||
**确定版本号:**
|
||||
- 如果 `$ARGUMENTS` 非空,使用它作为版本号(VERSION)。
|
||||
- 如果 `$ARGUMENTS` 为空,运行 `git describe --tags --abbrev=0` 获取最新 tag(如 `v1.0.20`),去掉 `v` 前缀后将 patch 号加 1(如 `1.0.21`),作为版本号(VERSION)。将自动确定的版本号告知用户。
|
||||
- **part**(必填):`client` | `site` | `server`,分别对应 tag 前缀 `client-v*` / `site-v*` / `server-v*`、CHANGELOG 文件 `CHANGELOG-client.md` / `CHANGELOG-site.md` / `CHANGELOG-server.md`。
|
||||
- **version**(选填):如 `1.0.55`。
|
||||
|
||||
后续所有步骤中,将确定好的版本号记为 VERSION。
|
||||
**三者互不影响**:client 发版只动 app 产物与 `version.yaml`(官网下载页与后端 `/version` 自动同步,无需重建官网/重启后端);site 只重建营销站;server 只换后端二进制 + nginx。
|
||||
|
||||
## 0. 解析参数
|
||||
|
||||
- 校验 part 合法(client|site|server),非法立即报错退出。
|
||||
- 记 PART = part。
|
||||
|
||||
**确定版本号 VERSION:**
|
||||
- 给了 version:直接用。
|
||||
- 没给:`git describe --tags --match "<PART>-v*" --abbrev=0` 取该前缀最新 tag(如 `client-v1.0.54`),去掉 `<PART>-v` 前缀后 patch +1(如 `1.0.55`)。若该前缀尚无任何 tag,则 VERSION=`1.0.0`。将自动确定的版本号告知用户。
|
||||
- 记 TAG = `<PART>-v<VERSION>`,CHANGELOG_FILE = `CHANGELOG-<PART>.md`。
|
||||
|
||||
按以下步骤顺序执行,任意步骤失败立即停止并报告错误。
|
||||
|
||||
@@ -19,27 +28,31 @@ allowed-tools: Bash, Read, Edit, Write
|
||||
|
||||
检查是否有未提交的改动(`git status`)。如果有,列出文件并询问用户是否继续。
|
||||
|
||||
## 2. 本地构建
|
||||
## 2. 本地构建 + 测试门禁(按 part)
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
cd backend && go build ./...
|
||||
```
|
||||
|
||||
## 3. 运行测试
|
||||
- **server**:
|
||||
```bash
|
||||
cd backend && go build ./... && go vet ./... && go test ./...
|
||||
```
|
||||
- **client**:
|
||||
```bash
|
||||
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
```
|
||||
- **site**:
|
||||
```bash
|
||||
cd web && npm ci && npm run build
|
||||
```
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
cd backend && go test ./...
|
||||
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
```
|
||||
## 3. 更新 CHANGELOG_FILE
|
||||
|
||||
## 4. 更新 CHANGELOG.md
|
||||
|
||||
检查 CHANGELOG.md 中是否已有 `## [VERSION]` 版本节。
|
||||
检查 CHANGELOG_FILE 中是否已有 `## [VERSION]` 版本节。
|
||||
|
||||
- **已有**:直接使用,不修改。
|
||||
- **没有**:读取 `git log` 从上一个 tag 到 HEAD 的提交记录,自动生成一个版本节插入到文件顶部(位于已有版本节之前),格式如下:
|
||||
- **没有**:读取该 part 上一个 tag(`<PART>-v*`)到 HEAD 的 `git log`,自动生成版本节插入文件顶部(位于已有版本节之前),格式:
|
||||
|
||||
```
|
||||
## [VERSION] - <今天日期>
|
||||
@@ -54,36 +67,33 @@ cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
- ...
|
||||
```
|
||||
|
||||
只保留有内容的分类(新功能/改进/修复),根据 commit message 的类型(feat/fix/refactor 等)归类。
|
||||
只保留有内容的分类,按 commit 类型(feat/fix/refactor 等)归类。
|
||||
|
||||
**CHANGELOG 撰写原则:**
|
||||
- 站在**用户视角**总结,描述对使用体验的实际影响,而非技术实现
|
||||
- **忽略**以下类型的提交(产品感知弱):
|
||||
- 发版工具、CI/CD、部署脚本相关(chore: release、ci:、devops:)
|
||||
- 内部文档、开发规范、CLAUDE.md 更新(docs(claude):、docs(internal):)
|
||||
- 代码格式、lint、重构(style:、refactor: 纯内部)
|
||||
- 依赖升级、版本号 bump(chore: bump、chore: upgrade)
|
||||
- **保留并重点描述**:feat、fix、perf 类,以及用户能感知到的 refactor(如界面改版、交互优化)
|
||||
- 每条描述用一句话说清楚"用户能做什么"或"修了什么问题",不堆砌技术术语
|
||||
- 站在**用户视角**总结,描述对使用体验的实际影响,而非技术实现。
|
||||
- **忽略**产品感知弱的提交:发版/CI/部署(chore: release、ci:、devops:)、内部文档/规范(docs(claude):、docs(internal):)、纯格式/重构(style:、refactor:)、依赖升级(chore: bump)。
|
||||
- **保留并重点描述** feat / fix / perf,以及用户可感知的 refactor(界面改版、交互优化)。
|
||||
- 每条一句话说清"用户能做什么"或"修了什么问题"。
|
||||
- **范围按 part 过滤**:client 只收 `client/` 相关;server 只收 `backend/` 相关;site 只收 `web/`(营销站)相关。
|
||||
|
||||
## 5. 提交代码
|
||||
## 4. 提交代码
|
||||
|
||||
将所有改动(包括 CHANGELOG.md)用以下格式提交到 main:
|
||||
将所有改动(含 CHANGELOG_FILE)提交到 main:
|
||||
|
||||
```
|
||||
chore: release vVERSION
|
||||
chore: release <PART>-v<VERSION>
|
||||
```
|
||||
|
||||
## 6. 打 tag
|
||||
## 5. 打 tag
|
||||
|
||||
```bash
|
||||
git tag vVERSION
|
||||
git tag <PART>-v<VERSION>
|
||||
```
|
||||
|
||||
## 7. 推送
|
||||
## 6. 推送
|
||||
|
||||
```bash
|
||||
git push ssh://git@git.51yanmei.com:2222/wangjia/jiu.git main vVERSION
|
||||
git push ssh://git@git.51yanmei.com:2222/wangjia/jiu.git main <PART>-v<VERSION>
|
||||
```
|
||||
|
||||
推送成功后提示用户:CI/CD 已触发,等待 Telegram 通知。
|
||||
推送成功后提示用户:对应流水线(deploy-<PART>)已触发,等待 Telegram 通知。
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
name: Deploy
|
||||
name: Deploy Client
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]*.[0-9]*.[0-9]*'
|
||||
- 'client-v[0-9]*.[0-9]*.[0-9]*'
|
||||
|
||||
concurrency:
|
||||
group: deploy-client
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-linux-web:
|
||||
build-client-web:
|
||||
runs-on: mac
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
@@ -19,19 +23,17 @@ jobs:
|
||||
- name: Provision (idempotent — auto-runs if host not initialized)
|
||||
run: sh scripts/ci/provision-mac.sh
|
||||
|
||||
- name: Compile (Linux backend + Flutter Web)
|
||||
run: sh scripts/ci/compile.sh "${{ gitea.ref_name }}"
|
||||
- name: Compile (Flutter Web)
|
||||
run: sh scripts/ci/compile-client-web.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Upload linux-web artifacts
|
||||
- name: Upload web artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: linux-web
|
||||
name: client-web
|
||||
path: dist/
|
||||
|
||||
build-macos:
|
||||
# Same physical runner as build-linux-web (label: mac, capacity 1). Run them
|
||||
# serially via needs so the second job doesn't sit queued and time out.
|
||||
needs: build-linux-web
|
||||
needs: build-client-web
|
||||
runs-on: mac
|
||||
env:
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
@@ -53,9 +55,6 @@ jobs:
|
||||
path: dist/
|
||||
|
||||
build-android:
|
||||
# Same physical mac runner (label: mac, capacity 1) — it already has the
|
||||
# Android SDK. Serialize after build-macos via needs so it doesn't sit
|
||||
# queued and time out.
|
||||
needs: build-macos
|
||||
runs-on: mac
|
||||
env:
|
||||
@@ -83,10 +82,6 @@ jobs:
|
||||
path: dist/
|
||||
|
||||
build-ios:
|
||||
# Same physical mac runner (label: mac, capacity 1) with Xcode. Serialize
|
||||
# after build-android. Builds a signed IPA and uploads to TestFlight; if the
|
||||
# iOS signing secrets are absent the script skips gracefully (exit 0), so this
|
||||
# job is safe to keep before the Apple Developer account is configured.
|
||||
needs: build-android
|
||||
runs-on: mac
|
||||
env:
|
||||
@@ -134,8 +129,8 @@ jobs:
|
||||
name: windows
|
||||
path: dist/
|
||||
|
||||
release-deploy:
|
||||
needs: [build-linux-web, build-macos, build-android, build-ios, build-windows]
|
||||
release-deploy-client:
|
||||
needs: [build-client-web, build-macos, build-android, build-ios, build-windows]
|
||||
runs-on: mac
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
@@ -145,10 +140,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download linux-web artifacts
|
||||
- name: Download web artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux-web
|
||||
name: client-web
|
||||
path: dist/
|
||||
|
||||
- name: Download macos artifacts
|
||||
@@ -169,15 +164,15 @@ jobs:
|
||||
name: windows
|
||||
path: dist/
|
||||
|
||||
- name: Test
|
||||
run: sh scripts/ci/test.sh
|
||||
- name: Test (flutter analyze)
|
||||
run: sh scripts/ci/test.sh client
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
run: sh scripts/ci/release.sh "${{ gitea.ref_name }}"
|
||||
run: sh scripts/ci/release-client.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
@@ -187,7 +182,7 @@ jobs:
|
||||
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
|
||||
EC2_HOST: ${{ secrets.EC2_HOST }}
|
||||
EC2_USER: ${{ secrets.EC2_USER }}
|
||||
run: sh scripts/ci/deploy.sh "${{ gitea.ref_name }}"
|
||||
run: sh scripts/ci/deploy-client.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Notify
|
||||
if: always()
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Deploy Server
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'server-v[0-9]*.[0-9]*.[0-9]*'
|
||||
|
||||
concurrency:
|
||||
group: deploy-server
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release-deploy-server:
|
||||
runs-on: mac
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Provision (idempotent — auto-runs if host not initialized)
|
||||
run: sh scripts/ci/provision-mac.sh
|
||||
|
||||
- name: Compile (Linux backend + configs)
|
||||
run: sh scripts/ci/compile-backend.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Test (go test)
|
||||
run: sh scripts/ci/test.sh server
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
run: sh scripts/ci/release-server.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
|
||||
EC2_HOST: ${{ secrets.EC2_HOST }}
|
||||
EC2_USER: ${{ secrets.EC2_USER }}
|
||||
run: sh scripts/ci/deploy-server.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Notify
|
||||
if: always()
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
run: sh scripts/ci/notify.sh "${{ gitea.ref_name }}" "${{ job.status }}"
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Deploy Site
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'site-v[0-9]*.[0-9]*.[0-9]*'
|
||||
|
||||
concurrency:
|
||||
group: deploy-site
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release-deploy-site:
|
||||
runs-on: mac
|
||||
env:
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Provision (idempotent — auto-runs if host not initialized)
|
||||
run: sh scripts/ci/provision-mac.sh
|
||||
|
||||
- name: Compile (Eleventy marketing site)
|
||||
run: sh scripts/ci/compile-site.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
run: sh scripts/ci/release-site.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Deploy
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
|
||||
EC2_HOST: ${{ secrets.EC2_HOST }}
|
||||
EC2_USER: ${{ secrets.EC2_USER }}
|
||||
run: sh scripts/ci/deploy-site.sh "${{ gitea.ref_name }}"
|
||||
|
||||
- name: Notify
|
||||
if: always()
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
run: sh scripts/ci/notify.sh "${{ gitea.ref_name }}" "${{ job.status }}"
|
||||
@@ -1,10 +1,16 @@
|
||||
name: Manual Deploy
|
||||
|
||||
# Re-deploy a previously released tag. The tag prefix selects which part:
|
||||
# client-v* -> deploy-client.sh (web app + installers + version.yaml)
|
||||
# site-v* -> deploy-site.sh (marketing site)
|
||||
# server-v* -> deploy-server.sh (backend binary + nginx)
|
||||
# Each deploy script downloads its assets from the matching Forgejo Release.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Tag to deploy (e.g. v1.0.0)'
|
||||
description: 'Prefixed tag to deploy (e.g. client-v1.0.55 / site-v1.0.0 / server-v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -17,7 +23,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.version }}
|
||||
|
||||
- name: Deploy
|
||||
- name: Deploy (routed by tag prefix)
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
@@ -25,4 +31,18 @@ jobs:
|
||||
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
|
||||
EC2_HOST: ${{ secrets.EC2_HOST }}
|
||||
EC2_USER: ${{ secrets.EC2_USER }}
|
||||
run: sh scripts/ci/deploy.sh "${{ inputs.version }}"
|
||||
run: |
|
||||
TAG="${{ inputs.version }}"
|
||||
case "$TAG" in
|
||||
client-v*) sh scripts/ci/deploy-client.sh "$TAG" ;;
|
||||
site-v*) sh scripts/ci/deploy-site.sh "$TAG" ;;
|
||||
server-v*) sh scripts/ci/deploy-server.sh "$TAG" ;;
|
||||
*) echo "Unknown tag prefix: $TAG (expected client-v* / site-v* / server-v*)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Notify
|
||||
if: always()
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
run: sh scripts/ci/notify.sh "${{ inputs.version }}" "${{ job.status }}"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Changelog — Server(后端)
|
||||
|
||||
后端服务(`backend/`,`jiu-server`)的更新记录。tag 前缀 `server-v*`。
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2026-06-17
|
||||
|
||||
### 新功能
|
||||
- 后端服务独立发版流水线启用(`server-v*`),与客户端、官网互不影响
|
||||
@@ -0,0 +1,12 @@
|
||||
# Changelog — Site(官网)
|
||||
|
||||
营销宣传站(`web/`,Eleventy)的更新记录。tag 前缀 `site-v*`。
|
||||
**不含** Web 版 app(Web 版 app 属于客户端,见 `CHANGELOG-client.md`)。
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2026-06-17
|
||||
|
||||
### 新功能
|
||||
- 官网独立发版流水线启用(`site-v*`),与客户端、后端互不影响
|
||||
@@ -261,22 +261,36 @@ cd client && flutter test
|
||||
|
||||
### 发版流程
|
||||
|
||||
使用 `/release <version>` slash command:
|
||||
**发版拆成三条互不影响的流水线**,各有 tag 前缀、独立版本序列、独立 CHANGELOG:
|
||||
|
||||
| part | 范围 | tag 前缀 | CHANGELOG | workflow |
|
||||
|------|------|---------|-----------|----------|
|
||||
| **client** | `client/` Flutter 全平台(Web→`/app`、macOS、Windows、Android、iOS)+ 应用自更新清单 `version.yaml` | `client-v*` | `CHANGELOG-client.md` | `deploy-client.yml` |
|
||||
| **site** | `web/` Eleventy 营销宣传站(→`/opt/jiu/marketing`,nginx `/`)。**不含** Web 版 app | `site-v*` | `CHANGELOG-site.md` | `deploy-site.yml` |
|
||||
| **server** | `backend/` Go 服务(`jiu-server`)+ 共享基建 nginx-jiu.conf / jiu.service | `server-v*` | `CHANGELOG-server.md` | `deploy-server.yml` |
|
||||
|
||||
使用 `/release <part> [version]` slash command:
|
||||
|
||||
```
|
||||
/release 1.0.2
|
||||
/release client 1.0.55 # 指定版本
|
||||
/release server # 省略则自增 server-v* 最新 tag 的 patch
|
||||
```
|
||||
|
||||
执行顺序:本地 build → test → 更新 CHANGELOG.md → git commit → tag → push main+tag。
|
||||
CI/CD(Forgejo,`.gitea/workflows/deploy.yml`)收到 tag 后自动:编译 → 测试 → 创建 Release → 部署 EC2 → Telegram 通知。
|
||||
执行顺序:本地 build → test(按 part)→ 更新对应 CHANGELOG → git commit → tag `<part>-v<ver>` → push main+tag。
|
||||
CI/CD(Forgejo)按 tag 前缀触发对应 workflow,自动:编译 → 测试 → 创建 Release → 部署 EC2 → Telegram 通知。
|
||||
|
||||
**多平台构建矩阵**(脚本在 `scripts/ci/`):
|
||||
- mac runner(容量 1)串行链:`build-linux-web`(后端+Web+官网)→ `build-macos` → `build-android` → `build-ios` → `release-deploy`
|
||||
- windows runner 并行:`build-windows`(Inno Setup 打包 setup.exe)
|
||||
- 分发:桌面(Win/macOS) 发布到下载页 `/downloads/` + 应用内更新;**Android** APK 同样挂 `/downloads/`,需 `ANDROID_*` 签名 secrets(见 `docs/android-signing.md`);**iOS** 走 TestFlight,需 `IOS_*` / `APPSTORE_*` secrets(见 `docs/ios-signing.md`)
|
||||
- Android/iOS 的 job 在对应 secrets 未配置时**优雅跳过**(exit 0,不阻塞发版);本地无 `client/android/key.properties` 时 APK 自动回退 debug 签名
|
||||
**归属与解耦**(关键):
|
||||
- `version.yaml` 归 **client**(写 version/build_number/release_notes/下载链接/changelog);nginx/systemd 归 **server**。
|
||||
- 后端 `/version` 与 `/api/v1/public/release` **每请求实时读** `version.yaml`,无缓存——client 部署 version.yaml 后立即生效,**不重启后端、不触发 server 流水线**。
|
||||
- 官网下载页运行时 `fetch('/api/v1/public/release')`:版本徽章、各平台下载链接、**更新日志时间线**全部动态刷新——client 发版**无需重建官网**。接口不可达时回退到构建时静态内容(`web/_data/changelog.js` 读 `CHANGELOG-client.md`)。
|
||||
|
||||
**CHANGELOG 格式**(Keep a Changelog):
|
||||
**CI 脚本**(`scripts/ci/`):`lib-forgejo.sh`(公共函数)+ `compile-{client-web,site,backend,macos,android,ios,windows}.sh` + `release-{client,site,server}.sh` + `deploy-{client,site,server}.sh`。`compile-{macos,android,ios,windows}.sh` 去 `client-v` 前缀取版本。
|
||||
|
||||
**client 多平台构建矩阵**:mac runner(容量 1)串行链 `build-client-web → build-macos → build-android → build-ios`;windows runner 并行 `build-windows`(Inno Setup);`release-deploy-client` 收齐后发布。Android 需 `ANDROID_*`、iOS 走 TestFlight 需 `IOS_*`/`APPSTORE_*` secrets,未配置时该 job 优雅跳过(exit 0,不阻塞)。
|
||||
|
||||
**手动回滚**:`manual.yml` 输入带前缀 tag(如 `client-v1.0.54`),按前缀路由到对应 `deploy-<part>.sh`,从 Forgejo Release 下载产物。
|
||||
|
||||
**CHANGELOG 格式**(Keep a Changelog,三个文件同构):
|
||||
```markdown
|
||||
## [1.0.2] - YYYY-MM-DD
|
||||
|
||||
@@ -289,7 +303,7 @@ CI/CD(Forgejo,`.gitea/workflows/deploy.yml`)收到 tag 后自动:编译
|
||||
### 修复
|
||||
- ...
|
||||
```
|
||||
只保留有内容的分类。`/release` 会检查 CHANGELOG 是否已有该版本节,没有则从 git log 自动生成。
|
||||
只保留有内容的分类。`/release` 会检查对应 CHANGELOG 是否已有该版本节,没有则从该 part 上一个 tag 的 git log 自动生成。
|
||||
|
||||
### 异常上报
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ func (h *PublicHandler) GetRelease(c *gin.Context) {
|
||||
"version": "1.0.0",
|
||||
"release_notes": "",
|
||||
"download_urls": gin.H{"web": "https://jiu.51yanmei.com/app"},
|
||||
"changelog": []changelogEntry{},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -190,6 +191,7 @@ func (h *PublicHandler) GetRelease(c *gin.Context) {
|
||||
"version": cfg.Version,
|
||||
"release_notes": cfg.ReleaseNotes,
|
||||
"download_urls": cfg.DownloadURLs,
|
||||
"changelog": changelogOrEmpty(cfg.Changelog),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,29 @@ type versionConfig struct {
|
||||
ForceUpdate bool `yaml:"force_update"`
|
||||
ReleaseNotes string `yaml:"release_notes"`
|
||||
DownloadURLs map[string]string `yaml:"download_urls"`
|
||||
// Changelog 由 client 发版脚本从 CHANGELOG-client.md 写入(最近 3 条),
|
||||
// 供官网下载页运行时拉取渲染更新日志,无需重建官网。
|
||||
Changelog []changelogEntry `yaml:"changelog"`
|
||||
}
|
||||
|
||||
type changelogEntry struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
Date string `yaml:"date" json:"date"`
|
||||
Intro string `yaml:"intro" json:"intro"`
|
||||
Sections []changelogSection `yaml:"sections" json:"sections"`
|
||||
}
|
||||
|
||||
type changelogSection struct {
|
||||
Type string `yaml:"type" json:"type"`
|
||||
Items []string `yaml:"items" json:"items"`
|
||||
}
|
||||
|
||||
// changelogOrEmpty 保证响应里 changelog 始终是数组(而非 null),方便前端遍历。
|
||||
func changelogOrEmpty(c []changelogEntry) []changelogEntry {
|
||||
if c == nil {
|
||||
return []changelogEntry{}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// GetVersion GET /version
|
||||
@@ -33,6 +56,7 @@ func GetVersion(c *gin.Context) {
|
||||
"force_update": cfg.ForceUpdate,
|
||||
"release_notes": cfg.ReleaseNotes,
|
||||
"download_urls": cfg.DownloadURLs,
|
||||
"changelog": changelogOrEmpty(cfg.Changelog),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Runner 实际运行在**本机 Mac**(不是 NAS),通过 LaunchAgent 开机
|
||||
|
||||
### Workflow 标签
|
||||
|
||||
`.gitea/workflows/deploy.yml` 和 `ci.yml` 中使用 `runs-on: mac`,对应 runner label `mac:host`。
|
||||
`.gitea/workflows/deploy-{client,site,server}.yml` 中使用 `runs-on: mac`,对应 runner label `mac:host`。
|
||||
|
||||
### 检查 Runner 状态
|
||||
|
||||
@@ -89,19 +89,21 @@ kill $(pgrep -f act_runner_start.sh)
|
||||
|
||||
仓库根目录 `.gitea/workflows/` 下有三个 workflow 文件:
|
||||
|
||||
### `deploy.yml` — 主部署流程
|
||||
### `deploy-{client,site,server}.yml` — 三条独立部署流程
|
||||
|
||||
**触发条件:** push `v*.*.*` tag
|
||||
发版已拆成三条互不影响的流水线,按 tag 前缀触发:
|
||||
|
||||
**步骤:**
|
||||
1. `compile.sh` — 编译 Go 后端(linux/amd64)+ Flutter Web,打包到 `dist/`
|
||||
2. `test.sh` — `go test` + `flutter analyze`
|
||||
3. `release.sh` — 解析 CHANGELOG.md,更新 version.yaml,创建 Forgejo Release,上传 3 个 asset
|
||||
4. `deploy.sh` — SSH 到 EC2 部署,完成后发 Telegram 通知
|
||||
| workflow | 触发 tag | 范围 |
|
||||
|----------|---------|------|
|
||||
| `deploy-client.yml` | `client-v*.*.*` | Flutter 全平台 + version.yaml |
|
||||
| `deploy-site.yml` | `site-v*.*.*` | Eleventy 营销站 |
|
||||
| `deploy-server.yml` | `server-v*.*.*` | Go 后端 + nginx |
|
||||
|
||||
**步骤(各自):** `compile-<part>*.sh` 编译打包 → `test.sh <part>` 门禁 → `release-<part>.sh` 创建 Forgejo Release → `deploy-<part>.sh` SSH 到 EC2 部署 → Telegram 通知。公共函数在 `scripts/ci/lib-forgejo.sh`。
|
||||
|
||||
### `manual.yml` — 手动部署 / 回滚
|
||||
|
||||
**触发条件:** Forgejo UI 手动触发(workflow_dispatch),输入目标 tag(如 `v1.0.0`)
|
||||
**触发条件:** Forgejo UI 手动触发(workflow_dispatch),输入带前缀 tag(如 `client-v1.0.55`),按前缀路由到对应 `deploy-<part>.sh`
|
||||
|
||||
**步骤:** 只运行 `deploy.sh`,从 Forgejo Release 下载已有产物直接部署,跳过编译和测试。
|
||||
|
||||
|
||||
+1
-1
@@ -69,5 +69,5 @@ download_urls:
|
||||
## 工作原理
|
||||
|
||||
- `scripts/ci/compile-ios.sh`:建临时 keychain 导入证书、安装 profile、生成 ExportOptions(manual / app-store)、`flutter build ipa`、`xcrun altool --upload-app` 上传;CFBundleVersion 由版本号推导单调递增;缺 secrets 时跳过。
|
||||
- `.gitea/workflows/deploy.yml` 的 `build-ios` job:mac runner,串行于 build-android。
|
||||
- `.gitea/workflows/deploy-client.yml` 的 `build-ios` job:mac runner,串行于 build-android。
|
||||
- 工程:`client/ios`(bundle id `com.yanmei.jiu`,应用名「岩美酒库」)。
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
VER="${TAG#client-v}"
|
||||
|
||||
# versionCode 必须单调递增,否则 Android 无法覆盖升级安装。
|
||||
# 由版本号推导:major*10000 + minor*100 + patch(如 1.0.17 -> 10017)。
|
||||
@@ -43,7 +43,7 @@ cd client
|
||||
flutter build apk --release \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
"--dart-define=APP_VERSION=v${VER}"
|
||||
cd ..
|
||||
|
||||
# Locate the built APK (path differs across Flutter versions) and copy to dist/
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# compile-backend.sh <tag> — build the Linux backend binary + shared-infra
|
||||
# configs.tar.gz into dist/. Server pipeline (server-v*). Does NOT touch
|
||||
# version.yaml (that is client-owned) nor any Flutter/web artifact.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/_env.sh
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
echo "==> compile-backend: tag=${TAG}"
|
||||
|
||||
# Build Go backend (linux/amd64 for EC2)
|
||||
cd backend
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jiu-server .
|
||||
cd ..
|
||||
|
||||
mkdir -p dist
|
||||
mv backend/jiu-server dist/jiu-server
|
||||
|
||||
# Shared infrastructure (nginx/systemd/env/compose). version.yaml is NOT here —
|
||||
# it belongs to the client pipeline.
|
||||
tar -czf dist/configs.tar.gz \
|
||||
deploy/nginx-jiu.conf \
|
||||
deploy/jiu.service \
|
||||
deploy/production.env.template \
|
||||
deploy/setup-ec2.sh \
|
||||
deploy/docker-compose.yml \
|
||||
deploy/docker-compose.jiu.yml
|
||||
|
||||
echo "==> compile-backend: done — dist/ contents:"
|
||||
ls -lh dist/
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# compile-client-web.sh <tag> — build the Flutter Web app into dist/web.tar.gz.
|
||||
# Client pipeline (client-v*). Served at /app by nginx.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/_env.sh
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#client-v}"
|
||||
|
||||
echo "==> compile-client-web: version=${VER}"
|
||||
|
||||
# Sync Flutter pubspec version (BSD sed on macOS)
|
||||
sed -i '' "s/^version:.*/version: ${VER}+1/" client/pubspec.yaml
|
||||
|
||||
cd client
|
||||
flutter build web --release \
|
||||
--base-href=/app/ \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=v${VER}"
|
||||
cd ..
|
||||
|
||||
mkdir -p dist
|
||||
tar -czf dist/web.tar.gz -C client/build web
|
||||
|
||||
echo "==> compile-client-web: done — dist/ contents:"
|
||||
ls -lh dist/
|
||||
@@ -17,7 +17,7 @@ set -euo pipefail
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
VER="${TAG#client-v}"
|
||||
|
||||
# CFBundleVersion(build 号)必须单调递增,否则 TestFlight 拒绝重复上传。
|
||||
MAJOR="$(echo "$VER" | cut -d. -f1)"
|
||||
@@ -105,7 +105,7 @@ flutter build ipa --release \
|
||||
--export-options-plist="$WORK/ExportOptions.plist" \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
"--dart-define=APP_VERSION=v${VER}"
|
||||
cd ..
|
||||
|
||||
IPA="$(ls client/build/ios/ipa/*.ipa | head -1)"
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
VER="${TAG#client-v}"
|
||||
|
||||
echo "==> compile-macos: version=${VER}"
|
||||
|
||||
@@ -18,7 +18,7 @@ cd client
|
||||
flutter build macos --release \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
"--dart-define=APP_VERSION=v${VER}"
|
||||
cd ..
|
||||
|
||||
# Package .app bundle into zip
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# compile-site.sh <tag> — build the Eleventy marketing site into
|
||||
# dist/marketing.tar.gz. Site pipeline (site-v*). Marketing pages only —
|
||||
# the web version of the app is built by compile-client-web.sh.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/_env.sh
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
echo "==> compile-site: tag=${TAG}"
|
||||
|
||||
cd web
|
||||
npm ci --prefer-offline
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
mkdir -p dist
|
||||
tar -czf dist/marketing.tar.gz -C web/dist .
|
||||
|
||||
echo "==> compile-site: done — dist/ contents:"
|
||||
ls -lh dist/
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
VER="${TAG#client-v}"
|
||||
|
||||
echo "==> compile-windows: version=${VER}"
|
||||
|
||||
@@ -41,7 +41,7 @@ flutter create --platforms=windows . --project-name jiu_client
|
||||
flutter build windows --release \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
"--dart-define=APP_VERSION=v${VER}"
|
||||
popd > /dev/null
|
||||
|
||||
# Package the Release folder into a Windows installer with Inno Setup (ISCC).
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# compile.sh <tag> — build backend + Flutter web, package into dist/
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/_env.sh
|
||||
. "$(dirname "$0")/_env.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
|
||||
echo "==> compile: version=$VER"
|
||||
|
||||
# Sync Flutter pubspec version
|
||||
sed -i '' "s/^version:.*/version: ${VER}+1/" client/pubspec.yaml
|
||||
|
||||
# Build Go backend (linux/amd64 for EC2)
|
||||
cd backend
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jiu-server .
|
||||
cd ..
|
||||
|
||||
# Build Flutter Web
|
||||
cd client
|
||||
flutter build web --release \
|
||||
--base-href=/app/ \
|
||||
"--dart-define=BASE_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=PUBLIC_URL=https://jiu.51yanmei.com" \
|
||||
"--dart-define=APP_VERSION=${TAG}"
|
||||
cd ..
|
||||
|
||||
# Build marketing site
|
||||
cd web
|
||||
npm ci --prefer-offline
|
||||
npm run build
|
||||
cd ..
|
||||
|
||||
# Package artifacts
|
||||
mkdir -p dist
|
||||
mv backend/jiu-server dist/jiu-server
|
||||
tar -czf dist/web.tar.gz -C client/build web
|
||||
tar -czf dist/marketing.tar.gz -C web/dist .
|
||||
|
||||
echo "==> compile: done — dist/ contents:"
|
||||
ls -lh dist/
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-client.sh <tag> — deploy the Flutter web app, desktop/mobile installers,
|
||||
# and version.yaml to EC2. Does NOT restart the backend (version.yaml is read
|
||||
# per-request) nor touch nginx / the marketing site.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/lib-forgejo.sh
|
||||
. "$(dirname "$0")/lib-forgejo.sh"
|
||||
|
||||
TAG="$1"
|
||||
echo "==> deploy-client: tag=${TAG}"
|
||||
|
||||
if [ ! -f dist/web.tar.gz ] || [ ! -f dist/version.yaml ]; then
|
||||
download_release_assets "$TAG"
|
||||
fi
|
||||
|
||||
echo "==> deploy-client: dist/ contents:"
|
||||
ls -lh dist/
|
||||
|
||||
# Prefer the version.yaml released for this tag (carries bumped build_number +
|
||||
# changelog); fall back to the checkout copy only if the asset is absent.
|
||||
VERSION_YAML="dist/version.yaml"
|
||||
[ -f "$VERSION_YAML" ] || VERSION_YAML="backend/config/version.yaml"
|
||||
|
||||
rm -rf /tmp/jiu-web-new
|
||||
mkdir -p /tmp/jiu-web-new
|
||||
tar -xzf dist/web.tar.gz -C /tmp/jiu-web-new --strip-components=1
|
||||
|
||||
setup_ssh
|
||||
echo "==> deploy-client: uploading files to EC2"
|
||||
${SCP} "$VERSION_YAML" "${EC2_USER}@${EC2_HOST}:/tmp/version.yaml"
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||
/tmp/jiu-web-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/"
|
||||
|
||||
# Desktop / mobile installers (served from /downloads/ by nginx). Guarded —
|
||||
# may be absent in a partial manual deploy.
|
||||
[ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${EC2_USER}@${EC2_HOST}:/tmp/jiu-windows-x64-setup.exe" || true
|
||||
[ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${EC2_USER}@${EC2_HOST}:/tmp/jiu-macos-x64.zip" || true
|
||||
[ -f dist/jiu-android.apk ] && ${SCP} dist/jiu-android.apk "${EC2_USER}@${EC2_HOST}:/tmp/jiu-android.apk" || true
|
||||
|
||||
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
||||
set -e
|
||||
|
||||
# Update version config (WorkingDirectory=/opt/jiu reads config/version.yaml).
|
||||
# Backend re-reads it per request — no restart needed.
|
||||
mkdir -p /opt/jiu/config
|
||||
cp /tmp/version.yaml /opt/jiu/config/version.yaml
|
||||
|
||||
# Swap Flutter web app (atomic)
|
||||
rm -rf /opt/jiu/web-old
|
||||
mv /opt/jiu/web /opt/jiu/web-old 2>/dev/null || true
|
||||
mv /tmp/jiu-web-new /opt/jiu/web
|
||||
|
||||
# Publish installers to the nginx-served downloads dir; keep only the latest.
|
||||
mkdir -p /opt/jiu/downloads
|
||||
rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* /opt/jiu/downloads/jiu-android-* /opt/jiu/downloads/jiu-android.apk 2>/dev/null || true
|
||||
[ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true
|
||||
[ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true
|
||||
[ -f /tmp/jiu-android.apk ] && mv -f /tmp/jiu-android.apk /opt/jiu/downloads/ || true
|
||||
|
||||
echo "Client deploy complete!"
|
||||
ENDSSH
|
||||
|
||||
teardown_ssh
|
||||
rm -rf /tmp/jiu-web-new
|
||||
echo "==> deploy-client: done"
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-server.sh <tag> — deploy the backend binary + nginx config to EC2.
|
||||
# Two modes: uses dist/ built in the same job, or downloads the release assets
|
||||
# by tag (manual rollback). Does NOT touch the web app, marketing site, or
|
||||
# version.yaml.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/lib-forgejo.sh
|
||||
. "$(dirname "$0")/lib-forgejo.sh"
|
||||
|
||||
TAG="$1"
|
||||
echo "==> deploy-server: tag=${TAG}"
|
||||
|
||||
if [ ! -f dist/jiu-server ] || [ ! -f dist/configs.tar.gz ]; then
|
||||
download_release_assets "$TAG"
|
||||
fi
|
||||
|
||||
echo "==> deploy-server: dist/ contents:"
|
||||
ls -lh dist/
|
||||
|
||||
rm -rf /tmp/jiu-configs
|
||||
mkdir -p /tmp/jiu-configs
|
||||
tar -xzf dist/configs.tar.gz -C /tmp/jiu-configs
|
||||
|
||||
setup_ssh
|
||||
echo "==> deploy-server: uploading files to EC2"
|
||||
${SCP} dist/jiu-server "${EC2_USER}@${EC2_HOST}:/tmp/jiu-server"
|
||||
${SCP} /tmp/jiu-configs/deploy/nginx-jiu.conf "${EC2_USER}@${EC2_HOST}:/tmp/nginx-jiu.conf"
|
||||
|
||||
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
||||
set -e
|
||||
|
||||
# Replace backend binary
|
||||
sudo systemctl stop jiu
|
||||
cp /tmp/jiu-server /opt/jiu/backend/jiu-server
|
||||
chmod +x /opt/jiu/backend/jiu-server
|
||||
|
||||
# Start and health check
|
||||
sudo systemctl start jiu
|
||||
echo "Waiting for health check..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -f http://localhost:8080/health > /dev/null 2>&1; then
|
||||
echo "Health check passed"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
curl -f http://localhost:8080/health > /dev/null || { echo "Health check failed!"; exit 1; }
|
||||
|
||||
# Update jiu nginx config in the pangolin-edge reverse proxy (host-bind-mounted
|
||||
# conf.d; reload nginx inside the container).
|
||||
cp /tmp/nginx-jiu.conf /home/ec2-user/pangolin/edge/conf.d/jiu.conf
|
||||
docker exec pangolin-edge nginx -t && docker exec pangolin-edge nginx -s reload
|
||||
|
||||
echo "Server deploy complete!"
|
||||
ENDSSH
|
||||
|
||||
teardown_ssh
|
||||
rm -rf /tmp/jiu-configs
|
||||
echo "==> deploy-server: done"
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-site.sh <tag> — deploy the Eleventy marketing site to EC2
|
||||
# (/opt/jiu/marketing). Does NOT restart the backend or touch nginx.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/lib-forgejo.sh
|
||||
. "$(dirname "$0")/lib-forgejo.sh"
|
||||
|
||||
TAG="$1"
|
||||
echo "==> deploy-site: tag=${TAG}"
|
||||
|
||||
if [ ! -f dist/marketing.tar.gz ]; then
|
||||
download_release_assets "$TAG"
|
||||
fi
|
||||
|
||||
echo "==> deploy-site: dist/ contents:"
|
||||
ls -lh dist/
|
||||
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
mkdir -p /tmp/jiu-marketing-new
|
||||
tar -xzf dist/marketing.tar.gz -C /tmp/jiu-marketing-new
|
||||
|
||||
setup_ssh
|
||||
echo "==> deploy-site: uploading marketing site to EC2"
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||
/tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/"
|
||||
|
||||
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
||||
set -e
|
||||
mkdir -p /opt/jiu/marketing
|
||||
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
echo "Site deploy complete!"
|
||||
ENDSSH
|
||||
|
||||
teardown_ssh
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
echo "==> deploy-site: done"
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh <tag> — deploy release to EC2
|
||||
# Works in two modes:
|
||||
# 1. Automated (after compile.sh): uses dist/ built in the same job
|
||||
# 2. Manual/rollback: downloads assets from Forgejo Release by tag
|
||||
set -euo pipefail
|
||||
|
||||
TAG="$1"
|
||||
|
||||
echo "==> deploy: tag=${TAG}"
|
||||
|
||||
# Download from Forgejo if dist/ was not built in this job
|
||||
if [ ! -f dist/jiu-server ] || [ ! -f dist/web.tar.gz ] || [ ! -f dist/configs.tar.gz ] || [ ! -f dist/marketing.tar.gz ]; then
|
||||
echo "==> deploy: dist/ incomplete — downloading assets for ${TAG} from Forgejo"
|
||||
mkdir -p dist
|
||||
|
||||
curl -kf \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
"${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${TAG}" \
|
||||
-o /tmp/release-info.json
|
||||
|
||||
python3 - <<'PYEOF'
|
||||
import json, urllib.request, ssl, os, sys
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
with open('/tmp/release-info.json') as f:
|
||||
release = json.load(f)
|
||||
|
||||
token = os.environ['FORGEJO_TOKEN']
|
||||
forgejo_url = os.environ['FORGEJO_URL']
|
||||
repo = os.environ['GITEA_REPOSITORY']
|
||||
release_id = release['id']
|
||||
|
||||
for asset in release.get('assets', []):
|
||||
url = f"{forgejo_url}/api/v1/repos/{repo}/releases/{release_id}/assets/{asset['id']}"
|
||||
req = urllib.request.Request(url, headers={'Authorization': f'token {token}'})
|
||||
with urllib.request.urlopen(req, context=ctx) as resp:
|
||||
with open(f"dist/{asset['name']}", 'wb') as out:
|
||||
out.write(resp.read())
|
||||
print(f"Downloaded: {asset['name']}")
|
||||
PYEOF
|
||||
fi
|
||||
|
||||
echo "==> deploy: dist/ contents:"
|
||||
ls -lh dist/
|
||||
|
||||
# Extract configs (nginx, version.yaml, etc.)
|
||||
rm -rf /tmp/jiu-configs
|
||||
mkdir -p /tmp/jiu-configs
|
||||
tar -xzf dist/configs.tar.gz -C /tmp/jiu-configs
|
||||
|
||||
# Extract Flutter web build
|
||||
rm -rf /tmp/jiu-web-new
|
||||
mkdir -p /tmp/jiu-web-new
|
||||
tar -xzf dist/web.tar.gz -C /tmp/jiu-web-new --strip-components=1
|
||||
|
||||
# Extract marketing site
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
mkdir -p /tmp/jiu-marketing-new
|
||||
tar -xzf dist/marketing.tar.gz -C /tmp/jiu-marketing-new
|
||||
|
||||
# Setup SSH
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s' "${EC2_SSH_KEY}" > ~/.ssh/ec2_deploy.pem
|
||||
chmod 600 ~/.ssh/ec2_deploy.pem
|
||||
ssh-keyscan -H "${EC2_HOST}" >> ~/.ssh/known_hosts
|
||||
|
||||
SSH="ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no"
|
||||
SCP="scp -O -i ~/.ssh/ec2_deploy.pem"
|
||||
|
||||
echo "==> deploy: uploading files to EC2"
|
||||
|
||||
# Upload backend binary
|
||||
${SCP} dist/jiu-server "${EC2_USER}@${EC2_HOST}:/tmp/jiu-server"
|
||||
|
||||
# Upload version.yaml
|
||||
${SCP} /tmp/jiu-configs/backend/config/version.yaml "${EC2_USER}@${EC2_HOST}:/tmp/version.yaml"
|
||||
|
||||
# Upload nginx config
|
||||
${SCP} /tmp/jiu-configs/deploy/nginx-jiu.conf "${EC2_USER}@${EC2_HOST}:/tmp/nginx-jiu.conf"
|
||||
|
||||
# Upload Flutter web
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||
/tmp/jiu-web-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-web-new/"
|
||||
|
||||
# Upload marketing site (built by compile.sh)
|
||||
rsync -avz --delete -e "ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" \
|
||||
/tmp/jiu-marketing-new/ "${EC2_USER}@${EC2_HOST}:/tmp/jiu-marketing-new/"
|
||||
|
||||
# Upload desktop client installers (served from /downloads/ by nginx).
|
||||
# Guarded: may be absent in a partial manual deploy.
|
||||
[ -f dist/jiu-windows-x64-setup.exe ] && ${SCP} dist/jiu-windows-x64-setup.exe "${EC2_USER}@${EC2_HOST}:/tmp/jiu-windows-x64-setup.exe" || true
|
||||
[ -f dist/jiu-macos-x64.zip ] && ${SCP} dist/jiu-macos-x64.zip "${EC2_USER}@${EC2_HOST}:/tmp/jiu-macos-x64.zip" || true
|
||||
[ -f dist/jiu-android.apk ] && ${SCP} dist/jiu-android.apk "${EC2_USER}@${EC2_HOST}:/tmp/jiu-android.apk" || true
|
||||
|
||||
echo "==> deploy: running remote deploy"
|
||||
|
||||
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
|
||||
set -e
|
||||
|
||||
# Replace backend binary
|
||||
sudo systemctl stop jiu
|
||||
cp /tmp/jiu-server /opt/jiu/backend/jiu-server
|
||||
chmod +x /opt/jiu/backend/jiu-server
|
||||
|
||||
# Update version config(WorkingDirectory=/opt/jiu,读 config/version.yaml)
|
||||
mkdir -p /opt/jiu/config
|
||||
cp /tmp/version.yaml /opt/jiu/config/version.yaml
|
||||
|
||||
# Start and health check
|
||||
sudo systemctl start jiu
|
||||
echo "Waiting for health check..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -f http://localhost:8080/health > /dev/null 2>&1; then
|
||||
echo "Health check passed"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
curl -f http://localhost:8080/health > /dev/null || { echo "Health check failed!"; exit 1; }
|
||||
|
||||
# Swap Flutter web app (atomic)
|
||||
rm -rf /opt/jiu/web-old
|
||||
mv /opt/jiu/web /opt/jiu/web-old 2>/dev/null || true
|
||||
mv /tmp/jiu-web-new /opt/jiu/web
|
||||
|
||||
# Update marketing site
|
||||
mkdir -p /opt/jiu/marketing
|
||||
rsync -a --delete /tmp/jiu-marketing-new/ /opt/jiu/marketing/
|
||||
rm -rf /tmp/jiu-marketing-new
|
||||
|
||||
# Publish desktop client installers to the nginx-served downloads dir.
|
||||
# Keep only the latest version: clear old installers, then drop in the new ones.
|
||||
mkdir -p /opt/jiu/downloads
|
||||
rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* /opt/jiu/downloads/jiu-android-* /opt/jiu/downloads/jiu-android.apk 2>/dev/null || true
|
||||
[ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true
|
||||
[ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true
|
||||
[ -f /tmp/jiu-android.apk ] && mv -f /tmp/jiu-android.apk /opt/jiu/downloads/ || true
|
||||
|
||||
# Update jiu nginx config in the pangolin-edge reverse proxy.
|
||||
# nginx now runs inside the `pangolin-edge` container (host networking), not on
|
||||
# the host. Its /etc/nginx/conf.d is bind-mounted from the host dir below, so we
|
||||
# write the config there and reload nginx *inside the container*.
|
||||
cp /tmp/nginx-jiu.conf /home/ec2-user/pangolin/edge/conf.d/jiu.conf
|
||||
docker exec pangolin-edge nginx -t && docker exec pangolin-edge nginx -s reload
|
||||
|
||||
echo "Deploy complete!"
|
||||
ENDSSH
|
||||
|
||||
# Cleanup SSH key
|
||||
rm -f ~/.ssh/ec2_deploy.pem
|
||||
rm -rf /tmp/jiu-configs /tmp/jiu-web-new
|
||||
|
||||
echo "==> deploy: done"
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib-forgejo.sh — shared CI helpers for the three release pipelines
|
||||
# (client / site / server). `source` this from release-*.sh and deploy-*.sh.
|
||||
#
|
||||
# Provides:
|
||||
# ver_from_tag <tag> -> strips client-v|site-v|server-v|v prefix
|
||||
# extract_release_notes <file> -> prints the latest "## [x]" section body
|
||||
# create_release <tag> <body> -> POSTs a Forgejo Release, sets RELEASE_ID
|
||||
# upload_asset <file> -> uploads one asset (needs RELEASE_ID)
|
||||
# download_release_assets <tag> -> downloads all assets of a release into dist/
|
||||
# setup_ssh / teardown_ssh -> prepare/clean the EC2 deploy key (sets SSH/SCP)
|
||||
#
|
||||
# Requires env (where relevant): FORGEJO_URL, FORGEJO_TOKEN, GITEA_REPOSITORY,
|
||||
# EC2_SSH_KEY, EC2_HOST, EC2_USER.
|
||||
|
||||
# Strip the part-specific tag prefix, leaving a bare semver (1.2.3).
|
||||
ver_from_tag() {
|
||||
local tag="$1"
|
||||
tag="${tag#client-v}"
|
||||
tag="${tag#site-v}"
|
||||
tag="${tag#server-v}"
|
||||
tag="${tag#v}"
|
||||
printf '%s' "$tag"
|
||||
}
|
||||
|
||||
# Print the body (lines) of the most recent "## [ver] - date" section.
|
||||
extract_release_notes() {
|
||||
python3 - "$1" <<'PYEOF'
|
||||
import re, sys
|
||||
fn = sys.argv[1]
|
||||
try:
|
||||
with open(fn, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
parts = re.split(r'\n## ', '\n' + content)
|
||||
if len(parts) > 1:
|
||||
lines = parts[1].strip().split('\n')
|
||||
notes = []
|
||||
for line in lines[1:]:
|
||||
s = line.strip()
|
||||
if s.startswith('## '):
|
||||
break
|
||||
if s:
|
||||
notes.append(s)
|
||||
print('\n'.join(notes) if notes else lines[0])
|
||||
else:
|
||||
print('')
|
||||
except Exception:
|
||||
print('')
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# create_release <tag> <body_json_string> — sets global RELEASE_ID on success.
|
||||
create_release() {
|
||||
local tag="$1" body="$2" http_code resp
|
||||
echo "==> release: creating Forgejo Release ${tag}"
|
||||
http_code=$(curl -k -w "%{http_code}" -o /tmp/release_resp.json \
|
||||
-X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${tag}\",\"name\":\"Release ${tag}\",\"body\":${body},\"draft\":false,\"prerelease\":false}")
|
||||
resp=$(cat /tmp/release_resp.json)
|
||||
echo "==> release: API response (HTTP ${http_code}): ${resp}"
|
||||
if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
|
||||
echo "==> release: FAILED — HTTP ${http_code}" >&2
|
||||
return 1
|
||||
fi
|
||||
RELEASE_ID=$(python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" <<< "${resp}")
|
||||
echo "==> release: release_id=${RELEASE_ID}"
|
||||
}
|
||||
|
||||
# upload_asset <file> — requires RELEASE_ID.
|
||||
upload_asset() {
|
||||
local file="$1" code
|
||||
code=$(curl -k -w "%{http_code}" -o /tmp/upload_resp.json \
|
||||
-X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-F "attachment=@${file}")
|
||||
echo "==> release: uploaded ${file} (HTTP ${code}): $(cat /tmp/upload_resp.json)"
|
||||
if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then return 1; fi
|
||||
}
|
||||
|
||||
# download_release_assets <tag> — pull every asset of the release into dist/.
|
||||
download_release_assets() {
|
||||
local tag="$1"
|
||||
echo "==> deploy: downloading assets for ${tag} from Forgejo"
|
||||
mkdir -p dist
|
||||
curl -kf -H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
"${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/tags/${tag}" \
|
||||
-o /tmp/release-info.json
|
||||
python3 - <<'PYEOF'
|
||||
import json, urllib.request, ssl, os
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
rel = json.load(open('/tmp/release-info.json'))
|
||||
token = os.environ['FORGEJO_TOKEN']
|
||||
url = os.environ['FORGEJO_URL']
|
||||
repo = os.environ['GITEA_REPOSITORY']
|
||||
rid = rel['id']
|
||||
for a in rel.get('assets', []):
|
||||
u = f"{url}/api/v1/repos/{repo}/releases/{rid}/assets/{a['id']}"
|
||||
req = urllib.request.Request(u, headers={'Authorization': f'token {token}'})
|
||||
with urllib.request.urlopen(req, context=ctx) as r, open(f"dist/{a['name']}", 'wb') as o:
|
||||
o.write(r.read())
|
||||
print('Downloaded:', a['name'])
|
||||
PYEOF
|
||||
}
|
||||
|
||||
# setup_ssh — write the EC2 deploy key and export SSH / SCP commands.
|
||||
setup_ssh() {
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s' "${EC2_SSH_KEY}" > ~/.ssh/ec2_deploy.pem
|
||||
chmod 600 ~/.ssh/ec2_deploy.pem
|
||||
ssh-keyscan -H "${EC2_HOST}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
SSH="ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no"
|
||||
SCP="scp -O -i ~/.ssh/ec2_deploy.pem"
|
||||
}
|
||||
|
||||
teardown_ssh() {
|
||||
rm -f ~/.ssh/ec2_deploy.pem
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
# release-client.sh <tag> — client pipeline release.
|
||||
# 1. Update backend/config/version.yaml: version / build_number / release_notes
|
||||
# / macos+windows+android download URLs / changelog[] (latest 3 from
|
||||
# CHANGELOG-client.md).
|
||||
# 2. Create the Forgejo Release client-vX and upload the app artifacts +
|
||||
# version.yaml (so manual rollback can re-fetch the exact manifest).
|
||||
#
|
||||
# version.yaml is client-owned: the backend reads it per-request, so deploying
|
||||
# it does NOT require a backend restart or a server-pipeline run.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/lib-forgejo.sh
|
||||
. "$(dirname "$0")/lib-forgejo.sh"
|
||||
|
||||
TAG="$1"
|
||||
VER="$(ver_from_tag "$TAG")"
|
||||
echo "==> release-client: tag=${TAG} version=${VER}"
|
||||
|
||||
NOTES=$(extract_release_notes CHANGELOG-client.md)
|
||||
echo "==> release-client: release_notes=${NOTES}"
|
||||
|
||||
# --- Update version.yaml (scalars + download URLs) and append changelog[] ---
|
||||
python3 - "$VER" "$NOTES" "CHANGELOG-client.md" <<'PYEOF'
|
||||
import sys, re, json
|
||||
|
||||
ver = sys.argv[1]
|
||||
notes = sys.argv[2]
|
||||
changelog_file = sys.argv[3]
|
||||
|
||||
base = "https://jiu.51yanmei.com/downloads"
|
||||
mac_url = f"{base}/jiu-macos-x64.zip"
|
||||
win_url = f"{base}/jiu-windows-x64-setup.exe"
|
||||
android_url = f"{base}/jiu-android.apk"
|
||||
|
||||
# Read current version.yaml, dropping any existing trailing `changelog:` block
|
||||
# (changelog is always emitted last, so everything from it to EOF is regenerated).
|
||||
with open('backend/config/version.yaml', encoding='utf-8') as f:
|
||||
raw = f.read()
|
||||
raw = re.split(r'\nchangelog:', raw, maxsplit=1)[0].rstrip('\n') + '\n'
|
||||
|
||||
out = []
|
||||
for line in raw.splitlines(keepends=True):
|
||||
if line.startswith('version:'):
|
||||
out.append('version: "%s"\n' % ver)
|
||||
elif line.startswith('build_number:'):
|
||||
try:
|
||||
b = int(line.split(':', 1)[1].strip()) + 1
|
||||
except Exception:
|
||||
b = 1
|
||||
out.append('build_number: %d\n' % b)
|
||||
elif line.startswith('release_notes:'):
|
||||
safe = notes.replace('\\', '\\\\').replace('"', '\\"').replace('\n', ';')
|
||||
out.append('release_notes: "%s"\n' % safe)
|
||||
elif line.startswith(' macos:'):
|
||||
out.append(' macos: "%s"\n' % mac_url)
|
||||
elif line.startswith(' windows:'):
|
||||
out.append(' windows: "%s"\n' % win_url)
|
||||
elif line.startswith(' android:'):
|
||||
out.append(' android: "%s"\n' % android_url)
|
||||
else:
|
||||
out.append(line)
|
||||
|
||||
# Parse CHANGELOG-client.md -> latest 3 entries (same shape as web/_data/changelog.js)
|
||||
versions = []
|
||||
cur = None
|
||||
sec = None
|
||||
for line in open(changelog_file, encoding='utf-8'):
|
||||
line = line.rstrip('\n')
|
||||
m = re.match(r'^## \[([^\]]+)\] - (\d{4}-\d{2}-\d{2})', line)
|
||||
if m:
|
||||
if cur:
|
||||
versions.append(cur)
|
||||
if len(versions) >= 3:
|
||||
cur = None
|
||||
break
|
||||
cur = {'version': m.group(1), 'date': m.group(2), 'intro': '', 'sections': []}
|
||||
sec = None
|
||||
continue
|
||||
ms = re.match(r'^### (.+)', line)
|
||||
if ms and cur is not None:
|
||||
sec = {'type': ms.group(1).strip(), 'items': []}
|
||||
cur['sections'].append(sec)
|
||||
continue
|
||||
mi = re.match(r'^- (.+)', line)
|
||||
if mi and cur is not None and sec is not None:
|
||||
sec['items'].append(mi.group(1).strip())
|
||||
continue
|
||||
if line.strip() and cur is not None and sec is None and not line.startswith('#'):
|
||||
cur['intro'] = (cur['intro'] + ' ' + line.strip()).strip()
|
||||
if cur is not None and len(versions) < 3:
|
||||
versions.append(cur)
|
||||
|
||||
# Append changelog as a JSON array (valid YAML flow style — no pyyaml needed).
|
||||
text = ''.join(out).rstrip('\n') + '\n'
|
||||
text += 'changelog: ' + json.dumps(versions, ensure_ascii=False) + '\n'
|
||||
with open('backend/config/version.yaml', 'w', encoding='utf-8') as f:
|
||||
f.write(text)
|
||||
print('version.yaml updated to', ver, '|', len(versions), 'changelog entries')
|
||||
PYEOF
|
||||
|
||||
# Stage version.yaml as a release asset so manual rollback can re-fetch it.
|
||||
mkdir -p dist
|
||||
cp backend/config/version.yaml dist/version.yaml
|
||||
|
||||
BODY=$(python3 - "$TAG" "$NOTES" <<'PYEOF'
|
||||
import sys, json
|
||||
print(json.dumps('## ' + sys.argv[1] + '\n\n' + sys.argv[2]))
|
||||
PYEOF
|
||||
)
|
||||
|
||||
create_release "$TAG" "$BODY"
|
||||
upload_asset dist/web.tar.gz
|
||||
upload_asset dist/version.yaml
|
||||
[ -f dist/jiu-macos-x64.zip ] && upload_asset dist/jiu-macos-x64.zip || true
|
||||
[ -f dist/jiu-windows-x64-setup.exe ] && upload_asset dist/jiu-windows-x64-setup.exe || true
|
||||
[ -f dist/jiu-android.apk ] && upload_asset dist/jiu-android.apk || true
|
||||
|
||||
echo "==> release-client: done — Release ${TAG} created"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# release-server.sh <tag> — create the Forgejo Release for the backend pipeline
|
||||
# and upload jiu-server + configs.tar.gz.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/lib-forgejo.sh
|
||||
. "$(dirname "$0")/lib-forgejo.sh"
|
||||
|
||||
TAG="$1"
|
||||
echo "==> release-server: tag=${TAG}"
|
||||
|
||||
NOTES=$(extract_release_notes CHANGELOG-server.md)
|
||||
echo "==> release-server: release_notes=${NOTES}"
|
||||
|
||||
BODY=$(python3 - "$TAG" "$NOTES" <<'PYEOF'
|
||||
import sys, json
|
||||
print(json.dumps('## ' + sys.argv[1] + '\n\n' + sys.argv[2]))
|
||||
PYEOF
|
||||
)
|
||||
|
||||
create_release "$TAG" "$BODY"
|
||||
upload_asset dist/jiu-server
|
||||
upload_asset dist/configs.tar.gz
|
||||
|
||||
echo "==> release-server: done — Release ${TAG} created"
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# release-site.sh <tag> — create the Forgejo Release for the marketing-site
|
||||
# pipeline and upload marketing.tar.gz.
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=scripts/ci/lib-forgejo.sh
|
||||
. "$(dirname "$0")/lib-forgejo.sh"
|
||||
|
||||
TAG="$1"
|
||||
echo "==> release-site: tag=${TAG}"
|
||||
|
||||
NOTES=$(extract_release_notes CHANGELOG-site.md)
|
||||
echo "==> release-site: release_notes=${NOTES}"
|
||||
|
||||
BODY=$(python3 - "$TAG" "$NOTES" <<'PYEOF'
|
||||
import sys, json
|
||||
print(json.dumps('## ' + sys.argv[1] + '\n\n' + sys.argv[2]))
|
||||
PYEOF
|
||||
)
|
||||
|
||||
create_release "$TAG" "$BODY"
|
||||
upload_asset dist/marketing.tar.gz
|
||||
|
||||
echo "==> release-site: done — Release ${TAG} created"
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# release.sh <tag> — update version.yaml, package configs, create Forgejo Release
|
||||
set -euo pipefail
|
||||
|
||||
TAG="$1"
|
||||
VER="${TAG#v}"
|
||||
|
||||
echo "==> release: tag=${TAG} version=${VER}"
|
||||
|
||||
# Extract release notes from CHANGELOG.md (first summary line of the latest section)
|
||||
RELEASE_NOTES=$(python3 - <<'PYEOF'
|
||||
import re, sys
|
||||
try:
|
||||
with open('CHANGELOG.md') as f:
|
||||
content = f.read()
|
||||
parts = re.split(r'\n## ', '\n' + content)
|
||||
if len(parts) > 1:
|
||||
section = parts[1]
|
||||
lines = section.strip().split('\n')
|
||||
notes = []
|
||||
for line in lines[1:]:
|
||||
stripped = line.strip()
|
||||
# 只在遇到同级 ## 时停止,### 子标题保留
|
||||
if stripped.startswith('## '):
|
||||
break
|
||||
if stripped:
|
||||
notes.append(stripped)
|
||||
print('\n'.join(notes) if notes else lines[0])
|
||||
else:
|
||||
print('')
|
||||
except Exception as e:
|
||||
print('', file=sys.stderr)
|
||||
print('')
|
||||
PYEOF
|
||||
)
|
||||
|
||||
echo "==> release: release_notes=${RELEASE_NOTES}"
|
||||
|
||||
# Update backend/config/version.yaml (version, build_number, release_notes, macos/windows URL)
|
||||
python3 - "${VER}" "${RELEASE_NOTES}" "${TAG}" "${FORGEJO_URL}" "${GITEA_REPOSITORY}" <<'PYEOF'
|
||||
import sys
|
||||
ver = sys.argv[1]
|
||||
notes = sys.argv[2]
|
||||
tag = sys.argv[3]
|
||||
furl = sys.argv[4].rstrip('/')
|
||||
repo = sys.argv[5]
|
||||
# Public download URLs are served by jiu.51yanmei.com (same-origin HTTPS via the
|
||||
# pangolin nginx /downloads/ location), NOT the LAN-only HTTP Forgejo instance —
|
||||
# otherwise the HTTPS download page hands the browser an http://192.168.x URL and
|
||||
# Chrome blocks it as insecure / it's unreachable from the public internet.
|
||||
base = "https://jiu.51yanmei.com/downloads"
|
||||
mac_url = f"{base}/jiu-macos-x64.zip"
|
||||
win_url = f"{base}/jiu-windows-x64-setup.exe"
|
||||
android_url = f"{base}/jiu-android.apk"
|
||||
|
||||
lines = []
|
||||
with open('backend/config/version.yaml') as f:
|
||||
for line in f:
|
||||
if line.startswith('version:'):
|
||||
lines.append('version: "' + ver + '"\n')
|
||||
elif line.startswith('build_number:'):
|
||||
try:
|
||||
build = int(line.split(':', 1)[1].strip()) + 1
|
||||
except Exception:
|
||||
build = 1
|
||||
lines.append('build_number: ' + str(build) + '\n')
|
||||
elif line.startswith('release_notes:'):
|
||||
lines.append('release_notes: "' + notes.replace('\\', '\\\\').replace('"', '\\"') + '"\n')
|
||||
elif line.startswith(' macos:'):
|
||||
lines.append(' macos: "' + mac_url + '"\n')
|
||||
elif line.startswith(' windows:'):
|
||||
lines.append(' windows: "' + win_url + '"\n')
|
||||
elif line.startswith(' android:'):
|
||||
lines.append(' android: "' + android_url + '"\n')
|
||||
else:
|
||||
lines.append(line)
|
||||
with open('backend/config/version.yaml', 'w') as f:
|
||||
f.writelines(lines)
|
||||
print('version.yaml updated to', ver, '| macos:', mac_url, '| windows:', win_url, '| android:', android_url)
|
||||
PYEOF
|
||||
|
||||
# Package configs.tar.gz (includes updated version.yaml)
|
||||
tar -czf dist/configs.tar.gz \
|
||||
deploy/nginx-jiu.conf \
|
||||
deploy/jiu.service \
|
||||
deploy/production.env.template \
|
||||
deploy/setup-ec2.sh \
|
||||
deploy/docker-compose.yml \
|
||||
deploy/docker-compose.jiu.yml \
|
||||
backend/config/version.yaml
|
||||
|
||||
echo "==> release: creating Forgejo Release ${TAG}"
|
||||
|
||||
# Build release body from CHANGELOG excerpt
|
||||
BODY=$(python3 - "${TAG}" "${RELEASE_NOTES}" <<'PYEOF'
|
||||
import sys, json
|
||||
tag = sys.argv[1]
|
||||
notes = sys.argv[2]
|
||||
body = '## ' + tag + '\n\n' + notes
|
||||
print(json.dumps(body))
|
||||
PYEOF
|
||||
)
|
||||
|
||||
HTTP_CODE=$(curl -k -w "%{http_code}" -o /tmp/release_resp.json \
|
||||
-X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Release ${TAG}\",\"body\":${BODY},\"draft\":false,\"prerelease\":false}")
|
||||
|
||||
RESP=$(cat /tmp/release_resp.json)
|
||||
echo "==> release: API response (HTTP ${HTTP_CODE}): ${RESP}"
|
||||
|
||||
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
|
||||
echo "==> release: FAILED — HTTP ${HTTP_CODE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RELEASE_ID=$(python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" <<< "${RESP}")
|
||||
echo "==> release: release_id=${RELEASE_ID}"
|
||||
|
||||
# Upload assets
|
||||
upload_asset() {
|
||||
local file="$1"
|
||||
local code
|
||||
code=$(curl -k -w "%{http_code}" -o /tmp/upload_resp.json \
|
||||
-X POST "${FORGEJO_URL}/api/v1/repos/${GITEA_REPOSITORY}/releases/${RELEASE_ID}/assets" \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-F "attachment=@${file}")
|
||||
echo "==> release: uploaded ${file} (HTTP ${code}): $(cat /tmp/upload_resp.json)"
|
||||
if [ "$code" -lt 200 ] || [ "$code" -ge 300 ]; then exit 1; fi
|
||||
}
|
||||
|
||||
upload_asset dist/jiu-server
|
||||
upload_asset dist/web.tar.gz
|
||||
upload_asset dist/marketing.tar.gz
|
||||
upload_asset dist/configs.tar.gz
|
||||
upload_asset dist/jiu-macos-x64.zip
|
||||
upload_asset dist/jiu-windows-x64-setup.exe
|
||||
upload_asset dist/jiu-android.apk
|
||||
|
||||
echo "==> release: done — Release ${TAG} created"
|
||||
+26
-9
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# test.sh — run backend and frontend checks
|
||||
# test.sh [part] — run gate checks for a pipeline part.
|
||||
# server -> go test
|
||||
# client -> flutter analyze
|
||||
# (none) -> both (backwards compatible)
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
@@ -7,14 +10,28 @@ export GOPROXY="${GOPROXY:-https://goproxy.cn,direct}"
|
||||
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://pub.flutter-io.cn}"
|
||||
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://storage.flutter-io.cn}"
|
||||
|
||||
echo "==> test: go test"
|
||||
cd backend
|
||||
go test ./...
|
||||
cd ..
|
||||
PART="${1:-all}"
|
||||
|
||||
echo "==> test: flutter analyze"
|
||||
cd client
|
||||
flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
cd ..
|
||||
run_server() {
|
||||
echo "==> test: go test"
|
||||
cd backend
|
||||
go test ./...
|
||||
cd ..
|
||||
}
|
||||
|
||||
run_client() {
|
||||
echo "==> test: flutter analyze"
|
||||
cd client
|
||||
flutter analyze --no-fatal-infos --no-fatal-warnings
|
||||
cd ..
|
||||
}
|
||||
|
||||
case "$PART" in
|
||||
server) run_server ;;
|
||||
client) run_client ;;
|
||||
site) echo "==> test: site has no test gate (build is the gate)" ;;
|
||||
all) run_server; run_client ;;
|
||||
*) echo "unknown part: $PART" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "==> test: done"
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = function() {
|
||||
const raw = fs.readFileSync(path.join(__dirname, '../../CHANGELOG.md'), 'utf8');
|
||||
const raw = fs.readFileSync(path.join(__dirname, '../../CHANGELOG-client.md'), 'utf8');
|
||||
const lines = raw.split('\n');
|
||||
const versions = [];
|
||||
let current = null;
|
||||
|
||||
Vendored
+3
-4
@@ -72,7 +72,7 @@
|
||||
<div class="docs-version">
|
||||
<i data-lucide="tag" class="icon"></i>
|
||||
<span class="docs-version-label">版本</span>
|
||||
<span>v1.0.23</span>
|
||||
<span>v1.0.54</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -840,6 +840,7 @@
|
||||
<img src="/assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
|
||||
<div><a href="mailto:yammy2023@163.com">yammy2023@163.com</a></div>
|
||||
|
||||
</div>
|
||||
@@ -872,14 +873,12 @@
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
<li><a href="/terms/">服务条款</a></li>
|
||||
<li><a href="/privacy/">隐私政策</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<div>© 2026 岩美科技 · 保留所有权利</div>
|
||||
<div>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Vendored
+89
-66
@@ -308,7 +308,7 @@
|
||||
<div class="container ph-inner">
|
||||
<div>
|
||||
<div class="ph-eyebrow">
|
||||
<span class="version-badge">v1.0.23</span>
|
||||
<span class="version-badge">v1.0.54</span>
|
||||
<span>下载岩美</span>
|
||||
</div>
|
||||
<h1 class="ph-title">随时随地,<br/>轻松管理酒库。</h1>
|
||||
@@ -318,11 +318,11 @@
|
||||
<div class="ph-meta">
|
||||
<div class="ph-meta-item">
|
||||
<div class="ph-meta-label">最新版本</div>
|
||||
<div class="ph-meta-val version-badge">v1.0.23</div>
|
||||
<div class="ph-meta-val version-badge">v1.0.54</div>
|
||||
</div>
|
||||
<div class="ph-meta-item">
|
||||
<div class="ph-meta-label">发布日期</div>
|
||||
<div class="ph-meta-val">2026-06-07</div>
|
||||
<div class="ph-meta-val">2026-06-17</div>
|
||||
</div>
|
||||
<div class="ph-meta-item">
|
||||
<div class="ph-meta-label">可用平台</div>
|
||||
@@ -545,14 +545,14 @@
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2 class="fs-3xl fw-6 text-brand-900 m-0 mb-6" style="letter-spacing: var(--tracking-cn-display);">版本更新日志</h2>
|
||||
<p class="fs-md text-muted m-0">最近 3 个版本。</p>
|
||||
<p class="fs-md text-muted m-0" id="changelog-count">最近 3 个版本。</p>
|
||||
|
||||
<div class="changelog">
|
||||
<div class="changelog" id="changelog-list">
|
||||
|
||||
<div class="changelog-entry current">
|
||||
<div class="changelog-meta">
|
||||
<div class="changelog-version">v1.0.23</div>
|
||||
<div class="changelog-date">2026-06-07</div>
|
||||
<div class="changelog-version">v1.0.54</div>
|
||||
<div class="changelog-date">2026-06-17</div>
|
||||
<span class="changelog-tag">当前版本</span>
|
||||
</div>
|
||||
<div class="changelog-body">
|
||||
@@ -561,12 +561,16 @@
|
||||
<div class="changelog-cat">
|
||||
<div class="changelog-cat-label">
|
||||
|
||||
<span class="dot dot-fix"></span>问题修复
|
||||
<span class="dot dot-feature"></span>新功能
|
||||
|
||||
</div>
|
||||
<ul>
|
||||
|
||||
<li>官网功能详情页(库存管理/审核流)导航栏现与主站完全一致,不再缺少「库存」「审核流」入口</li>
|
||||
<li>只读账号自动隐藏所有写操作按钮(新增/编辑/删除/审核/提交/结清/导入等),顶栏显示「只读」标识,权限一目了然</li>
|
||||
|
||||
<li>左侧抽屉和系统设置页新增「退出登录」入口,窄屏也能方便退出</li>
|
||||
|
||||
<li>酒行信息新增「微信号」显示</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@@ -579,47 +583,9 @@
|
||||
</div>
|
||||
<ul>
|
||||
|
||||
<li>官网功能详情页接入统一模板系统,导航/页脚/版权信息改一处自动同步全站</li>
|
||||
<li>网络请求失败时自动重试(间隔递增),离线时提供带状态反馈的「重试」按钮</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="changelog-entry">
|
||||
<div class="changelog-meta">
|
||||
<div class="changelog-version">v1.0.22</div>
|
||||
<div class="changelog-date">2026-06-07</div>
|
||||
|
||||
</div>
|
||||
<div class="changelog-body">
|
||||
|
||||
|
||||
<div class="changelog-cat">
|
||||
<div class="changelog-cat-label">
|
||||
|
||||
<span class="dot dot-feature"></span>新功能
|
||||
|
||||
</div>
|
||||
<ul>
|
||||
|
||||
<li>官网移动端适配:手机浏览器下导航折叠、首页各区块单列排版、features 详情页和页脚自适应窄屏</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="changelog-cat">
|
||||
<div class="changelog-cat-label">
|
||||
|
||||
<span class="dot dot-improve"></span>改进
|
||||
|
||||
</div>
|
||||
<ul>
|
||||
|
||||
<li>客户端移动端布局优化:入库/出库详情页商品明细改为卡片式竖排,操作按钮收纳至溢出菜单,窄屏不再横向溢出</li>
|
||||
|
||||
<li>信息架构整理:仓库管理移入基础数据,系统设置新增授权 Tab 和数据管理组,关于我们补充帮助文档/法律信息/系统信息入口</li>
|
||||
<li>iOS 顶栏避开状态栏,菜单按钮不再被时间/信号遮挡</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@@ -632,17 +598,9 @@
|
||||
</div>
|
||||
<ul>
|
||||
|
||||
<li>官网所有功能页死链修正(`/download.html` → `/download/`,`/docs.html` → `/docs/`)</li>
|
||||
<li>库存导入改用 Excel 中的「入库日期」作为入库时间(不再统一记为导入当天);相同数据重复导入时自动跳过,仅更新有变化的字段,避免重复与误覆盖</li>
|
||||
|
||||
<li>官网删除未实现功能的虚假宣传(红冲、调拨单、库存推送通知、过期预警等),文案与实际功能对齐</li>
|
||||
|
||||
<li>官网客服邮箱更新为真实邮箱,移除假冒客服电话,页脚清除无目标死链</li>
|
||||
|
||||
<li>导航新增「库存」「审核流」功能页入口,首页模块卡加「了解更多」链接</li>
|
||||
|
||||
<li>下载页接口失败时按钮改为禁用态并提示,不再无响应</li>
|
||||
|
||||
<li>首页 iOS 平台说明改为「TestFlight 内测中」,与下载页状态一致</li>
|
||||
<li>修复 iOS 上顶栏被状态栏遮挡导致无法打开菜单/退出登录的问题</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@@ -652,8 +610,8 @@
|
||||
|
||||
<div class="changelog-entry">
|
||||
<div class="changelog-meta">
|
||||
<div class="changelog-version">v1.0.21</div>
|
||||
<div class="changelog-date">2026-06-07</div>
|
||||
<div class="changelog-version">v1.0.53</div>
|
||||
<div class="changelog-date">2026-06-15</div>
|
||||
|
||||
</div>
|
||||
<div class="changelog-body">
|
||||
@@ -662,12 +620,37 @@
|
||||
<div class="changelog-cat">
|
||||
<div class="changelog-cat-label">
|
||||
|
||||
<span class="dot dot-feature"></span>新功能
|
||||
<span class="dot dot-fix"></span>问题修复
|
||||
|
||||
</div>
|
||||
<ul>
|
||||
|
||||
<li>商品详情页添加照片时,手机端(Android/iOS)支持直接拍照或从相册选择</li>
|
||||
<li>修复 Windows 上点击打印无反应的问题(GBK 编码改为直接调用系统 API,不再依赖第三方插件)</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="changelog-entry">
|
||||
<div class="changelog-meta">
|
||||
<div class="changelog-version">v1.0.52</div>
|
||||
<div class="changelog-date">2026-06-15</div>
|
||||
|
||||
</div>
|
||||
<div class="changelog-body">
|
||||
|
||||
|
||||
<div class="changelog-cat">
|
||||
<div class="changelog-cat-label">
|
||||
|
||||
<span class="dot dot-improve"></span>改进
|
||||
|
||||
</div>
|
||||
<ul>
|
||||
|
||||
<li>热敏标签打印改用打印机内置中文点阵字体(TSS24.BF2/TSS16.BF2),文字清晰度大幅提升,彻底解决 203 DPI 下字体模糊问题</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@@ -747,6 +730,46 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 用 /api/v1/public/release 返回的 changelog 重渲染更新日志时间线,使客户端
|
||||
// 发版无需重建官网即可同步。结构与构建时 web/_data/changelog.js 一致;接口
|
||||
// 不可达或字段缺失时保留构建时静态时间线作 fallback。
|
||||
function renderChangelog(entries) {
|
||||
if (!Array.isArray(entries) || entries.length === 0) return;
|
||||
var dotClass = { '新功能': 'dot-feature', '改进': 'dot-improve', '修复': 'dot-fix' };
|
||||
var label = { '修复': '问题修复' };
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"]/g, function(c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
|
||||
});
|
||||
}
|
||||
var html = '';
|
||||
entries.forEach(function(entry, i) {
|
||||
var current = i === 0;
|
||||
html += '<div class="changelog-entry' + (current ? ' current' : '') + '">';
|
||||
html += '<div class="changelog-meta">';
|
||||
html += '<div class="changelog-version">v' + esc(entry.version) + '</div>';
|
||||
html += '<div class="changelog-date">' + esc(entry.date) + '</div>';
|
||||
if (current) html += '<span class="changelog-tag">当前版本</span>';
|
||||
html += '</div><div class="changelog-body">';
|
||||
if (entry.intro) html += '<p class="changelog-intro">' + esc(entry.intro) + '</p>';
|
||||
(entry.sections || []).forEach(function(section) {
|
||||
var dot = dotClass[section.type] || 'dot-improve';
|
||||
var name = label[section.type] || section.type;
|
||||
html += '<div class="changelog-cat"><div class="changelog-cat-label">';
|
||||
html += '<span class="dot ' + dot + '"></span>' + esc(name) + '</div><ul>';
|
||||
(section.items || []).forEach(function(item) {
|
||||
html += '<li>' + esc(item) + '</li>';
|
||||
});
|
||||
html += '</ul></div>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
});
|
||||
var list = document.getElementById('changelog-list');
|
||||
if (list) list.innerHTML = html;
|
||||
var count = document.getElementById('changelog-count');
|
||||
if (count) count.textContent = '最近 ' + entries.length + ' 个版本。';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var detected = detectPlatform();
|
||||
updateDetectCard(detected);
|
||||
@@ -756,6 +779,7 @@
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data && data.version) updateVersionBadges(data.version);
|
||||
if (data && data.changelog) renderChangelog(data.changelog);
|
||||
if (data && data.download_urls && data.download_urls.macos) {
|
||||
platforms.macos.btnHref = data.download_urls.macos;
|
||||
var macBtn = document.getElementById('macos-dl-btn');
|
||||
@@ -822,6 +846,7 @@
|
||||
<img src="/assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
|
||||
<div><a href="mailto:yammy2023@163.com">yammy2023@163.com</a></div>
|
||||
|
||||
</div>
|
||||
@@ -854,14 +879,12 @@
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
<li><a href="/terms/">服务条款</a></li>
|
||||
<li><a href="/privacy/">隐私政策</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<div>© 2026 岩美科技 · 保留所有权利</div>
|
||||
<div>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Vendored
+2
-3
@@ -670,6 +670,7 @@
|
||||
<img src="/assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
|
||||
<div><a href="mailto:yammy2023@163.com">yammy2023@163.com</a></div>
|
||||
|
||||
</div>
|
||||
@@ -702,14 +703,12 @@
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
<li><a href="/terms/">服务条款</a></li>
|
||||
<li><a href="/privacy/">隐私政策</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<div>© 2026 岩美科技 · 保留所有权利</div>
|
||||
<div>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Vendored
+2
-3
@@ -707,6 +707,7 @@
|
||||
<img src="/assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
|
||||
<div><a href="mailto:yammy2023@163.com">yammy2023@163.com</a></div>
|
||||
|
||||
</div>
|
||||
@@ -739,14 +740,12 @@
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
<li><a href="/terms/">服务条款</a></li>
|
||||
<li><a href="/privacy/">隐私政策</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<div>© 2026 岩美科技 · 保留所有权利</div>
|
||||
<div>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Vendored
+5
-7
@@ -240,8 +240,8 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<div class="container hero-inner">
|
||||
<div>
|
||||
<div class="d-inline-flex items-center gap-8 bg-0 border rounded-pill fs-sm text-gray-7 mb-24" style="padding: 6px 14px 6px 8px;">
|
||||
<span class="badge badge-brand">v1.0.23</span>
|
||||
<span>2026-06-07 更新 ·</span>
|
||||
<span class="badge badge-brand">v1.0.54</span>
|
||||
<span>2026-06-17 更新 ·</span>
|
||||
<span class="text-brand fw-5">查看更新</span>
|
||||
</div>
|
||||
<h1>从入库到出库,<br/><em>一套系统</em>管到底。</h1>
|
||||
@@ -605,7 +605,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>单门店 · 最多 3 用户</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>全部模块开放</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>Web 端 + 移动端</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>邮件技术支持</li>
|
||||
<li class="d-flex items-start gap-8 fs-sm text-gray-7"><i data-lucide="check" class="icon-sm text-brand flex-shrink-0 mt-2"></i>微信技术支持</li>
|
||||
</ul>
|
||||
<a href="/register/" class="btn btn-secondary w-full justify-center mt-auto">免费开通</a>
|
||||
</div>
|
||||
@@ -703,6 +703,7 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<img src="/assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
|
||||
<div><a href="mailto:yammy2023@163.com">yammy2023@163.com</a></div>
|
||||
|
||||
</div>
|
||||
@@ -715,7 +716,6 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<li><a href="/#reports">数据洞察</a></li>
|
||||
<li><a href="/#pricing">价格方案</a></li>
|
||||
<li><a href="/download/">版本更新</a></li>
|
||||
<li><a href="/changelog/">更新日志</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
@@ -736,14 +736,12 @@ details[open] .faq-q .icon { transform: rotate(45deg); }
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
<li><a href="/terms/">服务条款</a></li>
|
||||
<li><a href="/privacy/">隐私政策</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<div>© 2026 岩美科技 · 保留所有权利</div>
|
||||
<div>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Vendored
+4
-5
@@ -210,7 +210,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p class="success-note">如忘记门店编码,登录后可在「系统设置 → 关于」中查看。</p>
|
||||
<p class="success-note">如忘记门店编码,登录后可在「系统设置」中查看。</p>
|
||||
<a href="/app/" class="btn btn-primary" style="display:inline-flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="log-in" class="icon"></i>前往登录
|
||||
</a>
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var API_URL = window.location.origin + '/api/v1/public/register';
|
||||
var API_URL = "https://jiu.51yanmei.com/api/v1/public/register";
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
@@ -383,6 +383,7 @@
|
||||
<img src="/assets/logo-full.svg" alt="岩美" />
|
||||
<p>为酒行与酒店设计的库存、审核、财务一体化管理平台。</p>
|
||||
<div class="footer-contact">
|
||||
|
||||
<div><a href="mailto:yammy2023@163.com">yammy2023@163.com</a></div>
|
||||
|
||||
</div>
|
||||
@@ -415,14 +416,12 @@
|
||||
<h5>公司</h5>
|
||||
<ul>
|
||||
<li><a href="/register/">注册门店</a></li>
|
||||
<li><a href="/terms/">服务条款</a></li>
|
||||
<li><a href="/privacy/">隐私政策</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<div>© 2026 岩美科技 · 保留所有权利</div>
|
||||
<div>沪 ICP 备 2026000000 号 · 沪公网安备 31010000000000 号</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
+43
-2
@@ -390,9 +390,9 @@ pageStyle: |
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2 class="fs-3xl fw-6 text-brand-900 m-0 mb-6" style="letter-spacing: var(--tracking-cn-display);">版本更新日志</h2>
|
||||
<p class="fs-md text-muted m-0">最近 {{ changelog | length }} 个版本。</p>
|
||||
<p class="fs-md text-muted m-0" id="changelog-count">最近 {{ changelog | length }} 个版本。</p>
|
||||
|
||||
<div class="changelog">
|
||||
<div class="changelog" id="changelog-list">
|
||||
{% for entry in changelog %}
|
||||
<div class="changelog-entry{% if loop.first %} current{% endif %}">
|
||||
<div class="changelog-meta">
|
||||
@@ -499,6 +499,46 @@ pageStyle: |
|
||||
});
|
||||
}
|
||||
|
||||
// 用 /api/v1/public/release 返回的 changelog 重渲染更新日志时间线,使客户端
|
||||
// 发版无需重建官网即可同步。结构与构建时 web/_data/changelog.js 一致;接口
|
||||
// 不可达或字段缺失时保留构建时静态时间线作 fallback。
|
||||
function renderChangelog(entries) {
|
||||
if (!Array.isArray(entries) || entries.length === 0) return;
|
||||
var dotClass = { '新功能': 'dot-feature', '改进': 'dot-improve', '修复': 'dot-fix' };
|
||||
var label = { '修复': '问题修复' };
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"]/g, function(c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c];
|
||||
});
|
||||
}
|
||||
var html = '';
|
||||
entries.forEach(function(entry, i) {
|
||||
var current = i === 0;
|
||||
html += '<div class="changelog-entry' + (current ? ' current' : '') + '">';
|
||||
html += '<div class="changelog-meta">';
|
||||
html += '<div class="changelog-version">v' + esc(entry.version) + '</div>';
|
||||
html += '<div class="changelog-date">' + esc(entry.date) + '</div>';
|
||||
if (current) html += '<span class="changelog-tag">当前版本</span>';
|
||||
html += '</div><div class="changelog-body">';
|
||||
if (entry.intro) html += '<p class="changelog-intro">' + esc(entry.intro) + '</p>';
|
||||
(entry.sections || []).forEach(function(section) {
|
||||
var dot = dotClass[section.type] || 'dot-improve';
|
||||
var name = label[section.type] || section.type;
|
||||
html += '<div class="changelog-cat"><div class="changelog-cat-label">';
|
||||
html += '<span class="dot ' + dot + '"></span>' + esc(name) + '</div><ul>';
|
||||
(section.items || []).forEach(function(item) {
|
||||
html += '<li>' + esc(item) + '</li>';
|
||||
});
|
||||
html += '</ul></div>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
});
|
||||
var list = document.getElementById('changelog-list');
|
||||
if (list) list.innerHTML = html;
|
||||
var count = document.getElementById('changelog-count');
|
||||
if (count) count.textContent = '最近 ' + entries.length + ' 个版本。';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var detected = detectPlatform();
|
||||
updateDetectCard(detected);
|
||||
@@ -508,6 +548,7 @@ pageStyle: |
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data && data.version) updateVersionBadges(data.version);
|
||||
if (data && data.changelog) renderChangelog(data.changelog);
|
||||
if (data && data.download_urls && data.download_urls.macos) {
|
||||
platforms.macos.btnHref = data.download_urls.macos;
|
||||
var macBtn = document.getElementById('macos-dl-btn');
|
||||
|
||||
Reference in New Issue
Block a user