From 61ad14037fe637edf764fa4686682300bea01542 Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:17:20 +0000 Subject: [PATCH 1/8] feat(docker): add optional full-stack image --- .../workflows/docker-publish-fullstack.yml | 73 +++++++++++++++++++ DOCKER.md | 71 +++++++++++++++++- Dockerfile.fullstack | 35 +++++++++ Dockerfile.fullstack.dockerignore | 19 +++++ README.md | 12 ++- docker-compose.fullstack.yml | 14 ++++ server/src/index.ts | 5 ++ server/src/services/staticFrontend.ts | 48 ++++++++++++ server/tests/services/staticFrontend.test.ts | 52 +++++++++++++ 9 files changed, 323 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/docker-publish-fullstack.yml create mode 100644 Dockerfile.fullstack create mode 100644 Dockerfile.fullstack.dockerignore create mode 100644 docker-compose.fullstack.yml create mode 100644 server/src/services/staticFrontend.ts create mode 100644 server/tests/services/staticFrontend.test.ts diff --git a/.github/workflows/docker-publish-fullstack.yml b/.github/workflows/docker-publish-fullstack.yml new file mode 100644 index 00000000..f02d4c9f --- /dev/null +++ b/.github/workflows/docker-publish-fullstack.yml @@ -0,0 +1,73 @@ +name: Publish Full-Stack Docker Image to GHCR + +on: + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + +# Keep only the newest full-stack build for a branch or tag. This group is +# deliberately independent from the existing frontend and backend workflows. +concurrency: + group: fullstack-image-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/github-stars-manager + +jobs: + build-and-push: + runs-on: ubuntu-latest + # A cache or registry outage must not leave a publishing run blocked forever. + timeout-minutes: 30 + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # branch push → "latest" + type=raw,value=latest,enable={{is_default_branch}} + # tag push → "v1.2.3", "1.2.3", "1.2", "1" + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + # every build → sha-abc1234 + type=sha,prefix=sha- + + - name: Build and push Docker image + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + file: ./Dockerfile.fullstack + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # Keep this cache independent from frontend and server image builds. + cache-from: type=gha,scope=github-stars-manager-fullstack-${{ github.ref_type }},timeout=5m + cache-to: type=gha,scope=github-stars-manager-fullstack-${{ github.ref_type }},mode=max,timeout=5m,ignore-error=true diff --git a/DOCKER.md b/DOCKER.md index 6a88c049..55e35f52 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -1,6 +1,6 @@ # Docker Deployment -This application can be deployed using Docker with minimal configuration. The Docker setup serves the static frontend files via Nginx and handles CORS properly. +This application can be deployed using Docker with minimal configuration. Existing deployments use separate frontend and backend containers; an additional opt-in full-stack image is available for users who prefer a single container. The existing images and `docker-compose.yml` remain supported and unchanged. ## Prerequisites @@ -32,6 +32,69 @@ Available image tags (both images share the same tagging scheme): Published images: - Backend: `ghcr.io/amintacccp/github-stars-manager-server` - Frontend: `ghcr.io/amintacccp/github-stars-manager-frontend` +- Full stack (optional): `ghcr.io/amintacccp/github-stars-manager` + +## Optional Single-Container Full-Stack Deployment + +The full-stack image is an additional deployment option. It runs one Node/Express process that serves the web application, `/api`, and MCP endpoints from the same origin. It does **not** replace the standalone backend image, frontend image, or existing `docker-compose.yml` workflow. + +Use the dedicated Compose file for the simplest setup: + +```bash +# This leaves docker-compose.yml unchanged for existing deployments. +docker compose -f docker-compose.fullstack.yml up -d + +# Open the application at http://localhost:8080 +curl http://localhost:8080/api/health +``` + +You can also run the full-stack image directly. The data volume stores both SQLite data and the automatically generated encryption key, so keep the `-v` option when upgrading or recreating the container. + +```bash +docker run -d \ + --name github-stars-manager-fullstack \ + -p 8080:3000 \ + -v github-stars-data:/app/data \ + -e API_SECRET="your-secret-here" \ + -e ENCRYPTION_KEY="your-encryption-key" \ + ghcr.io/amintacccp/github-stars-manager:latest +``` + +Set `IMAGE_TAG` in a `.env` file to pin a full-stack version: + +```bash +IMAGE_TAG=0.7.0 +``` + +### Migrate an Existing Docker Compose Deployment + +Migration is optional. Existing frontend-plus-backend deployments continue to work and require no action. To migrate, first back up the current Docker volume. Replace `` with the volume name returned by `docker volume ls`; when Compose is run from this repository with its default project name, it normally ends in `_backend-data`. + +```bash +# Create a portable backup of the SQLite database and encryption key. +docker run --rm \ + -v :/data:ro \ + -v "$PWD":/backup \ + alpine tar czf /backup/github-stars-manager-data-backup.tgz -C /data . + +# Stop the split deployment without deleting its named volume. +docker compose down + +# Reuse the same Compose project directory and volume name. +docker compose -f docker-compose.fullstack.yml up -d + +# Verify the UI, API, and persisted data. +curl http://localhost:8080/api/health +``` + +Both Compose files declare the same `backend-data` volume key. When they run from the same directory with the same Compose project name, the full-stack deployment reuses the existing SQLite database and `.encryption-key`. If you normally use `docker compose -p `, pass the same `-p ` value for the migration command. + +To roll back, stop the full-stack container and start the original split deployment again. Do not add `-v` to either command, because that would delete the persisted data volume. + +```bash +docker compose -f docker-compose.fullstack.yml down +docker compose up -d +``` To pin specific versions in `docker-compose.yml`, set `BACKEND_IMAGE_TAG` and/or `FRONTEND_IMAGE_TAG` in your `.env` file: @@ -185,12 +248,12 @@ docker stop github-stars-backend && docker rm github-stars-backend This Docker setup does not affect the existing desktop packaging workflows. The GitHub Actions workflow for building desktop applications remains unchanged and continues to work as before. ## MCP Server (Agent access) -With Docker Compose, the backend MCP endpoints are exposed through nginx (frontend container) so agents on the host do not need a published backend port: +With the existing Docker Compose deployment, the backend MCP endpoints are exposed through nginx (frontend container) so agents on the host do not need a published backend port. The optional full-stack Compose deployment exposes the same endpoint URLs directly from its single Node service: | Endpoint | URL (default compose) | Notes | |----------|------------------------|--------| -| Streamable HTTP | `http://localhost:8080/mcp` | Preferred for Claude Code / modern clients | -| Legacy SSE | `http://localhost:8080/mcp/sse` | GET opens `text/event-stream`; client then POSTs to `/mcp/sse/messages?sessionId=…` | +| Streamable HTTP | `http://localhost:8080/mcp` | Same URL for split Compose and optional full-stack Compose; preferred for Claude Code / modern clients | +| Legacy SSE | `http://localhost:8080/mcp/sse` | Same URL for split Compose and optional full-stack Compose; GET opens `text/event-stream`, then clients POST to `/mcp/sse/messages?sessionId=…` | | Legacy SSE (alias) | `http://localhost:8080/sse` | Same protocol; messages at `/messages?sessionId=…` | **Desktop (Electron)** after enabling MCP in Settings: diff --git a/Dockerfile.fullstack b/Dockerfile.fullstack new file mode 100644 index 00000000..e7b2253d --- /dev/null +++ b/Dockerfile.fullstack @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1 + +# The frontend build only produces static assets. Build it once on the native +# BuildKit platform instead of emulating ARM64 for Node/Vite work. +FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend-build +WORKDIR /frontend +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Keep this stage target-platform aware: better-sqlite3 may contain native code +# and must match the final amd64 or arm64 Node runtime. +FROM node:22-alpine AS server-build +WORKDIR /app +COPY server/package*.json ./ +RUN npm ci +COPY server/ ./ +RUN npm run build && npm prune --omit=dev + +# One Node/Express process serves the SPA, API and MCP endpoints. This avoids +# introducing nginx plus process supervision into a single-container deployment. +FROM node:22-alpine AS runtime +ENV NODE_ENV=production \ + STATIC_DIR=/app/public +WORKDIR /app +COPY --from=server-build --chown=node:node /app/dist ./dist +COPY --from=server-build --chown=node:node /app/node_modules ./node_modules +COPY --from=server-build --chown=node:node /app/package.json ./package.json +COPY --from=frontend-build --chown=node:node /frontend/dist ./public +RUN mkdir -p /app/data && chown node:node /app/data +USER node +VOLUME ["/app/data"] +EXPOSE 3000 +CMD ["node", "dist/index.js"] diff --git a/Dockerfile.fullstack.dockerignore b/Dockerfile.fullstack.dockerignore new file mode 100644 index 00000000..ba0cbb8b --- /dev/null +++ b/Dockerfile.fullstack.dockerignore @@ -0,0 +1,19 @@ +.git +.github +.claude +.vscode +.sisyphus +.omo +.writing +node_modules +dist +build +electron +upload +versions +LandingPage +cloudflare-worker +assets +templates +*.md +.DS_Store diff --git a/README.md b/README.md index 358d126a..8e630d5a 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ https://github.com/AmintaCCCP/GithubStarsManager/releases ### 🐳 Run With Docker -Pre-built backend **and frontend** images are available on GHCR — no local build required: +Pre-built backend **and frontend** images are available on GHCR — no local build required. Existing Docker users should continue to use the unchanged two-service Compose deployment: ```bash docker pull ghcr.io/amintacccp/github-stars-manager-server:latest @@ -245,6 +245,14 @@ docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` +An additional **optional full-stack image** is available for users who prefer one container, one image tag, and one persistent data volume. It serves the same web UI, `/api`, and MCP endpoints from one origin: + +```bash +docker compose -f docker-compose.fullstack.yml up -d +``` + +This new option does not replace or modify the existing frontend image, backend image, `docker-compose.yml`, or desktop clients. See [DOCKER.md](DOCKER.md#optional-single-container-full-stack-deployment) for full-stack deployment, migration, backup, and rollback instructions. + > If the package is private, run `docker login ghcr.io` first (use a [PAT](https://github.com/settings/tokens) with `read:packages` scope). See [DOCKER.md](DOCKER.md) for detailed instructions. The Docker setup handles CORS properly and allows you to configure any AI or WebDAV service URLs directly in the application. @@ -260,7 +268,7 @@ The app works fully without a backend (pure frontend, localStorage). An optional ```bash docker-compose up -d ``` -Frontend on port 8080, backend on port 3000. Data persisted in a Docker volume. +Frontend on port 8080, backend on port 3000. Data is persisted in a Docker volume. This existing split deployment remains the recommended option when you need to version, operate, or scale the frontend and backend independently; the optional single-container alternative is documented in [DOCKER.md](DOCKER.md#optional-single-container-full-stack-deployment). To customize, create a `.env` file: ```bash diff --git a/docker-compose.fullstack.yml b/docker-compose.fullstack.yml new file mode 100644 index 00000000..5d67f636 --- /dev/null +++ b/docker-compose.fullstack.yml @@ -0,0 +1,14 @@ +services: + app: + image: ghcr.io/amintacccp/github-stars-manager:${IMAGE_TAG:-latest} + ports: + - "8080:3000" + environment: + API_SECRET: ${API_SECRET:-} + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} + volumes: + - backend-data:/app/data + restart: unless-stopped + +volumes: + backend-data: diff --git a/server/src/index.ts b/server/src/index.ts index cdfcf5f1..6921f89a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -6,6 +6,7 @@ import { config } from './config.js'; import { authMiddleware } from './middleware/auth.js'; import { errorHandler } from './middleware/errorHandler.js'; import { logger, morganLoggerStream } from './services/logger.js'; +import { mountStaticFrontend } from './services/staticFrontend.js'; import { getDb, closeDb } from './db/connection.js'; import { runMigrations } from './db/migrations.js'; import healthRouter from './routes/health.js'; @@ -77,6 +78,10 @@ export function createApp(): express.Express { // Mount always; each request is gated on live SQLite settings (no write on mount). mountMcpRoutes(app); + // Full-stack images opt in through STATIC_DIR. Standalone backend deployments + // leave it unset, preserving the previous API-only behavior. + mountStaticFrontend(app); + // Global error handler app.use(errorHandler); diff --git a/server/src/services/staticFrontend.ts b/server/src/services/staticFrontend.ts new file mode 100644 index 00000000..7639deff --- /dev/null +++ b/server/src/services/staticFrontend.ts @@ -0,0 +1,48 @@ +import express, { type Express, type NextFunction, type Request, type Response } from 'express'; +import fs from 'node:fs'; +import path from 'node:path'; + +const backendPathPrefixes = ['/api', '/mcp', '/sse', '/messages']; + +function isBackendPath(requestPath: string): boolean { + return backendPathPrefixes.some( + (prefix) => requestPath === prefix || requestPath.startsWith(`${prefix}/`) + ); +} + +/** + * Optionally serves a compiled frontend from the directory configured by STATIC_DIR. + * + * The standalone backend image intentionally does not set STATIC_DIR, so its routes + * and 404 behavior remain unchanged. The full-stack image sets STATIC_DIR=/app/public. + */ +export function mountStaticFrontend(app: Express, staticDir = process.env.STATIC_DIR): boolean { + if (!staticDir) { + return false; + } + + const resolvedStaticDir = path.resolve(staticDir); + const indexFile = path.join(resolvedStaticDir, 'index.html'); + if (!fs.existsSync(indexFile)) { + return false; + } + + app.use(express.static(resolvedStaticDir, { index: false })); + + // Register this after all API and MCP routes. Keep their unknown paths as 404s + // instead of returning the SPA shell. + app.get('*', (req: Request, res: Response, next: NextFunction) => { + if (isBackendPath(req.path)) { + next(); + return; + } + + res.sendFile(indexFile, (error) => { + if (error) { + next(error); + } + }); + }); + + return true; +} diff --git a/server/tests/services/staticFrontend.test.ts b/server/tests/services/staticFrontend.test.ts new file mode 100644 index 00000000..046d1989 --- /dev/null +++ b/server/tests/services/staticFrontend.test.ts @@ -0,0 +1,52 @@ +import express from 'express'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import request from 'supertest'; +import { afterEach, describe, expect, it } from 'vitest'; +import { mountStaticFrontend } from '../../src/services/staticFrontend.js'; + +const temporaryDirectories: string[] = []; + +function createStaticDirectory(): string { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'gsm-static-')); + temporaryDirectories.push(directory); + fs.writeFileSync(path.join(directory, 'index.html'), 'GithubStarsManager'); + fs.writeFileSync(path.join(directory, 'app.js'), 'window.appLoaded = true;'); + return directory; +} + +afterEach(() => { + while (temporaryDirectories.length > 0) { + fs.rmSync(temporaryDirectories.pop()!, { recursive: true, force: true }); + } +}); + +describe('mountStaticFrontend', () => { + it('does not change a standalone backend when STATIC_DIR is absent', () => { + const app = express(); + expect(mountStaticFrontend(app, undefined)).toBe(false); + }); + + it('serves assets and SPA deep links without intercepting backend paths', async () => { + const app = express(); + app.get('/api/health', (_req, res) => res.json({ status: 'ok' })); + expect(mountStaticFrontend(app, createStaticDirectory())).toBe(true); + + await request(app).get('/app.js').expect(200).expect('Content-Type', /javascript/); + const spaResponse = await request(app).get('/repositories/42').expect(200); + expect(spaResponse.text).toContain('GithubStarsManager'); + await request(app).get('/api/health').expect(200, { status: 'ok' }); + await request(app).get('/api/not-found').expect(404); + await request(app).get('/mcp').expect(404); + await request(app).get('/sse').expect(404); + await request(app).get('/messages').expect(404); + }); + + it('does not mount an incomplete static directory', () => { + const app = express(); + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'gsm-static-empty-')); + temporaryDirectories.push(directory); + expect(mountStaticFrontend(app, directory)).toBe(false); + }); +}); From 1615a56e59ca41d3a01242d95012a97a9c52bff7 Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:19:56 +0000 Subject: [PATCH 2/8] ci: validate full-stack image on pull requests --- .github/workflows/docker-publish-fullstack.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish-fullstack.yml b/.github/workflows/docker-publish-fullstack.yml index f02d4c9f..10a1cc86 100644 --- a/.github/workflows/docker-publish-fullstack.yml +++ b/.github/workflows/docker-publish-fullstack.yml @@ -4,6 +4,20 @@ on: push: branches: [main] tags: ['v*'] + pull_request: + branches: [main] + paths: + - 'Dockerfile.fullstack' + - 'Dockerfile.fullstack.dockerignore' + - 'docker-compose.fullstack.yml' + - '.github/workflows/docker-publish-fullstack.yml' + - 'package.json' + - 'package-lock.json' + - 'src/**' + - 'public/**' + - 'vite.config.ts' + - 'nginx.conf.template' + - 'server/**' workflow_dispatch: # Keep only the newest full-stack build for a branch or tag. This group is @@ -65,7 +79,8 @@ jobs: context: . file: ./Dockerfile.fullstack platforms: linux/amd64,linux/arm64 - push: true + # PRs validate the complete multi-architecture build but never publish an image. + push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # Keep this cache independent from frontend and server image builds. From d6b07d5b6e10b530d094f8923826d1600f127183 Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:33:54 +0000 Subject: [PATCH 3/8] docs: add Chinese full-stack Docker guide --- DOCKER_zh.md | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++ README_zh.md | 14 +++- 2 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 DOCKER_zh.md diff --git a/DOCKER_zh.md b/DOCKER_zh.md new file mode 100644 index 00000000..689ae29b --- /dev/null +++ b/DOCKER_zh.md @@ -0,0 +1,207 @@ +# Docker 部署指南 + +GithubStarsManager 提供两种 Docker 部署方式。原有的前后端分离方式继续得到完整支持;同时新增了一个**可选的全栈单镜像**,供希望以一个容器完成部署的用户使用。新增方式不会替换、重命名或改变任何现有镜像、`docker-compose.yml`、API 地址或客户端行为。 + +| 部署方式 | 使用的镜像 / 文件 | 适用场景 | 兼容性 | +|---|---|---|---| +| 前后端分离(现有) | `github-stars-manager-frontend`、`github-stars-manager-server`、`docker-compose.yml` | 需要独立升级、独立部署或自行配置前端反向代理的用户 | **保持不变** | +| 全栈单容器(可选) | `github-stars-manager`、`docker-compose.fullstack.yml` | 希望只运行一个容器、一个镜像标签和一个数据卷的个人服务器、Mac 或 homelab 用户 | 新增,不影响现有方式 | + +## 准备条件 + +请先安装 Docker。建议使用 Docker Compose v2(命令为 `docker compose`);已有用户仍可继续使用原有的 `docker-compose` 命令和 `docker-compose.yml`。 + +如果 GHCR 镜像被设为私有,请先登录: + +```bash +docker login ghcr.io -u YOUR_GITHUB_USERNAME +``` + +密码应使用具有 `read:packages` 权限的 [GitHub Personal Access Token](https://github.com/settings/tokens)。 + +所有镜像均使用相同的标签语义:`latest` 表示 `main` 的最新构建;`0.7.0`、`0.7`、`0` 表示发布版本;`sha-abc1234` 表示指定提交。发布镜像同时包含 `linux/amd64` 与 `linux/arm64` 变体,Docker 会根据宿主机架构自动选择 x86_64 或 ARM64 版本。 + +## 方式一:继续使用现有前后端分离部署 + +这是现有用户的默认路径,无需为全栈镜像做任何修改。`docker-compose.yml` 保持原样:前端容器对外暴露 8080 端口,后端容器在 Compose 网络中监听 3000 端口,并把 `/api`、`/mcp` 和 SSE 请求由前端代理到后端。 + +```bash +# 在仓库根目录执行 +docker-compose up -d + +# 或使用 Docker Compose v2 +docker compose up -d +``` + +应用入口为 `http://localhost:8080`。要固定前后端版本,请在项目根目录创建或修改 `.env`: + +```bash +API_SECRET=your-api-secret +ENCRYPTION_KEY=your-encryption-key +BACKEND_IMAGE_TAG=0.7.0 +FRONTEND_IMAGE_TAG=0.7.0 +# BACKEND_HOST=backend:3000 +``` + +也可以单独运行后端,适用于自行部署前端或只需要 API/MCP 的场景: + +```bash +docker run -d \ + --name github-stars-backend \ + -p 3000:3000 \ + -v github-stars-data:/app/data \ + -e API_SECRET="your-api-secret" \ + -e ENCRYPTION_KEY="your-encryption-key" \ + ghcr.io/amintacccp/github-stars-manager-server:latest +``` + +`/app/data` 中保存 SQLite 数据库和自动生成的 `.encryption-key`。请始终挂载此卷;不要在升级或清理容器时删除它。 + +## 方式二:可选的全栈单容器部署 + +全栈镜像 `ghcr.io/amintacccp/github-stars-manager` 在**一个 Node/Express 进程**中提供前端页面、`/api`、MCP 和 SSE 端点。它不在一个容器中并行管理 nginx 和 Node,因此无需额外的进程管理器。浏览器仍通过同源 `/api` 访问服务端,MCP 地址也保持为 `http://localhost:8080/mcp`。 + +最简单的部署方式是使用新增的 Compose 文件。该文件与原来的 `docker-compose.yml` 并列存在,不会覆盖或修改原文件: + +```bash +# 在仓库根目录执行 +docker compose -f docker-compose.fullstack.yml up -d + +# 验证健康检查 +curl http://localhost:8080/api/health +``` + +容器对外暴露 `8080:3000`;对客户端而言,页面、`/api`、`/mcp`、`/mcp/sse`、`/sse` 和 `/messages` 的 URL 语义与分离 Compose 部署保持一致。 + +如需固定版本,在 `.env` 中设置: + +```bash +IMAGE_TAG=0.7.0 +API_SECRET=your-api-secret +ENCRYPTION_KEY=your-encryption-key +``` + +不使用 Compose 时,可直接运行镜像: + +```bash +docker run -d \ + --name github-stars-manager-fullstack \ + -p 8080:3000 \ + -v github-stars-data:/app/data \ + -e API_SECRET="your-api-secret" \ + -e ENCRYPTION_KEY="your-encryption-key" \ + ghcr.io/amintacccp/github-stars-manager:latest +``` + +本地构建全栈镜像时,请明确指定新的 Dockerfile: + +```bash +docker build -f Dockerfile.fullstack -t github-stars-manager:local . +docker run -d \ + --name github-stars-manager-fullstack \ + -p 8080:3000 \ + -v github-stars-data:/app/data \ + github-stars-manager:local +``` + +## 从现有 Compose 部署迁移到单容器 + +迁移是**可选的**。如果当前前后端分离部署运行正常,您无需执行任何操作。只有在希望简化为一个容器时才迁移。 + +### 1. 识别并备份现有数据卷 + +先查看数据卷。默认从本仓库目录启动 Compose 时,卷名通常类似 `<项目名>_backend-data`;如果您使用了 `docker compose -p <项目名>`,卷名会使用该项目名作为前缀。 + +```bash +docker volume ls +``` + +将下方的 `` 替换为实际卷名。以下命令会在当前目录创建一个同时包含 SQLite 数据库和 `.encryption-key` 的归档: + +```bash +docker run --rm \ + -v :/data:ro \ + -v "$PWD":/backup \ + alpine tar czf /backup/github-stars-manager-data-backup.tgz -C /data . +``` + +请确认 `github-stars-manager-data-backup.tgz` 已生成,再继续下一步。 + +### 2. 停止分离部署,但不要删除卷 + +```bash +# 不要添加 -v;该参数会删除具名数据卷。 +docker compose down +``` + +### 3. 使用相同 Compose 项目名启动全栈容器 + +两个 Compose 文件都声明了 `backend-data` 卷。只要在**同一目录**下执行,并沿用相同的 Compose 项目名,全栈部署会复用原有 SQLite 数据和加密密钥。 + +```bash +# 默认项目名 +docker compose -f docker-compose.fullstack.yml up -d + +# 如原部署使用自定义项目名,请保持一致 +docker compose -p -f docker-compose.fullstack.yml up -d +``` + +### 4. 验证迁移结果 + +```bash +curl http://localhost:8080/api/health +``` + +随后在浏览器中打开 `http://localhost:8080`,检查仓库、分类、设置和跨设备同步数据。启用 MCP 的用户可继续使用同一个端点: + +| 端点 | 默认地址 | 用途 | +|---|---|---| +| Streamable HTTP | `http://localhost:8080/mcp` | 推荐用于 Claude Code、Cursor 等现代客户端 | +| Legacy SSE | `http://localhost:8080/mcp/sse` | 兼容旧式 SSE 客户端 | +| Legacy SSE alias | `http://localhost:8080/sse` | 消息发送地址为 `/messages?sessionId=…` | + +MCP Token 和 `API_SECRET` 仍是两个独立的凭据。迁移只更换容器打包方式,不会重置 SQLite 中保存的 MCP Token。 + +## 回滚到前后端分离部署 + +如果需要回滚,停止全栈容器后重新启动原有 Compose 服务即可。不要使用 `-v`,这样同一数据卷仍会被保留。 + +```bash +docker compose -f docker-compose.fullstack.yml down +docker compose up -d +``` + +若部署时使用了自定义 Compose 项目名,请在两条命令中都添加同一个 `-p `。只要保留 `/app/data` 对应的具名卷,回滚后现有数据、加密密钥与 MCP 配置都会继续可用。 + +## 环境变量 + +| 变量 | 分离部署 | 全栈部署 | 说明 | +|---|---:|---:|---| +| `API_SECRET` | 可选 | 可选 | 后端 API 的 Bearer Token;未设置时禁用 API 认证。 | +| `ENCRYPTION_KEY` | 可选 | 可选 | 用于加密服务端保存的密钥;未设置时生成并保存至数据卷。 | +| `DB_PATH` | 可选 | 可选 | SQLite 文件路径,默认位于 `data/data.db`。 | +| `PORT` | 可选 | 可选 | Node 服务端口,默认 3000;全栈 Compose 默认将宿主机 8080 映射至容器 3000。 | +| `BACKEND_HOST` | 可选 | 不需要 | 仅分离前端 nginx 镜像用于指定 `/api` 上游;全栈镜像不使用。 | +| `IMAGE_TAG` | 不使用 | 可选 | `docker-compose.fullstack.yml` 使用的全栈镜像标签,默认 `latest`。 | +| `BACKEND_IMAGE_TAG` | 可选 | 不使用 | 现有 `docker-compose.yml` 后端镜像标签。 | +| `FRONTEND_IMAGE_TAG` | 可选 | 不使用 | 现有 `docker-compose.yml` 前端镜像标签。 | + +## 停止和清理 + +```bash +# 停止原有前后端分离部署 +docker compose down + +# 停止可选全栈部署 +docker compose -f docker-compose.fullstack.yml down + +# 删除全栈容器(直接 docker run 时) +docker stop github-stars-manager-fullstack +docker rm github-stars-manager-fullstack +``` + +除非您已经完成备份并明确希望销毁所有服务端数据,否则请不要使用 `docker volume rm` 或 `docker compose down -v` 删除 `backend-data` 卷。 + +## 客户端与部署兼容性说明 + +全栈镜像是新增入口,不会影响任何现有用户:现有前端镜像、后端镜像、`docker-compose.yml`、桌面客户端、API 地址和 MCP 客户端均继续按原方式工作。选择全栈镜像的用户使用同源 URL;选择分离部署的用户无需改动任何命令、端口、环境变量或客户端设置。 diff --git a/README_zh.md b/README_zh.md index 1e112125..eed1f78c 100644 --- a/README_zh.md +++ b/README_zh.md @@ -390,7 +390,7 @@ npm run build ### Docker 部署 -GHCR 上提供预构建的**后端和前端**镜像,无需本地构建: +GHCR 上提供预构建的**后端和前端**镜像,无需本地构建。现有 Docker 用户可继续使用完全不变的前后端分离 Compose 部署: ```bash docker pull ghcr.io/amintacccp/github-stars-manager-server:latest @@ -398,9 +398,15 @@ docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` -> 如果镜像为私有,需先执行 `docker login ghcr.io`(使用具有 `read:packages` 权限的 [PAT](https://github.com/settings/tokens))。 +此外,项目新增了一个**可选的全栈单镜像**,适合希望只运行一个容器、一个镜像标签和一个数据卷的用户。它在同一来源下提供网页、`/api` 和 MCP 端点: + +```bash +docker compose -f docker-compose.fullstack.yml up -d +``` + +新增方式不会替换或修改现有的前端镜像、后端镜像、`docker-compose.yml`、桌面客户端或 API 路径。完整的中文部署、数据备份、从分离部署迁移和回滚说明请参阅 [DOCKER_zh.md](DOCKER_zh.md)。英文说明请参阅 [DOCKER.md](DOCKER.md)。 -请参阅 [DOCKER.md](DOCKER.md) 获取详细的构建和部署说明。Docker 设置正确处理了 CORS,并允许您直接在应用程序中配置任何 AI 或 WebDAV 服务 URL。 +> 如果镜像为私有,需先执行 `docker login ghcr.io`(使用具有 `read:packages` 权限的 [PAT](https://github.com/settings/tokens))。 ### 🖥️ 后端服务器(可选) @@ -414,7 +420,7 @@ docker-compose up -d ```bash docker-compose up -d ``` -前端运行在 8080 端口,后端运行在 3000 端口。数据持久化存储在 Docker 卷中。 +前端运行在 8080 端口,后端运行在 3000 端口。数据持久化存储在 Docker 卷中。该现有分离部署方式不会因全栈镜像而变化;需要独立升级、运维或扩缩容前后端时,仍建议继续使用它。若希望简化为单容器部署,请参阅 [DOCKER_zh.md](DOCKER_zh.md)。 自定义配置,创建 `.env` 文件: ```bash From b052f4fc92d053e94a79cf7410e87446edc6108a Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:46:29 +0000 Subject: [PATCH 4/8] fix(ci): align image releases and harden full-stack deployment --- .github/workflows/build-desktop.yml | 12 ++++ .github/workflows/docker-publish-frontend.yml | 5 ++ .../workflows/docker-publish-fullstack.yml | 58 +++++++++++++++---- .github/workflows/docker-publish.yml | 23 +++++--- DOCKER.md | 36 ++++++++---- DOCKER_zh.md | 29 ++++++---- README.md | 4 +- README_zh.md | 4 +- docker-compose.fullstack.yml | 4 +- scripts/check-release-version.cjs | 21 +++++++ server/src/services/staticFrontend.ts | 10 +++- server/tests/services/staticFrontend.test.ts | 2 + 12 files changed, 161 insertions(+), 47 deletions(-) create mode 100644 scripts/check-release-version.cjs diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index a91fe313..a68b42ce 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -9,7 +9,19 @@ on: workflow_dispatch: jobs: + verify-release-version: + name: Verify release tag matches client version + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Verify tagged release version + if: github.ref_type == 'tag' + run: node scripts/check-release-version.cjs "$GITHUB_REF_NAME" + build: + needs: verify-release-version runs-on: ${{ matrix.os }} continue-on-error: false diff --git a/.github/workflows/docker-publish-frontend.yml b/.github/workflows/docker-publish-frontend.yml index 48f1e0b2..32a1a0f4 100644 --- a/.github/workflows/docker-publish-frontend.yml +++ b/.github/workflows/docker-publish-frontend.yml @@ -31,6 +31,10 @@ jobs: with: persist-credentials: false + - name: Verify release tag matches client version + if: github.ref_type == 'tag' + run: node scripts/check-release-version.cjs "$GITHUB_REF_NAME" + - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 @@ -53,6 +57,7 @@ jobs: # branch push → "latest" type=raw,value=latest,enable={{is_default_branch}} # tag push → "v1.2.3", "1.2.3", "1.2", "1" + type=ref,event=tag type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} diff --git a/.github/workflows/docker-publish-fullstack.yml b/.github/workflows/docker-publish-fullstack.yml index 10a1cc86..357db556 100644 --- a/.github/workflows/docker-publish-fullstack.yml +++ b/.github/workflows/docker-publish-fullstack.yml @@ -20,18 +20,52 @@ on: - 'server/**' workflow_dispatch: -# Keep only the newest full-stack build for a branch or tag. This group is -# deliberately independent from the existing frontend and backend workflows. +# A semver publication updates shared tags (for example 1 and 1.2), so tag +# releases use one group. Branches and pull requests remain independent. concurrency: - group: fullstack-image-${{ github.ref }} - cancel-in-progress: true + group: fullstack-image-${{ github.ref_type == 'tag' && 'release' || github.ref }} + # Keep the newest branch/PR build, but finish every release tag in order so + # shared major/minor tags cannot race or leave an older version unpublished. + cancel-in-progress: ${{ github.ref_type != 'tag' }} env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/github-stars-manager + IMAGE_NAME: ${{ github.repository_owner }}/github-stars-manager-fullstack jobs: - build-and-push: + validate: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 30 + # PR validation does not authenticate to GHCR and cannot publish packages. + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Validate full-stack multi-architecture image + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 + with: + context: . + file: ./Dockerfile.fullstack + platforms: linux/amd64,linux/arm64 + push: false + # The cache remains isolated from the frontend and backend image workflows. + cache-from: type=gha,scope=github-stars-manager-fullstack-pr,timeout=5m + cache-to: type=gha,scope=github-stars-manager-fullstack-pr,mode=max,timeout=5m,ignore-error=true + + publish: + if: github.event_name != 'pull_request' runs-on: ubuntu-latest # A cache or registry outage must not leave a publishing run blocked forever. timeout-minutes: 30 @@ -45,6 +79,10 @@ jobs: with: persist-credentials: false + - name: Verify release tag matches client version + if: github.ref_type == 'tag' + run: node scripts/check-release-version.cjs "$GITHUB_REF_NAME" + - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 @@ -67,6 +105,7 @@ jobs: # branch push → "latest" type=raw,value=latest,enable={{is_default_branch}} # tag push → "v1.2.3", "1.2.3", "1.2", "1" + type=ref,event=tag type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} @@ -79,10 +118,9 @@ jobs: context: . file: ./Dockerfile.fullstack platforms: linux/amd64,linux/arm64 - # PRs validate the complete multi-architecture build but never publish an image. - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} # Keep this cache independent from frontend and server image builds. - cache-from: type=gha,scope=github-stars-manager-fullstack-${{ github.ref_type }},timeout=5m - cache-to: type=gha,scope=github-stars-manager-fullstack-${{ github.ref_type }},mode=max,timeout=5m,ignore-error=true + cache-from: type=gha,scope=github-stars-manager-fullstack-publish-${{ github.ref_type }},timeout=5m + cache-to: type=gha,scope=github-stars-manager-fullstack-publish-${{ github.ref_type }},mode=max,timeout=5m,ignore-error=true diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4b880e01..9234e65b 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,4 +1,4 @@ -name: Publish Docker Image to GHCR +name: Publish Backend Docker Image to GHCR on: push: @@ -9,12 +9,14 @@ on: # Keep only the newest run for each branch or tag, while allowing release tags # and main to publish independently. concurrency: - group: server-image-${{ github.ref }} + group: backend-image-${{ github.ref }} cancel-in-progress: true env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/github-stars-manager-server + IMAGE_NAME: ${{ github.repository_owner }}/github-stars-manager-backend + # Compatibility alias used by the existing docker-compose.yml and direct users. + LEGACY_IMAGE_NAME: ${{ github.repository_owner }}/github-stars-manager-server jobs: build-and-push: @@ -31,6 +33,10 @@ jobs: with: persist-credentials: false + - name: Verify release tag matches client version + if: github.ref_type == 'tag' + run: node scripts/check-release-version.cjs "$GITHUB_REF_NAME" + - name: Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 @@ -48,11 +54,14 @@ jobs: id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + ${{ env.REGISTRY }}/${{ env.LEGACY_IMAGE_NAME }} tags: | # branch push → "latest" type=raw,value=latest,enable={{is_default_branch}} # tag push → "v1.2.3", "1.2.3", "1.2", "1" + type=ref,event=tag type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} @@ -67,6 +76,6 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - # Do not share the default `buildkit` scope with the frontend image. - cache-from: type=gha,scope=github-stars-manager-server-${{ github.ref_type }},timeout=5m - cache-to: type=gha,scope=github-stars-manager-server-${{ github.ref_type }},mode=max,timeout=5m,ignore-error=true + # Do not share the default `buildkit` scope with frontend or full-stack images. + cache-from: type=gha,scope=github-stars-manager-backend-${{ github.ref_type }},timeout=5m + cache-to: type=gha,scope=github-stars-manager-backend-${{ github.ref_type }},mode=max,timeout=5m,ignore-error=true diff --git a/DOCKER.md b/DOCKER.md index 55e35f52..f4c81ec4 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -24,23 +24,33 @@ docker-compose up -d > ``` > Use a [Personal Access Token](https://github.com/settings/tokens) (with `read:packages` scope) as the password. -Available image tags (both images share the same tagging scheme): +Available image tags (all role-specific images share the same tagging scheme): - `latest` — latest build from the `main` branch -- `0.6.2`, `0.6`, `0` — specific version tags (semver, `v` prefix stripped) +- `v0.7.8` — the exact release tag; it must match the root `package.json` client version +- `0.7.8`, `0.7`, `0` — convenience semver tags derived from the same `v0.7.8` release - `sha-abc1234` — specific commit builds +The root `package.json` is the single version source for desktop clients and Docker releases. A release workflow rejects a Git tag unless it is exactly `v` plus that file's `version`, so matching release image tags and client versions are published together. + Published images: -- Backend: `ghcr.io/amintacccp/github-stars-manager-server` - Frontend: `ghcr.io/amintacccp/github-stars-manager-frontend` -- Full stack (optional): `ghcr.io/amintacccp/github-stars-manager` +- Backend (canonical): `ghcr.io/amintacccp/github-stars-manager-backend` +- Full stack (optional): `ghcr.io/amintacccp/github-stars-manager-fullstack` +- Backend legacy compatibility alias: `ghcr.io/amintacccp/github-stars-manager-server` + +The `-frontend`, `-backend`, and `-fullstack` names identify the image role consistently. The existing `-server` backend alias continues to receive the same tags so current `docker-compose.yml` and direct `docker run` deployments remain unchanged. ## Optional Single-Container Full-Stack Deployment The full-stack image is an additional deployment option. It runs one Node/Express process that serves the web application, `/api`, and MCP endpoints from the same origin. It does **not** replace the standalone backend image, frontend image, or existing `docker-compose.yml` workflow. -Use the dedicated Compose file for the simplest setup: +Use the dedicated Compose file for the simplest setup. Before starting, create a `.env` file with an API secret; the full-stack Compose file refuses to start without it so a new network-facing deployment is not accidentally unauthenticated. ```bash +API_SECRET=replace-with-a-long-random-secret +# Optional: set this to keep a chosen encryption key rather than generating one in the data volume. +# ENCRYPTION_KEY=replace-with-your-encryption-key + # This leaves docker-compose.yml unchanged for existing deployments. docker compose -f docker-compose.fullstack.yml up -d @@ -57,12 +67,13 @@ docker run -d \ -v github-stars-data:/app/data \ -e API_SECRET="your-secret-here" \ -e ENCRYPTION_KEY="your-encryption-key" \ - ghcr.io/amintacccp/github-stars-manager:latest + ghcr.io/amintacccp/github-stars-manager-fullstack:latest ``` -Set `IMAGE_TAG` in a `.env` file to pin a full-stack version: +Add `IMAGE_TAG` to the same `.env` file to pin a full-stack version: ```bash +API_SECRET=replace-with-a-long-random-secret IMAGE_TAG=0.7.0 ``` @@ -80,6 +91,7 @@ docker run --rm \ # Stop the split deployment without deleting its named volume. docker compose down +# Set API_SECRET in .env before starting the new network-facing service. # Reuse the same Compose project directory and volume name. docker compose -f docker-compose.fullstack.yml up -d @@ -106,7 +118,7 @@ FRONTEND_IMAGE_TAG=0.6.2 ## Backend Server (docker run) -The backend image is published to GHCR and can be run standalone: +The backend image is published to GHCR and can be run standalone. New standalone deployments should use the canonical `-backend` image. The legacy `-server` image remains published with identical tags exclusively for existing `docker-compose.yml` and direct deployments, so no current user must change an image reference. ```bash # Basic — no auth, port 3000, data persisted in volume @@ -114,7 +126,7 @@ docker run -d \ --name github-stars-backend \ -v github-stars-data:/app/data \ -p 3000:3000 \ - ghcr.io/amintacccp/github-stars-manager-server:latest + ghcr.io/amintacccp/github-stars-manager-backend:latest # With custom API secret and encryption key docker run -d \ @@ -123,7 +135,7 @@ docker run -d \ -p 3000:3000 \ -e API_SECRET="your-secret-here" \ -e ENCRYPTION_KEY="your-encryption-key" \ - ghcr.io/amintacccp/github-stars-manager-server:latest + ghcr.io/amintacccp/github-stars-manager-backend:latest # Map to a different host port (e.g. 8080) docker run -d \ @@ -131,14 +143,14 @@ docker run -d \ -v github-stars-data:/app/data \ -p 8080:3000 \ -e API_SECRET="your-secret-here" \ - ghcr.io/amintacccp/github-stars-manager-server:latest + ghcr.io/amintacccp/github-stars-manager-backend:latest ``` ### Environment Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| -| `API_SECRET` | No | `null` (auth disabled) | Bearer token for API authentication | +| `API_SECRET` | Optional for standalone backend | `null` (auth disabled) | Bearer token for API authentication. It is required by `docker-compose.fullstack.yml` so the new single-container web service cannot start unauthenticated. | | `ENCRYPTION_KEY` | No | Auto-generated (saved to `data/.encryption-key`) | AES-256 key for encrypting stored secrets. Accepts any format — 64-char hex, shorter hex, base64, or plain text (all normalized via SHA-256) | | `PORT` | No | `3000` | Server listening port | | `DB_PATH` | No | `data/data.db` | Path to SQLite database file | diff --git a/DOCKER_zh.md b/DOCKER_zh.md index 689ae29b..cf9631ed 100644 --- a/DOCKER_zh.md +++ b/DOCKER_zh.md @@ -5,7 +5,9 @@ GithubStarsManager 提供两种 Docker 部署方式。原有的前后端分离 | 部署方式 | 使用的镜像 / 文件 | 适用场景 | 兼容性 | |---|---|---|---| | 前后端分离(现有) | `github-stars-manager-frontend`、`github-stars-manager-server`、`docker-compose.yml` | 需要独立升级、独立部署或自行配置前端反向代理的用户 | **保持不变** | -| 全栈单容器(可选) | `github-stars-manager`、`docker-compose.fullstack.yml` | 希望只运行一个容器、一个镜像标签和一个数据卷的个人服务器、Mac 或 homelab 用户 | 新增,不影响现有方式 | +| 全栈单容器(可选) | `github-stars-manager-fullstack`、`docker-compose.fullstack.yml` | 希望只运行一个容器、一个镜像标签和一个数据卷的个人服务器、Mac 或 homelab 用户 | 新增,不影响现有方式 | + +规范镜像名称使用明确的角色后缀:`-frontend`、`-backend` 与 `-fullstack`。原有 `-server` 后端镜像会继续发布同样的标签,作为现有 `docker-compose.yml` 和直接部署用户的兼容别名。 ## 准备条件 @@ -19,7 +21,7 @@ docker login ghcr.io -u YOUR_GITHUB_USERNAME 密码应使用具有 `read:packages` 权限的 [GitHub Personal Access Token](https://github.com/settings/tokens)。 -所有镜像均使用相同的标签语义:`latest` 表示 `main` 的最新构建;`0.7.0`、`0.7`、`0` 表示发布版本;`sha-abc1234` 表示指定提交。发布镜像同时包含 `linux/amd64` 与 `linux/arm64` 变体,Docker 会根据宿主机架构自动选择 x86_64 或 ARM64 版本。 +所有角色镜像均使用相同的标签语义:`latest` 表示 `main` 的最新构建;`v0.7.8` 表示与客户端完全一致的正式发布标签;`0.7.8`、`0.7`、`0` 是由该正式标签派生的便捷标签;`sha-abc1234` 表示指定提交。根目录 `package.json` 的 `version` 是客户端和 Docker 正式发布的唯一版本来源;只有与该版本完全匹配的 `v` Git 标签才能发布正式镜像。发布镜像同时包含 `linux/amd64` 与 `linux/arm64` 变体,Docker 会根据宿主机架构自动选择 x86_64 或 ARM64 版本。 ## 方式一:继续使用现有前后端分离部署 @@ -43,7 +45,7 @@ FRONTEND_IMAGE_TAG=0.7.0 # BACKEND_HOST=backend:3000 ``` -也可以单独运行后端,适用于自行部署前端或只需要 API/MCP 的场景: +也可以单独运行后端,适用于自行部署前端或只需要 API/MCP 的场景。新部署建议使用规范的 `-backend` 镜像;原有的 `-server` 镜像仍会同步发布相同标签,因此现有用户不需要修改部署: ```bash docker run -d \ @@ -52,19 +54,24 @@ docker run -d \ -v github-stars-data:/app/data \ -e API_SECRET="your-api-secret" \ -e ENCRYPTION_KEY="your-encryption-key" \ - ghcr.io/amintacccp/github-stars-manager-server:latest + ghcr.io/amintacccp/github-stars-manager-backend:latest ``` `/app/data` 中保存 SQLite 数据库和自动生成的 `.encryption-key`。请始终挂载此卷;不要在升级或清理容器时删除它。 ## 方式二:可选的全栈单容器部署 -全栈镜像 `ghcr.io/amintacccp/github-stars-manager` 在**一个 Node/Express 进程**中提供前端页面、`/api`、MCP 和 SSE 端点。它不在一个容器中并行管理 nginx 和 Node,因此无需额外的进程管理器。浏览器仍通过同源 `/api` 访问服务端,MCP 地址也保持为 `http://localhost:8080/mcp`。 +全栈镜像 `ghcr.io/amintacccp/github-stars-manager-fullstack` 在**一个 Node/Express 进程**中提供前端页面、`/api`、MCP 和 SSE 端点。它不在一个容器中并行管理 nginx 和 Node,因此无需额外的进程管理器。浏览器仍通过同源 `/api` 访问服务端,MCP 地址也保持为 `http://localhost:8080/mcp`。 -最简单的部署方式是使用新增的 Compose 文件。该文件与原来的 `docker-compose.yml` 并列存在,不会覆盖或修改原文件: +最简单的部署方式是使用新增的 Compose 文件。该文件与原来的 `docker-compose.yml` 并列存在,不会覆盖或修改原文件。为避免新增的网络服务意外以无认证状态启动,Compose 会要求先在 `.env` 设置 `API_SECRET`: ```bash -# 在仓库根目录执行 +# 在仓库根目录的 .env 中设置 +API_SECRET=替换为足够长的随机密钥 +# 可选:不设置时会在数据卷中自动生成并保存。 +# ENCRYPTION_KEY=替换为你的加密密钥 + +# 启动全栈单容器 docker compose -f docker-compose.fullstack.yml up -d # 验证健康检查 @@ -90,18 +97,18 @@ docker run -d \ -v github-stars-data:/app/data \ -e API_SECRET="your-api-secret" \ -e ENCRYPTION_KEY="your-encryption-key" \ - ghcr.io/amintacccp/github-stars-manager:latest + ghcr.io/amintacccp/github-stars-manager-fullstack:latest ``` 本地构建全栈镜像时,请明确指定新的 Dockerfile: ```bash -docker build -f Dockerfile.fullstack -t github-stars-manager:local . +docker build -f Dockerfile.fullstack -t github-stars-manager-fullstack:local . docker run -d \ --name github-stars-manager-fullstack \ -p 8080:3000 \ -v github-stars-data:/app/data \ - github-stars-manager:local + github-stars-manager-fullstack:local ``` ## 从现有 Compose 部署迁移到单容器 @@ -177,7 +184,7 @@ docker compose up -d | 变量 | 分离部署 | 全栈部署 | 说明 | |---|---:|---:|---| -| `API_SECRET` | 可选 | 可选 | 后端 API 的 Bearer Token;未设置时禁用 API 认证。 | +| `API_SECRET` | 可选 | 全栈 Compose 必填 | 后端 API 的 Bearer Token;独立后端未设置时禁用认证。全栈 Compose 必须设置,以避免新服务无认证启动。 | | `ENCRYPTION_KEY` | 可选 | 可选 | 用于加密服务端保存的密钥;未设置时生成并保存至数据卷。 | | `DB_PATH` | 可选 | 可选 | SQLite 文件路径,默认位于 `data/data.db`。 | | `PORT` | 可选 | 可选 | Node 服务端口,默认 3000;全栈 Compose 默认将宿主机 8080 映射至容器 3000。 | diff --git a/README.md b/README.md index 8e630d5a..10566352 100644 --- a/README.md +++ b/README.md @@ -245,13 +245,13 @@ docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` -An additional **optional full-stack image** is available for users who prefer one container, one image tag, and one persistent data volume. It serves the same web UI, `/api`, and MCP endpoints from one origin: +An additional **optional full-stack image** (`ghcr.io/amintacccp/github-stars-manager-fullstack`) is available for users who prefer one container, one image tag, and one persistent data volume. It serves the same web UI, `/api`, and MCP endpoints from one origin: ```bash docker compose -f docker-compose.fullstack.yml up -d ``` -This new option does not replace or modify the existing frontend image, backend image, `docker-compose.yml`, or desktop clients. See [DOCKER.md](DOCKER.md#optional-single-container-full-stack-deployment) for full-stack deployment, migration, backup, and rollback instructions. +This new option does not replace or modify the existing frontend image, backend image, `docker-compose.yml`, or desktop clients. The canonical role names are `-frontend`, `-backend`, and `-fullstack`; the existing `-server` backend image remains a compatibility alias for current deployments. Formal `vX.Y.Z` Docker tags must match the root `package.json` client version, while `latest` and `sha-*` remain development and traceability tags. See [DOCKER.md](DOCKER.md#optional-single-container-full-stack-deployment) for full-stack deployment, migration, backup, and rollback instructions. > If the package is private, run `docker login ghcr.io` first (use a [PAT](https://github.com/settings/tokens) with `read:packages` scope). diff --git a/README_zh.md b/README_zh.md index eed1f78c..6d60840a 100644 --- a/README_zh.md +++ b/README_zh.md @@ -398,13 +398,13 @@ docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` -此外,项目新增了一个**可选的全栈单镜像**,适合希望只运行一个容器、一个镜像标签和一个数据卷的用户。它在同一来源下提供网页、`/api` 和 MCP 端点: +此外,项目新增了一个**可选的全栈单镜像**(`ghcr.io/amintacccp/github-stars-manager-fullstack`),适合希望只运行一个容器、一个镜像标签和一个数据卷的用户。它在同一来源下提供网页、`/api` 和 MCP 端点: ```bash docker compose -f docker-compose.fullstack.yml up -d ``` -新增方式不会替换或修改现有的前端镜像、后端镜像、`docker-compose.yml`、桌面客户端或 API 路径。完整的中文部署、数据备份、从分离部署迁移和回滚说明请参阅 [DOCKER_zh.md](DOCKER_zh.md)。英文说明请参阅 [DOCKER.md](DOCKER.md)。 +新增方式不会替换或修改现有的前端镜像、后端镜像、`docker-compose.yml`、桌面客户端或 API 路径。规范名称以角色结尾:`-frontend`、`-backend` 与 `-fullstack`;已有用户使用的 `-server` 后端镜像会继续作为兼容别名发布。正式的 `vX.Y.Z` Docker 标签必须与根目录 `package.json` 的客户端版本一致,`latest` 与 `sha-*` 则分别用于开发和提交追溯。完整的中文部署、数据备份、从分离部署迁移和回滚说明请参阅 [DOCKER_zh.md](DOCKER_zh.md)。英文说明请参阅 [DOCKER.md](DOCKER.md)。 > 如果镜像为私有,需先执行 `docker login ghcr.io`(使用具有 `read:packages` 权限的 [PAT](https://github.com/settings/tokens))。 diff --git a/docker-compose.fullstack.yml b/docker-compose.fullstack.yml index 5d67f636..f42a64a4 100644 --- a/docker-compose.fullstack.yml +++ b/docker-compose.fullstack.yml @@ -1,10 +1,10 @@ services: app: - image: ghcr.io/amintacccp/github-stars-manager:${IMAGE_TAG:-latest} + image: ghcr.io/amintacccp/github-stars-manager-fullstack:${IMAGE_TAG:-latest} ports: - "8080:3000" environment: - API_SECRET: ${API_SECRET:-} + API_SECRET: ${API_SECRET:?Set API_SECRET in .env before starting the full-stack deployment} ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} volumes: - backend-data:/app/data diff --git a/scripts/check-release-version.cjs b/scripts/check-release-version.cjs new file mode 100644 index 00000000..7c7bfc77 --- /dev/null +++ b/scripts/check-release-version.cjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const releaseTag = process.argv[2]; +if (!releaseTag) { + console.error('Usage: node scripts/check-release-version.cjs vX.Y.Z'); + process.exit(2); +} + +const packagePath = path.resolve(__dirname, '..', 'package.json'); +const { version } = JSON.parse(fs.readFileSync(packagePath, 'utf8')); +const expectedTag = `v${version}`; + +if (releaseTag !== expectedTag) { + console.error(`Release tag ${releaseTag} does not match client version ${version}. Expected ${expectedTag}.`); + process.exit(1); +} + +console.log(`Release tag ${releaseTag} matches client version ${version}.`); diff --git a/server/src/services/staticFrontend.ts b/server/src/services/staticFrontend.ts index 7639deff..e22b7dff 100644 --- a/server/src/services/staticFrontend.ts +++ b/server/src/services/staticFrontend.ts @@ -27,7 +27,15 @@ export function mountStaticFrontend(app: Express, staticDir = process.env.STATIC return false; } - app.use(express.static(resolvedStaticDir, { index: false })); + const serveStatic = express.static(resolvedStaticDir, { index: false }); + app.use((req: Request, res: Response, next: NextFunction) => { + if (isBackendPath(req.path)) { + next(); + return; + } + + serveStatic(req, res, next); + }); // Register this after all API and MCP routes. Keep their unknown paths as 404s // instead of returning the SPA shell. diff --git a/server/tests/services/staticFrontend.test.ts b/server/tests/services/staticFrontend.test.ts index 046d1989..6c667d63 100644 --- a/server/tests/services/staticFrontend.test.ts +++ b/server/tests/services/staticFrontend.test.ts @@ -13,6 +13,8 @@ function createStaticDirectory(): string { temporaryDirectories.push(directory); fs.writeFileSync(path.join(directory, 'index.html'), 'GithubStarsManager'); fs.writeFileSync(path.join(directory, 'app.js'), 'window.appLoaded = true;'); + fs.mkdirSync(path.join(directory, 'api')); + fs.writeFileSync(path.join(directory, 'api', 'not-found'), 'must not shadow an API path'); return directory; } From e4ad65eaa567085187cf3dcfb9ee59afe82f3c94 Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:50:46 +0000 Subject: [PATCH 5/8] fix(release): standardize image roles and version contract --- DOCKER.md | 33 +++++++++++------ DOCKER_zh.md | 37 ++++++++++++-------- README.md | 4 +-- README_zh.md | 4 +-- server/src/services/staticFrontend.ts | 1 + server/tests/services/staticFrontend.test.ts | 1 + 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index f4c81ec4..61f0f6e4 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -74,7 +74,7 @@ Add `IMAGE_TAG` to the same `.env` file to pin a full-stack version: ```bash API_SECRET=replace-with-a-long-random-secret -IMAGE_TAG=0.7.0 +IMAGE_TAG=0.7.8 ``` ### Migrate an Existing Docker Compose Deployment @@ -82,18 +82,22 @@ IMAGE_TAG=0.7.0 Migration is optional. Existing frontend-plus-backend deployments continue to work and require no action. To migrate, first back up the current Docker volume. Replace `` with the volume name returned by `docker volume ls`; when Compose is run from this repository with its default project name, it normally ends in `_backend-data`. ```bash -# Create a portable backup of the SQLite database and encryption key. +# Stop all SQLite writers without deleting the named data volume. +# Default Compose project name: +docker compose down +# If you use a custom project name, use it for every command below instead: +# docker compose -p down + +# Create a portable backup only after the database is quiesced. docker run --rm \ -v :/data:ro \ -v "$PWD":/backup \ alpine tar czf /backup/github-stars-manager-data-backup.tgz -C /data . -# Stop the split deployment without deleting its named volume. -docker compose down - # Set API_SECRET in .env before starting the new network-facing service. # Reuse the same Compose project directory and volume name. docker compose -f docker-compose.fullstack.yml up -d +# For a custom project: docker compose -p -f docker-compose.fullstack.yml up -d # Verify the UI, API, and persisted data. curl http://localhost:8080/api/health @@ -101,19 +105,28 @@ curl http://localhost:8080/api/health Both Compose files declare the same `backend-data` volume key. When they run from the same directory with the same Compose project name, the full-stack deployment reuses the existing SQLite database and `.encryption-key`. If you normally use `docker compose -p `, pass the same `-p ` value for the migration command. -To roll back, stop the full-stack container and start the original split deployment again. Do not add `-v` to either command, because that would delete the persisted data volume. +To roll back from a Compose deployment, stop the full-stack service and start the original split deployment again. If the full-stack service was created with direct `docker run`, stop and remove that container instead before starting Compose. Do not add `-v` to any of these commands, because that would delete the persisted data volume. ```bash +# Full-stack service started by Compose: docker compose -f docker-compose.fullstack.yml down +# For a custom project: docker compose -p -f docker-compose.fullstack.yml down + +# Full-stack service started by direct docker run (use this instead of Compose down): +# docker stop github-stars-manager-fullstack +# docker rm github-stars-manager-fullstack + +# Restore the existing split deployment. docker compose up -d +# For a custom project: docker compose -p up -d ``` To pin specific versions in `docker-compose.yml`, set `BACKEND_IMAGE_TAG` and/or `FRONTEND_IMAGE_TAG` in your `.env` file: ```bash -BACKEND_IMAGE_TAG=0.6.2 -FRONTEND_IMAGE_TAG=0.6.2 +BACKEND_IMAGE_TAG=0.7.8 +FRONTEND_IMAGE_TAG=0.7.8 ``` ## Backend Server (docker run) @@ -170,8 +183,8 @@ To customize secrets and image versions, create a `.env` file in the project roo ```bash API_SECRET=my-strong-secret ENCRYPTION_KEY=my-encryption-key -BACKEND_IMAGE_TAG=0.6.2 # pin backend image version (default: latest) -FRONTEND_IMAGE_TAG=0.6.2 # pin frontend image version (default: latest) +BACKEND_IMAGE_TAG=0.7.8 # pin backend image version (default: latest) +FRONTEND_IMAGE_TAG=0.7.8 # pin frontend image version (default: latest) # BACKEND_HOST=backend:3000 # target for the frontend's /api proxy (default: backend:3000) ``` diff --git a/DOCKER_zh.md b/DOCKER_zh.md index cf9631ed..eeb6fc4e 100644 --- a/DOCKER_zh.md +++ b/DOCKER_zh.md @@ -40,8 +40,8 @@ docker compose up -d ```bash API_SECRET=your-api-secret ENCRYPTION_KEY=your-encryption-key -BACKEND_IMAGE_TAG=0.7.0 -FRONTEND_IMAGE_TAG=0.7.0 +BACKEND_IMAGE_TAG=0.7.8 +FRONTEND_IMAGE_TAG=0.7.8 # BACKEND_HOST=backend:3000 ``` @@ -83,7 +83,7 @@ curl http://localhost:8080/api/health 如需固定版本,在 `.env` 中设置: ```bash -IMAGE_TAG=0.7.0 +IMAGE_TAG=0.7.8 API_SECRET=your-api-secret ENCRYPTION_KEY=your-encryption-key ``` @@ -123,9 +123,16 @@ docker run -d \ docker volume ls ``` -将下方的 `` 替换为实际卷名。以下命令会在当前目录创建一个同时包含 SQLite 数据库和 `.encryption-key` 的归档: +将下方的 `` 替换为实际卷名。请先停止所有 SQLite 写入,再创建包含数据库和 `.encryption-key` 的归档;这样不会在写入期间复制数据库与 WAL/journal 文件。 ```bash +# 停止分离部署,但不要添加 -v;该参数会删除具名数据卷。 +# 默认 Compose 项目名: +docker compose down +# 如原部署使用自定义项目名,后续所有命令均使用同一项目名: +# docker compose -p down + +# 数据库静止后创建可移植备份。 docker run --rm \ -v :/data:ro \ -v "$PWD":/backup \ @@ -134,19 +141,12 @@ docker run --rm \ 请确认 `github-stars-manager-data-backup.tgz` 已生成,再继续下一步。 -### 2. 停止分离部署,但不要删除卷 - -```bash -# 不要添加 -v;该参数会删除具名数据卷。 -docker compose down -``` - ### 3. 使用相同 Compose 项目名启动全栈容器 两个 Compose 文件都声明了 `backend-data` 卷。只要在**同一目录**下执行,并沿用相同的 Compose 项目名,全栈部署会复用原有 SQLite 数据和加密密钥。 ```bash -# 默认项目名 +# 先在 .env 设置 API_SECRET。默认项目名: docker compose -f docker-compose.fullstack.yml up -d # 如原部署使用自定义项目名,请保持一致 @@ -171,14 +171,23 @@ MCP Token 和 `API_SECRET` 仍是两个独立的凭据。迁移只更换容器 ## 回滚到前后端分离部署 -如果需要回滚,停止全栈容器后重新启动原有 Compose 服务即可。不要使用 `-v`,这样同一数据卷仍会被保留。 +如果全栈服务通过 Compose 启动,停止该服务后重新启动原有 Compose 服务即可。如果全栈服务通过直接 `docker run` 创建,则先停止并删除该容器,再启动 Compose。不要使用 `-v`,这样同一数据卷仍会被保留。 ```bash +# 通过 Compose 启动的全栈服务: docker compose -f docker-compose.fullstack.yml down +# 自定义项目名:docker compose -p -f docker-compose.fullstack.yml down + +# 通过直接 docker run 启动的全栈服务(使用本组命令替代上面的 Compose down): +# docker stop github-stars-manager-fullstack +# docker rm github-stars-manager-fullstack + +# 恢复原有前后端分离部署: docker compose up -d +# 自定义项目名:docker compose -p up -d ``` -若部署时使用了自定义 Compose 项目名,请在两条命令中都添加同一个 `-p `。只要保留 `/app/data` 对应的具名卷,回滚后现有数据、加密密钥与 MCP 配置都会继续可用。 +只要保留 `/app/data` 对应的具名卷,回滚后现有数据、加密密钥与 MCP 配置都会继续可用。 ## 环境变量 diff --git a/README.md b/README.md index 10566352..0cdfc33e 100644 --- a/README.md +++ b/README.md @@ -274,8 +274,8 @@ To customize, create a `.env` file: ```bash API_SECRET=your-secret ENCRYPTION_KEY=your-key -BACKEND_IMAGE_TAG=0.6.2 # pin backend image version (default: latest) -FRONTEND_IMAGE_TAG=0.6.2 # pin frontend image version (default: latest) +BACKEND_IMAGE_TAG=0.7.8 # pin backend image version (default: latest) +FRONTEND_IMAGE_TAG=0.7.8 # pin frontend image version (default: latest) ``` #### Backend only (docker run) diff --git a/README_zh.md b/README_zh.md index 6d60840a..3132b7b3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -426,8 +426,8 @@ docker-compose up -d ```bash API_SECRET=your-secret ENCRYPTION_KEY=your-key -BACKEND_IMAGE_TAG=0.6.2 # 固定后端版本(默认:latest) -FRONTEND_IMAGE_TAG=0.6.2 # 固定前端版本(默认:latest) +BACKEND_IMAGE_TAG=0.7.8 # 固定后端版本(默认:latest) +FRONTEND_IMAGE_TAG=0.7.8 # 固定前端版本(默认:latest) ``` #### 仅后端(docker run) diff --git a/server/src/services/staticFrontend.ts b/server/src/services/staticFrontend.ts index e22b7dff..1d0e3052 100644 --- a/server/src/services/staticFrontend.ts +++ b/server/src/services/staticFrontend.ts @@ -4,6 +4,7 @@ import path from 'node:path'; const backendPathPrefixes = ['/api', '/mcp', '/sse', '/messages']; +/** Returns true when a URL belongs to an API, MCP, or SSE endpoint namespace. */ function isBackendPath(requestPath: string): boolean { return backendPathPrefixes.some( (prefix) => requestPath === prefix || requestPath.startsWith(`${prefix}/`) diff --git a/server/tests/services/staticFrontend.test.ts b/server/tests/services/staticFrontend.test.ts index 6c667d63..525ef036 100644 --- a/server/tests/services/staticFrontend.test.ts +++ b/server/tests/services/staticFrontend.test.ts @@ -8,6 +8,7 @@ import { mountStaticFrontend } from '../../src/services/staticFrontend.js'; const temporaryDirectories: string[] = []; +/** Creates a temporary SPA distribution that also contains an API-path collision. */ function createStaticDirectory(): string { const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'gsm-static-')); temporaryDirectories.push(directory); From bc73b7e4ea8ab947954b2bd6c4af57e818ad73c2 Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:00:38 +0000 Subject: [PATCH 6/8] fix(docs): harden full-stack migration guidance --- .github/workflows/build-desktop.yml | 13 ++++++++++++- DOCKER_zh.md | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index a68b42ce..5f16e6b4 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -8,6 +8,11 @@ on: branches: [ main, master ] workflow_dispatch: +# Pull requests and ordinary builds receive only repository read access. The +# release job elevates this explicitly when it creates a tagged GitHub release. +permissions: + contents: read + jobs: verify-release-version: name: Verify release tag matches client version @@ -15,6 +20,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + persist-credentials: false - name: Verify tagged release version if: github.ref_type == 'tag' @@ -33,6 +40,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v6 + with: + persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v6 @@ -399,7 +408,9 @@ jobs: - name: Build Electron app run: npm run dist env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Only tagged release builds receive a token; PR and branch builds never + # expose a credential to package scripts or electron-builder hooks. + GH_TOKEN: ${{ github.ref_type == 'tag' && secrets.GITHUB_TOKEN || '' }} CI: true DEBUG: electron-builder # Linux 特定环境变量 diff --git a/DOCKER_zh.md b/DOCKER_zh.md index eeb6fc4e..a6de5006 100644 --- a/DOCKER_zh.md +++ b/DOCKER_zh.md @@ -108,6 +108,8 @@ docker run -d \ --name github-stars-manager-fullstack \ -p 8080:3000 \ -v github-stars-data:/app/data \ + -e API_SECRET="your-api-secret" \ + -e ENCRYPTION_KEY="your-encryption-key" \ github-stars-manager-fullstack:local ``` From a6ef2b3034228b347058ed2661ea44f063329fca Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:08:33 +0000 Subject: [PATCH 7/8] fix(docs): preserve credentials during full-stack migration --- DOCKER.md | 19 +++++++++++++------ DOCKER_zh.md | 15 ++++++++++----- README.md | 3 ++- README_zh.md | 3 ++- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 61f0f6e4..42beb75f 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -58,28 +58,34 @@ docker compose -f docker-compose.fullstack.yml up -d curl http://localhost:8080/api/health ``` -You can also run the full-stack image directly. The data volume stores both SQLite data and the automatically generated encryption key, so keep the `-v` option when upgrading or recreating the container. +You can also run the full-stack image directly. The data volume stores SQLite data and an automatically generated encryption key, so keep the `-v` option when upgrading or recreating the container. A direct `docker run` does not load Compose's `.env` file; export `IMAGE_TAG` (or replace it inline) to pin the image version. ```bash +# Set this to 0.7.8 for a version-pinned deployment, or latest for main. +export IMAGE_TAG=0.7.8 + docker run -d \ --name github-stars-manager-fullstack \ -p 8080:3000 \ -v github-stars-data:/app/data \ -e API_SECRET="your-secret-here" \ - -e ENCRYPTION_KEY="your-encryption-key" \ - ghcr.io/amintacccp/github-stars-manager-fullstack:latest + ghcr.io/amintacccp/github-stars-manager-fullstack:${IMAGE_TAG} ``` -Add `IMAGE_TAG` to the same `.env` file to pin a full-stack version: +For a new deployment, omit `ENCRYPTION_KEY` and the service generates a key in the persisted data volume. If an existing deployment already uses `ENCRYPTION_KEY`, always pass the **exact same value** on every recreation and during migration; an environment-provided key overrides the file key, and changing it makes already encrypted credentials unreadable. + +For Compose deployments, add `IMAGE_TAG` to the same `.env` file to pin a full-stack version: ```bash API_SECRET=replace-with-a-long-random-secret IMAGE_TAG=0.7.8 +# Set this only when preserving an existing environment-provided key. +# ENCRYPTION_KEY=the-exact-existing-key ``` ### Migrate an Existing Docker Compose Deployment -Migration is optional. Existing frontend-plus-backend deployments continue to work and require no action. To migrate, first back up the current Docker volume. Replace `` with the volume name returned by `docker volume ls`; when Compose is run from this repository with its default project name, it normally ends in `_backend-data`. +Migration is optional. Existing frontend-plus-backend deployments continue to work and require no action. Before migration, preserve the exact existing `API_SECRET` in the full-stack `.env`; this keeps current browser, API, and MCP clients authenticated without reconfiguration. If the existing service explicitly sets `ENCRYPTION_KEY`, copy the **same value** into the full-stack `.env` as well. Replace `` with the volume name returned by `docker volume ls`; when Compose is run from this repository with its default project name, it normally ends in `_backend-data`. ```bash # Stop all SQLite writers without deleting the named data volume. @@ -94,7 +100,8 @@ docker run --rm \ -v "$PWD":/backup \ alpine tar czf /backup/github-stars-manager-data-backup.tgz -C /data . -# Set API_SECRET in .env before starting the new network-facing service. +# Copy the current API_SECRET into .env before startup; copy the same +# ENCRYPTION_KEY too when the old service set one explicitly. # Reuse the same Compose project directory and volume name. docker compose -f docker-compose.fullstack.yml up -d # For a custom project: docker compose -p -f docker-compose.fullstack.yml up -d diff --git a/DOCKER_zh.md b/DOCKER_zh.md index a6de5006..a873f060 100644 --- a/DOCKER_zh.md +++ b/DOCKER_zh.md @@ -88,18 +88,22 @@ API_SECRET=your-api-secret ENCRYPTION_KEY=your-encryption-key ``` -不使用 Compose 时,可直接运行镜像: +不使用 Compose 时,可直接运行镜像。直接执行 `docker run` 不会读取 Compose 的 `.env`,请导出 `IMAGE_TAG`(或在命令中直接替换)以固定镜像版本: ```bash +# 固定到客户端同版本的镜像;也可以改为 latest 使用 main 的最新构建。 +export IMAGE_TAG=0.7.8 + docker run -d \ --name github-stars-manager-fullstack \ -p 8080:3000 \ -v github-stars-data:/app/data \ -e API_SECRET="your-api-secret" \ - -e ENCRYPTION_KEY="your-encryption-key" \ - ghcr.io/amintacccp/github-stars-manager-fullstack:latest + ghcr.io/amintacccp/github-stars-manager-fullstack:${IMAGE_TAG} ``` +新部署可以不传 `ENCRYPTION_KEY`,服务会在持久化数据卷中生成并保存密钥。如果已有部署显式设置了 `ENCRYPTION_KEY`,每次重建和迁移时都必须传入**完全相同的值**;环境变量密钥优先于数据卷内的文件密钥,变更它会导致原先加密的凭据无法读取。 + 本地构建全栈镜像时,请明确指定新的 Dockerfile: ```bash @@ -115,7 +119,7 @@ docker run -d \ ## 从现有 Compose 部署迁移到单容器 -迁移是**可选的**。如果当前前后端分离部署运行正常,您无需执行任何操作。只有在希望简化为一个容器时才迁移。 +迁移是**可选的**。如果当前前后端分离部署运行正常,您无需执行任何操作。只有在希望简化为一个容器时才迁移。迁移前,请将当前的 `API_SECRET` 原样写入全栈 `.env`,这样现有浏览器、API 与 MCP 客户端无需重新配置;如果旧服务显式设置过 `ENCRYPTION_KEY`,也必须在全栈 `.env` 中写入**完全相同的值**。 ### 1. 识别并备份现有数据卷 @@ -148,7 +152,8 @@ docker run --rm \ 两个 Compose 文件都声明了 `backend-data` 卷。只要在**同一目录**下执行,并沿用相同的 Compose 项目名,全栈部署会复用原有 SQLite 数据和加密密钥。 ```bash -# 先在 .env 设置 API_SECRET。默认项目名: +# 在 .env 中原样保留现有 API_SECRET;如旧服务显式设置了 ENCRYPTION_KEY,也原样保留。 +# 默认项目名: docker compose -f docker-compose.fullstack.yml up -d # 如原部署使用自定义项目名,请保持一致 diff --git a/README.md b/README.md index 0cdfc33e..0f6fd756 100644 --- a/README.md +++ b/README.md @@ -245,9 +245,10 @@ docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` -An additional **optional full-stack image** (`ghcr.io/amintacccp/github-stars-manager-fullstack`) is available for users who prefer one container, one image tag, and one persistent data volume. It serves the same web UI, `/api`, and MCP endpoints from one origin: +An additional **optional full-stack image** (`ghcr.io/amintacccp/github-stars-manager-fullstack`) is available for users who prefer one container, one image tag, and one persistent data volume. It serves the same web UI, `/api`, and MCP endpoints from one origin. Set `API_SECRET` in a root `.env` file first; the full-stack Compose file refuses to start a new unauthenticated deployment: ```bash +API_SECRET=replace-with-a-long-random-secret docker compose -f docker-compose.fullstack.yml up -d ``` diff --git a/README_zh.md b/README_zh.md index 3132b7b3..e65b9e1d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -398,9 +398,10 @@ docker pull ghcr.io/amintacccp/github-stars-manager-frontend:latest docker-compose up -d ``` -此外,项目新增了一个**可选的全栈单镜像**(`ghcr.io/amintacccp/github-stars-manager-fullstack`),适合希望只运行一个容器、一个镜像标签和一个数据卷的用户。它在同一来源下提供网页、`/api` 和 MCP 端点: +此外,项目新增了一个**可选的全栈单镜像**(`ghcr.io/amintacccp/github-stars-manager-fullstack`),适合希望只运行一个容器、一个镜像标签和一个数据卷的用户。它在同一来源下提供网页、`/api` 和 MCP 端点。先在仓库根目录的 `.env` 设置 `API_SECRET`,全栈 Compose 会拒绝在无认证配置下启动: ```bash +API_SECRET=替换为足够长的随机密钥 docker compose -f docker-compose.fullstack.yml up -d ``` From f1f0a2c9cd9401d9ffd65b9f2e96317fe76a33af Mon Sep 17 00:00:00 2001 From: AmintaCCCP <7.4942183e+07+AmintaCCCP@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:21:11 +0000 Subject: [PATCH 8/8] fix(release): block invalid desktop releases --- .github/workflows/build-desktop.yml | 6 ++++-- DOCKER.md | 2 +- DOCKER_zh.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 5f16e6b4..8022a8d3 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -475,9 +475,11 @@ jobs: if-no-files-found: ignore release: - needs: build + needs: [verify-release-version, build] runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') && always() + # Keep partial platform releases possible, but never publish when the tag + # does not match the root package.json client version. + if: always() && startsWith(github.ref, 'refs/tags/v') && needs.verify-release-version.result == 'success' permissions: contents: write diff --git a/DOCKER.md b/DOCKER.md index 42beb75f..758bb513 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -85,7 +85,7 @@ IMAGE_TAG=0.7.8 ### Migrate an Existing Docker Compose Deployment -Migration is optional. Existing frontend-plus-backend deployments continue to work and require no action. Before migration, preserve the exact existing `API_SECRET` in the full-stack `.env`; this keeps current browser, API, and MCP clients authenticated without reconfiguration. If the existing service explicitly sets `ENCRYPTION_KEY`, copy the **same value** into the full-stack `.env` as well. Replace `` with the volume name returned by `docker volume ls`; when Compose is run from this repository with its default project name, it normally ends in `_backend-data`. +Migration is optional. Existing frontend-plus-backend deployments continue to work and require no action. If the current backend has an `API_SECRET`, preserve the exact value in the full-stack `.env`; this keeps current browser, API, and MCP clients authenticated without reconfiguration. If the existing backend has no `API_SECRET`, generate a strong new value in the full-stack `.env` and configure every direct API and MCP client with it after cutover; the full-stack Compose file intentionally does not start unauthenticated. If the existing service explicitly sets `ENCRYPTION_KEY`, copy the **same value** into the full-stack `.env` as well. Replace `` with the volume name returned by `docker volume ls`; when Compose is run from this repository with its default project name, it normally ends in `_backend-data`. ```bash # Stop all SQLite writers without deleting the named data volume. diff --git a/DOCKER_zh.md b/DOCKER_zh.md index a873f060..71287d62 100644 --- a/DOCKER_zh.md +++ b/DOCKER_zh.md @@ -119,7 +119,7 @@ docker run -d \ ## 从现有 Compose 部署迁移到单容器 -迁移是**可选的**。如果当前前后端分离部署运行正常,您无需执行任何操作。只有在希望简化为一个容器时才迁移。迁移前,请将当前的 `API_SECRET` 原样写入全栈 `.env`,这样现有浏览器、API 与 MCP 客户端无需重新配置;如果旧服务显式设置过 `ENCRYPTION_KEY`,也必须在全栈 `.env` 中写入**完全相同的值**。 +迁移是**可选的**。如果当前前后端分离部署运行正常,您无需执行任何操作。只有在希望简化为一个容器时才迁移。若当前后端已经设置 `API_SECRET`,请将其原样写入全栈 `.env`,这样现有浏览器、API 与 MCP 客户端无需重新配置。若旧后端未设置 `API_SECRET`,请在全栈 `.env` 生成一个新的高强度密钥,并在切换后为所有直接 API 与 MCP 客户端配置该密钥;全栈 Compose 不会允许以无认证状态启动。如果旧服务显式设置过 `ENCRYPTION_KEY`,也必须在全栈 `.env` 中写入**完全相同的值**。 ### 1. 识别并备份现有数据卷