diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index cf213fb9..99ca1335 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -14,6 +14,10 @@ name: CI/CD Pipeline +concurrency: + group: ci-cd-${{ github.ref }} + cancel-in-progress: true + on: push: branches: [main, dev] @@ -266,6 +270,17 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Resolve application version + id: app-version + shell: bash + run: | + if [ "$GITHUB_REF" = "refs/heads/main" ]; then + VERSION="v$(node -p "require('./package.json').version")" + else + VERSION="dev-${GITHUB_SHA: -4}" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + - name: Build and push by digest id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.13.0 @@ -290,6 +305,7 @@ jobs: build-args: | NODE_ENV=production USE_PREBUILT_AGENT=true + APP_VERSION=${{ steps.app-version.outputs.version }} - name: Export digest run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b4bb6c2..0d307e1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,6 +77,17 @@ jobs: echo "tag=v${VERSION}" >> $GITHUB_OUTPUT echo "📦 Version: ${VERSION}, Tag: v${VERSION}" + - name: Verify source version + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} + shell: bash + run: | + PACKAGE_VERSION=$(node -p "require('./package.json').version") + if [ "$RELEASE_VERSION" != "$PACKAGE_VERSION" ]; then + echo "Release version ${RELEASE_VERSION} does not match package.json ${PACKAGE_VERSION}." >&2 + exit 1 + fi + - name: Create tag (manual trigger only) if: github.event_name == 'workflow_dispatch' env: @@ -349,6 +360,15 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Resolve application version + id: app-version + shell: bash + env: + INPUT_VERSION: ${{ github.event.inputs.version }} + run: | + VERSION="${INPUT_VERSION:-${GITHUB_REF_NAME#v}}" + echo "version=v${VERSION}" >> "$GITHUB_OUTPUT" + - name: Extract metadata id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.6.1 @@ -384,6 +404,7 @@ jobs: cache-to: type=gha,mode=max,scope=${{ matrix.platform }} build-args: | NODE_ENV=production + APP_VERSION=${{ steps.app-version.outputs.version }} - name: Export digest run: | diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 00000000..ce9d595c --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,93 @@ +# Keep the application version in package.json aligned with main merges. +# The merge commit is bumped in a follow-up commit, then CI is dispatched +# explicitly because pushes made with GITHUB_TOKEN do not trigger workflows. +name: Bump Main Version + +on: + push: + branches: [main] + pull_request: + branches: [main] + types: [closed] + +permissions: + contents: write + actions: write + +concurrency: + group: version-bump-main + cancel-in-progress: false + +jobs: + bump: + if: >- + (github.event_name == 'push' && github.actor != 'github-actions[bot]' && + !contains(github.event.head_commit.message, 'chore(release): bump version')) || + (github.event_name == 'pull_request' && github.event.pull_request.merged == true) + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 + with: + fetch-depth: 2 + ref: main + + - name: Detect merge commit + id: merge + env: + EVENT_NAME: ${{ github.event_name }} + shell: bash + run: | + if [ "$EVENT_NAME" = "pull_request" ]; then + echo "parent_count=pr" >> "$GITHUB_OUTPUT" + echo "is_merge=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + PARENT_COUNT=$(git rev-list --parents -n 1 HEAD | awk '{print NF - 1}') + echo "parent_count=${PARENT_COUNT}" >> "$GITHUB_OUTPUT" + if [ "$PARENT_COUNT" -ge 2 ]; then + echo "is_merge=true" >> "$GITHUB_OUTPUT" + else + echo "is_merge=false" >> "$GITHUB_OUTPUT" + fi + + - name: Resolve next version + if: steps.merge.outputs.is_merge == 'true' + id: version + shell: bash + run: | + CURRENT_VERSION=$(node -p "require('./package.json').version") + BASE_VERSION=$(git show HEAD^1:package.json | node -e "let input=''; process.stdin.on('data', chunk => input += chunk); process.stdin.on('end', () => console.log(JSON.parse(input).version));") + + if node -e "const a='$CURRENT_VERSION'.split('.').map(Number); const b='$BASE_VERSION'.split('.').map(Number); process.exit(a[0] > b[0] || (a[0] === b[0] && (a[1] > b[1] || (a[1] === b[1] && a[2] > b[2]))) ? 0 : 1)"; then + VERSION="$CURRENT_VERSION" + CHANGED=false + else + npm version patch --no-git-tag-version --ignore-scripts + VERSION=$(node -p "require('./package.json').version") + CHANGED=true + fi + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "changed=${CHANGED}" >> "$GITHUB_OUTPUT" + + - name: Commit version bump + if: steps.merge.outputs.is_merge == 'true' && steps.version.outputs.changed == 'true' + env: + ACTOR: ${{ github.actor }} + ACTOR_ID: ${{ github.actor_id }} + VERSION: ${{ steps.version.outputs.version }} + shell: bash + run: | + git config user.name "$ACTOR" + git config user.email "${ACTOR_ID}+${ACTOR}@users.noreply.github.com" + git add package.json package-lock.json + git commit -m "chore(release): bump version to v${VERSION} [skip ci]" + git push origin HEAD:main + + - name: Dispatch CI for bumped main + if: steps.merge.outputs.is_merge == 'true' && steps.version.outputs.changed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run ci-cd.yml --ref main diff --git a/Dockerfile b/Dockerfile index 11682b3f..ea168677 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ # 阶段 1: 构建前端 (Frontend Builder) - 始终在构建主机平台运行 FROM --platform=$BUILDPLATFORM node:20-slim AS frontend-builder +ARG APP_VERSION=dev-local # 安装构建工具 RUN apt-get update && apt-get install -y --no-install-recommends \ python3 \ @@ -32,7 +33,8 @@ COPY . . # 显式设置 PATH (虽然 npm run 通常不需要,但以防万一) # 禁用 CDN 模式,所有依赖打包到本地 ENV PATH=/app/node_modules/.bin:$PATH \ - VITE_USE_CDN=false + VITE_USE_CDN=false \ + VITE_APP_VERSION=${APP_VERSION} RUN npm run build # 阶段 2: 构建 Go 后端 (Go Backend Builder) diff --git a/README.md b/README.md index c45eb5ef..5a149e12 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ API Monitor 是一个自托管的 API 管理、云资源管理与主机监控面板。 -集中管理服务器、DNS、对象存储、PaaS、文件分享等。 +集中管理服务器、DNS、对象存储、PaaS、文件分享等各种分散服务。 ## 功能概览 @@ -12,6 +12,9 @@ API Monitor 是一个自托管的 API 管理、云资源管理与主机监控面 - OpenAI 兼容接口、模型调用记录与用量统计 - 可用性监测、公开状态页、自定义域名与首页快捷入口 - 文件中转、TOTP、备份、定时任务、通知模板与系统日志 +- 还有很多待定功能>>>>>> + + ## 快速部署 @@ -73,8 +76,6 @@ npm run backend-go:build | `JWT_SECRET` | 会话密钥,建议使用长随机字符串 | | `LOG_LEVEL` | 日志级别:`DEBUG`、`INFO`、`WARN`、`ERROR` | -不要把真实密码、Token、Cookie、私钥或云厂商凭证提交到仓库。 - ## 技术栈 - 后端:Go + SQLite @@ -89,8 +90,6 @@ npm run backend-go:build - [API 接口文档](./docs/API接口文档.md) - [Kumo UI 规则](./docs/Kumo%20UI%20规则.md) -前端如果遇到 Kumo `Tabs` 描边被裁、tab 切换左右抖动、双栏页面高度填充不稳定等问题,先看 [docs/README.md](./docs/README.md) 里的“前端布局约定”。 - ## 许可证 [MIT](./LICENSE) diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 00000000..8332359b --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,38 @@ +# 版本管理 + +API Monitor 使用语义化版本(Semantic Versioning),格式为 `MAJOR.MINOR.PATCH`。 + +## 版本来源 + +- `package.json` 中的 `version` 是正式版本的单一来源。 +- GitHub Release 和 Git tag 使用相同版本,并增加 `v` 前缀,例如 `v2.0.1`。 +- `main` 构建显示正式版本,例如 `v2.0.1`。 +- `dev` 和其他开发分支显示 `dev-xxxx`,其中 `xxxx` 是完整提交哈希的末四位。 +- Docker 构建通过 `APP_VERSION` 构建参数把版本写入前端,不在运行时请求 GitHub。 + +## 日常发布流程 + +1. 在 `dev` 完成功能开发和验证。 +2. 将 `dev` 合并到 `main`,普通 merge、squash merge 和 rebase merge 都可以。 +3. `Bump Main Version` 工作流比较合并前后的版本: + - 版本未变化时,自动执行 patch 递增,例如 `2.0.0` 到 `2.0.1`。 + - 版本已人工提升到更高的 minor 或 major 时,保留人工版本。 +4. 工作流提交 `package.json` 和 `package-lock.json`,随后重新触发主分支 CI/CD。 +5. 创建 Release 时,输入版本必须与 `package.json` 一致;不一致时工作流会停止。 + +## 主版本和次版本 + +需要不兼容更新或较大功能版本时,在合并到 `main` 前运行: + +```bash +npm run version:major +npm run version:minor +``` + +普通修复版本通常不需要手动执行;主分支合并工作流会自动处理。需要本地手动递增 patch 时可运行: + +```bash +npm run version:patch +``` + +版本修改应与对应功能一起提交,不要单独修改 Git tag 来代替源代码版本。 diff --git a/package.json b/package.json index 6bae3099..842d96c1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "test": "vitest run --passWithNoTests", "test:watch": "vitest", "test:coverage": "vitest run --coverage", + "version:patch": "npm version patch --no-git-tag-version --ignore-scripts", + "version:minor": "npm version minor --no-git-tag-version --ignore-scripts", + "version:major": "npm version major --no-git-tag-version --ignore-scripts", "backend-go:dev": "cd backend-go && go run ./cmd/api-monitor", "backend-go:start": "cd backend-go && ./api-monitor.exe", "backend-go:test": "cd backend-go && go test ./...", diff --git a/src/js/components/MainLayout.jsx b/src/js/components/MainLayout.jsx index 66af6f0c..07f78516 100644 --- a/src/js/components/MainLayout.jsx +++ b/src/js/components/MainLayout.jsx @@ -8,6 +8,7 @@ import { Tooltip } from '@cloudflare/kumo/components/tooltip'; import { Button } from '@cloudflare/kumo/components/button'; import { Tabs } from '@cloudflare/kumo'; import { TOOL_TABS_PROPS } from '../modules/kumoTabs.js'; +import { APP_VERSION } from '../modules/appVersion.js'; import AppPageHeader, { AppBreadcrumbs } from './AppPageHeader.jsx'; import { AppCard } from './ui/AppPrimitives.jsx'; import { @@ -754,7 +755,7 @@ function MainLayout() { · 已运行 {appProcessUptimeMeasuredAt > 0 ? formatAppProcessUptime(displayedAppProcessUptime) : '加载中'} -