Compare commits

..

2 Commits

Author SHA1 Message Date
LanQin e7e4f43691 ci(.github/workflows): 拆分 CI 与发布流程。
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
- 新增独立的 CI 工作流,覆盖前端检查、构建和后端测试。
- 调整 Docker 工作流为仅负责标签发布与镜像构建。
- 同步优化管理页 Issues 图标颜色。
2026-06-21 02:30:01 +08:00
LanQin c872ed559d feat(admin): 增加关于页与发布版本信息
- 在系统设置中新增“关于”标签页,提供版本、仓库、Issues、文档和开源协议入口。
- 通过 Docker 构建参数注入版本号与发布链接,并在前端展示当前发布版本。
- 调整 GitHub Actions 与镜像构建流程,仅在 tag 发布时构建并发布带版本标识的镜像。
2026-06-21 02:23:40 +08:00
6 changed files with 192 additions and 22 deletions
+61
View File
@@ -0,0 +1,61 @@
name: CI
on:
push:
branches:
- main
paths-ignore:
- "**/*.md"
- "deploy/.env.example"
pull_request:
branches:
- main
paths-ignore:
- "**/*.md"
- "deploy/.env.example"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
checks:
name: Check web and api
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.28.2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install web dependencies
run: pnpm install --frozen-lockfile --filter lanqin-email-web...
- name: Check shadcn/ui usage
run: pnpm --dir apps/web run check:shadcn
- name: Build web
run: pnpm --dir apps/web run build
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: apps/api/go.mod
cache-dependency-path: apps/api/go.sum
- name: Test api
working-directory: apps/api
run: go test ./...
+32 -18
View File
@@ -1,21 +1,9 @@
name: CI and Docker
name: Docker Release
on:
push:
branches:
- main
tags:
- "v*"
paths-ignore:
- "**/*.md"
- "deploy/.env.example"
pull_request:
branches:
- main
paths-ignore:
- "**/*.md"
- "deploy/.env.example"
workflow_dispatch:
permissions:
contents: read
@@ -68,11 +56,31 @@ jobs:
working-directory: apps/api
run: go test ./...
release:
name: Resolve release tag
runs-on: ubuntu-latest
needs: checks
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}
release_url: ${{ steps.release.outputs.release_url }}
steps:
- name: Resolve tag version
id: release
shell: bash
run: |
tag="${GITHUB_REF_NAME}"
version="${tag#v}"
release_url="https://github.com/${GITHUB_REPOSITORY}/releases/tag/${tag}"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "release_url=${release_url}" >> "$GITHUB_OUTPUT"
echo "::notice::Docker release tag: ${tag}"
docker:
name: Build and publish ${{ matrix.name }}
runs-on: ubuntu-latest
needs: checks
if: github.event_name != 'pull_request'
needs: release
strategy:
fail-fast: false
matrix:
@@ -124,6 +132,8 @@ jobs:
run: |
image="${REGISTRY}/${GITHUB_REPOSITORY}${{ matrix.suffix }}"
echo "name=${image,,}" >> "$GITHUB_OUTPUT"
echo "tag=${{ needs.release.outputs.tag }}" >> "$GITHUB_OUTPUT"
echo "version=${{ needs.release.outputs.version }}" >> "$GITHUB_OUTPUT"
- name: Docker metadata
id: meta
@@ -131,12 +141,13 @@ jobs:
with:
images: ${{ steps.image.outputs.name }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch
type=ref,event=tag
type=raw,value=${{ steps.image.outputs.tag }}
type=raw,value=${{ steps.image.outputs.version }}
type=raw,value=latest
type=sha,prefix=sha-
labels: |
org.opencontainers.image.title=LanQin Email ${{ matrix.name }}
org.opencontainers.image.version=${{ steps.image.outputs.tag }}
- name: Build and push
uses: docker/build-push-action@v6
@@ -145,5 +156,8 @@ jobs:
file: ${{ matrix.file }}
platforms: ${{ env.PLATFORMS }}
push: true
build-args: |
VITE_APP_VERSION=${{ needs.release.outputs.tag }}
VITE_RELEASE_URL=${{ needs.release.outputs.release_url }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+86 -4
View File
@@ -2,7 +2,7 @@ import * as React from "react"
import DOMPurify from "dompurify"
import { useSearchParams } from "react-router-dom"
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { ArrowRight, CheckCircle2, Circle, Copy, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Search, ShieldCheck, Trash2, Users } from "lucide-react"
import { ArrowRight, BookOpen, CheckCircle2, Circle, Copy, GitBranch, Github, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Scale, Search, ShieldCheck, Star, Trash2, Users } from "lucide-react"
import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, SystemSettings } from "@/lib/api"
import { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -35,6 +35,9 @@ const sectionLabels: Record<Section, string> = {
settings: "系统设置",
}
const sectionKeys = Object.keys(sectionLabels) as Section[]
const projectRepositoryUrl = "https://github.com/LanQin996/LanQin-Email"
const projectTag = import.meta.env.VITE_APP_VERSION || ""
const projectReleaseUrl = import.meta.env.VITE_RELEASE_URL || (projectTag ? `${projectRepositoryUrl}/releases/tag/${projectTag}` : "")
export function AdminPage() {
const overview = useQuery({ queryKey: ["admin", "overview"], queryFn: api.adminOverview })
@@ -534,7 +537,7 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
const qc = useQueryClient()
const { toast } = useToast()
const templates = useQuery({ queryKey: ["admin", "mail-templates"], queryFn: api.mailTemplates })
const [settingsTab, setSettingsTab] = React.useState<"base" | "smtp" | "storage" | "mail" | "templates" | "security">("base")
const [settingsTab, setSettingsTab] = React.useState<"base" | "smtp" | "storage" | "mail" | "templates" | "security" | "about">("base")
const [smtpRequireTls, setSmtpRequireTls] = React.useState(false)
const [allowInsecureHttp, setAllowInsecureHttp] = React.useState(true)
const [openRegistration, setOpenRegistration] = React.useState(false)
@@ -620,6 +623,7 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
{ key: "mail", label: "邮件" },
{ key: "templates", label: "模板" },
{ key: "security", label: "安全" },
{ key: "about", label: "关于" },
]
return (
<form key={formKey} onSubmit={(event) => { event.preventDefault(); save.mutate(new FormData(event.currentTarget)) }} className="space-y-6">
@@ -736,13 +740,91 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
</CardContent>
</Card>}
<div className="flex justify-end">
{settingsTab === "about" && <AboutProjectCard />}
{settingsTab !== "about" && <div className="flex justify-end">
<Button disabled={save.isPending || !settings}>{save.isPending ? "保存中..." : "保存设置"}</Button>
</div>
</div>}
</form>
)
}
function AboutProjectCard() {
return (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4 text-sm">
<AboutRow label="版本">
{projectTag ? (
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
<a href={projectReleaseUrl} target="_blank" rel="noreferrer">
<GitBranch className="h-5 w-5 text-primary" />
{projectTag}
</a>
</Button>
) : (
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" disabled>
<GitBranch className="h-5 w-5 text-muted-foreground" />
</Button>
)}
</AboutRow>
<AboutRow label="交流">
<div className="flex flex-wrap gap-3">
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
<a href={projectRepositoryUrl} target="_blank" rel="noreferrer">
<Github className="h-5 w-5" />
GitHub
</a>
</Button>
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
<a href={`${projectRepositoryUrl}/issues`} target="_blank" rel="noreferrer">
<Circle className="h-5 w-5 text-muted-foreground" />
Issues
</a>
</Button>
</div>
</AboutRow>
<AboutRow label="支持">
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
<a href={projectRepositoryUrl} target="_blank" rel="noreferrer">
<Star className="h-5 w-5 text-yellow-500" />
Star
</a>
</Button>
</AboutRow>
<AboutRow label="帮助">
<div className="flex flex-wrap gap-3">
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
<a href={`${projectRepositoryUrl}#readme`} target="_blank" rel="noreferrer">
<BookOpen className="h-5 w-5 text-sky-500" />
</a>
</Button>
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
<a href={`${projectRepositoryUrl}/blob/main/LICENSE`} target="_blank" rel="noreferrer">
<Scale className="h-5 w-5 text-emerald-500" />
</a>
</Button>
</div>
</AboutRow>
</CardContent>
</Card>
)
}
function AboutRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="grid gap-2 sm:grid-cols-[4.5rem_minmax(0,1fr)] sm:items-center">
<div className="font-medium text-muted-foreground">{label}</div>
<div className="min-w-0">{children}</div>
</div>
)
}
function TestSMTPDialog({ disabled }: { disabled?: boolean }) {
const { toast } = useToast()
const [open, setOpen] = React.useState(false)
+9
View File
@@ -1 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_VERSION?: string
readonly VITE_RELEASE_URL?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+2
View File
@@ -18,6 +18,8 @@ RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --filter lanqin-email-web...
COPY apps/web apps/web
ARG VITE_APP_VERSION=""
ARG VITE_RELEASE_URL=""
RUN pnpm --dir apps/web run build
FROM debian:bookworm-slim
+2
View File
@@ -8,6 +8,8 @@ RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --filter lanqin-email-web...
COPY apps/web apps/web
ARG VITE_APP_VERSION=""
ARG VITE_RELEASE_URL=""
RUN pnpm --dir apps/web run build
FROM nginx:1.27-alpine