From 992d7f7f7a2efdca00bcd3d0753c9e6a3c861b93 Mon Sep 17 00:00:00 2001 From: iwvw <2285740204@qq.com> Date: Thu, 23 Jul 2026 19:35:00 +0800 Subject: [PATCH 1/3] README --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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、备份、定时任务、通知模板与系统日志 +- 还有很多待定功能>>>>>> + +![image](https://image.dooo.ng/t/2026/07/23/6a61fba454686.webp) ## 快速部署 @@ -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) From 9224517ae9b9c7c39c28640421597aca4939eafe Mon Sep 17 00:00:00 2001 From: iwvw <2285740204@qq.com> Date: Thu, 23 Jul 2026 20:21:32 +0800 Subject: [PATCH 2/3] feat: add application versioning --- .github/workflows/ci-cd.yml | 16 +++++ .github/workflows/release.yml | 21 ++++++ .github/workflows/version-bump.yml | 80 ++++++++++++++++++++++ Dockerfile | 4 +- docs/versioning.md | 38 +++++++++++ package.json | 3 + src/js/components/MainLayout.jsx | 10 ++- src/js/modules/appVersion.js | 1 + src/js/modules/appVersionResolver.test.js | 49 ++++++++++++++ src/js/pages/SettingsPage.jsx | 3 +- tools/app-version.mjs | 82 +++++++++++++++++++++++ vite.config.mjs | 8 +++ 12 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/version-bump.yml create mode 100644 docs/versioning.md create mode 100644 src/js/modules/appVersion.js create mode 100644 src/js/modules/appVersionResolver.test.js create mode 100644 tools/app-version.mjs 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..a9730504 --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,80 @@ +# 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] + +permissions: + contents: write + actions: write + +concurrency: + group: version-bump-main + cancel-in-progress: false + +jobs: + bump: + if: >- + github.actor != 'github-actions[bot]' && + !contains(github.event.head_commit.message, 'chore(release): bump version') + runs-on: ubuntu-latest + steps: + - name: Checkout full history + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2 + with: + fetch-depth: 2 + + - name: Detect merge commit + id: merge + shell: bash + run: | + 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/docs/versioning.md b/docs/versioning.md new file mode 100644 index 00000000..659c6da5 --- /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. 使用 merge commit 将 `dev` 合并到 `main`,不要使用 squash 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) : '加载中'} -
+
{dashboardFooterRecordNumber ? ( GitHub + + {APP_VERSION} +
)} diff --git a/src/js/modules/appVersion.js b/src/js/modules/appVersion.js new file mode 100644 index 00000000..0f5a05c9 --- /dev/null +++ b/src/js/modules/appVersion.js @@ -0,0 +1 @@ +export const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'dev-local'; diff --git a/src/js/modules/appVersionResolver.test.js b/src/js/modules/appVersionResolver.test.js new file mode 100644 index 00000000..57c0be11 --- /dev/null +++ b/src/js/modules/appVersionResolver.test.js @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest'; +import { + formatDevelopmentVersion, + normalizeReleaseVersion, + resolveAppVersion, +} from '../../../tools/app-version.mjs'; + +describe('application version resolver', () => { + it('formats development versions from the final four commit characters', () => { + expect(formatDevelopmentVersion('0123456789ABCDEF')).toBe('dev-cdef'); + expect(formatDevelopmentVersion('')).toBe('dev-local'); + }); + + it('normalizes release versions to a v-prefixed semantic version', () => { + expect(normalizeReleaseVersion('2.0.1')).toBe('v2.0.1'); + expect(normalizeReleaseVersion('v2.1.0-beta.1')).toBe('v2.1.0-beta.1'); + }); + + it('uses the package version for main branch builds', () => { + expect( + resolveAppVersion({ + branchName: 'main', + commitSha: 'abcdef12', + packageVersion: '2.0.1', + }) + ).toBe('v2.0.1'); + }); + + it('uses the tag for release builds and the hash for development builds', () => { + expect( + resolveAppVersion({ + refName: 'refs/tags/v3.0.0', + packageVersion: '2.0.1', + }) + ).toBe('v3.0.0'); + expect( + resolveAppVersion({ + branchName: 'dev', + commitSha: '0123456789abcdef', + packageVersion: '2.0.1', + }) + ).toBe('dev-cdef'); + }); + + it('allows CI and Docker builds to provide an explicit version', () => { + expect(resolveAppVersion({ explicitVersion: '2.0.2' })).toBe('v2.0.2'); + expect(resolveAppVersion({ explicitVersion: 'dev-9abc' })).toBe('dev-9abc'); + }); +}); diff --git a/src/js/pages/SettingsPage.jsx b/src/js/pages/SettingsPage.jsx index 5f622bd5..d28bf0cb 100644 --- a/src/js/pages/SettingsPage.jsx +++ b/src/js/pages/SettingsPage.jsx @@ -17,6 +17,7 @@ import useStore, { normalizeUserSettings, } from '../store.js'; import { MODULE_TABS_PROPS } from '../modules/kumoTabs.js'; +import { APP_VERSION } from '../modules/appVersion.js'; import { AppCard, SectionCard, cx } from '../components/ui/AppPrimitives.jsx'; import CodeEditor from '../components/ui/CodeEditor.jsx'; import { BackupPanel } from './BackupPage.jsx'; @@ -1466,7 +1467,7 @@ function SettingsPage() {
} bodyPadding="lg" > diff --git a/tools/app-version.mjs b/tools/app-version.mjs new file mode 100644 index 00000000..268d6348 --- /dev/null +++ b/tools/app-version.mjs @@ -0,0 +1,82 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +const SEMVER_PATTERN = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/; + +export function normalizeReleaseVersion(value, fallback = '0.0.0') { + const match = String(value || '') + .trim() + .match(SEMVER_PATTERN); + if (match) return `v${match[1]}`; + + const fallbackMatch = String(fallback || '') + .trim() + .match(SEMVER_PATTERN); + return `v${fallbackMatch?.[1] || '0.0.0'}`; +} + +export function formatDevelopmentVersion(commitSha) { + const normalizedSha = String(commitSha || '') + .trim() + .toLowerCase(); + return `dev-${normalizedSha ? normalizedSha.slice(-4) : 'local'}`; +} + +export function resolveAppVersion({ + explicitVersion = '', + refName = '', + branchName = '', + commitSha = '', + packageVersion = '0.0.0', +} = {}) { + const explicit = String(explicitVersion || '').trim(); + if (explicit) { + return SEMVER_PATTERN.test(explicit) + ? normalizeReleaseVersion(explicit, packageVersion) + : explicit; + } + + const ref = String(refName || '').trim(); + const tagName = ref.startsWith('refs/tags/') ? ref.slice('refs/tags/'.length) : ref; + if (SEMVER_PATTERN.test(tagName)) { + return normalizeReleaseVersion(tagName, packageVersion); + } + + const branch = String(branchName || ref.replace(/^refs\/heads\//, '')).trim(); + if (branch === 'main' || branch === 'master') { + return normalizeReleaseVersion(packageVersion); + } + + return formatDevelopmentVersion(commitSha); +} + +function readGitValue(cwd, args) { + try { + return execFileSync('git', args, { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return ''; + } +} + +export function resolveAppVersionFromEnvironment({ cwd = process.cwd(), env = process.env } = {}) { + let packageVersion = '0.0.0'; + try { + const packageJson = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8')); + packageVersion = packageJson.version || packageVersion; + } catch { + // Keep the safe fallback for unusual build contexts. + } + + return resolveAppVersion({ + explicitVersion: env.VITE_APP_VERSION, + refName: env.GITHUB_REF || env.GITHUB_REF_NAME, + branchName: env.GITHUB_HEAD_REF || readGitValue(cwd, ['branch', '--show-current']), + commitSha: env.GITHUB_SHA || readGitValue(cwd, ['rev-parse', 'HEAD']), + packageVersion, + }); +} diff --git a/vite.config.mjs b/vite.config.mjs index 692e6ace..88a31b06 100644 --- a/vite.config.mjs +++ b/vite.config.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from 'url'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import { visualizer } from 'rollup-plugin-visualizer'; +import { resolveAppVersionFromEnvironment } from './tools/app-version.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -31,12 +32,19 @@ function createDevLogger() { export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ''); const isProduction = mode === 'production'; + const appVersion = resolveAppVersionFromEnvironment({ + cwd: __dirname, + env: { ...process.env, ...env }, + }); return { root: 'src', base: '/', publicDir: path.resolve(__dirname, './src/pwa-public'), customLogger: createDevLogger(), + define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion), + }, plugins: [ react(), tailwindcss(), From 315b8208df664c1fa37b25e0e7251201358c79f1 Mon Sep 17 00:00:00 2001 From: iwvw <2285740204@qq.com> Date: Thu, 23 Jul 2026 20:27:08 +0800 Subject: [PATCH 3/3] fix: support all main merge version bumps --- .github/workflows/version-bump.yml | 17 +++++++++++++++-- docs/versioning.md | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index a9730504..ce9d595c 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -6,6 +6,9 @@ name: Bump Main Version on: push: branches: [main] + pull_request: + branches: [main] + types: [closed] permissions: contents: write @@ -18,19 +21,29 @@ concurrency: jobs: bump: if: >- - github.actor != 'github-actions[bot]' && - !contains(github.event.head_commit.message, 'chore(release): bump version') + (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 diff --git a/docs/versioning.md b/docs/versioning.md index 659c6da5..8332359b 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -13,7 +13,7 @@ API Monitor 使用语义化版本(Semantic Versioning),格式为 `MAJOR.MI ## 日常发布流程 1. 在 `dev` 完成功能开发和验证。 -2. 使用 merge commit 将 `dev` 合并到 `main`,不要使用 squash merge。 +2. 将 `dev` 合并到 `main`,普通 merge、squash merge 和 rebase merge 都可以。 3. `Bump Main Version` 工作流比较合并前后的版本: - 版本未变化时,自动执行 patch 递增,例如 `2.0.0` 到 `2.0.1`。 - 版本已人工提升到更高的 minor 或 major 时,保留人工版本。