mirror of
https://github.com/Gloridust/WechatOnCloud.git
synced 2026-06-16 19:53:53 +08:00
feat(panel): 显示构建版本号 + 自动检测新版(Docker Hub/GHCR 红点)
管理界面此前看不到面板版本,也无从知道有没有新版可升。现在: - 构建时把版本号烤进面板镜像:Dockerfile 新增 ARG/ENV WOC_VERSION(放末尾不 破坏依赖缓存);release.yml 用 git tag 注入(vX.Y.Z,手动触发为 dev-<sha>), 仅面板镜像消费;build-local.sh 支持 --build-arg(默认 dev)。 - 后端 version.ts:best-effort 查询 Docker Hub 与 GHCR 上 woc-panel 的语义化 标签取最大值,与当前版本比对;启动后 4s 首检 + 每 6h 复检 + 接口惰性触发, 失败静默(离线/被墙/私有源不报错、不显红点)。命名空间从 WOC_WECHAT_IMAGE 推断。 - 接口:GET /api/version(任意登录用户读缓存)、POST /api/admin/version/check (管理员手动重查)。 - 前端:管理页「关于」卡显示当前版本/最新版/升级提示/检查更新/发布日志链接; 侧栏「管理」入口在有新版时点红点(仅管理员)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,16 @@ jobs:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# 解析烤进面板镜像的版本号:打 tag / 发 Release 时用 tag 名(vX.Y.Z),手动触发则用 dev-<短SHA>。
|
||||
- name: Resolve build version
|
||||
id: ver
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
echo "version=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "version=dev-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Docker metadata (tags + labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -85,6 +95,8 @@ jobs:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# 仅面板镜像消费 WOC_VERSION(实例镜像不展示版本号),避免「未使用的 build-arg」告警。
|
||||
build-args: ${{ matrix.name == 'panel' && format('WOC_VERSION={0}', steps.ver.outputs.version) || '' }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
+5
-1
@@ -17,7 +17,11 @@ RUN npm install
|
||||
COPY server/ ./
|
||||
COPY --from=web /web/dist ./web-dist
|
||||
|
||||
ENV STATIC_DIR=/app/web-dist \
|
||||
# 构建版本号:CI 用 git tag 注入(vX.Y.Z),本地构建默认 dev。烤进镜像 → 面板运行时显示真实版本并据此检测更新。
|
||||
# 放在末尾:改版本号不会破坏上面的依赖安装缓存。
|
||||
ARG WOC_VERSION=dev
|
||||
ENV WOC_VERSION=${WOC_VERSION} \
|
||||
STATIC_DIR=/app/web-dist \
|
||||
PORT=8080
|
||||
EXPOSE 8080
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
} from './docker.js';
|
||||
import { createSession, getSession, destroySession, destroyUserSessions } from './sessions.js';
|
||||
import { parseHost, parseAllowedHosts, isRequestHostAllowed } from './host-guard.js';
|
||||
import { CURRENT_VERSION, versionInfo, ensureChecked, checkForUpdate, startUpdateChecker } from './version.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -172,6 +173,19 @@ app.get('/api/auth/me', async (req, reply) => {
|
||||
return { user: publicUser(u) };
|
||||
});
|
||||
|
||||
// ---------- 版本与更新检测 ----------
|
||||
// 当前构建版本 + 缓存的「最新版」检测结果(后台每 6h 查一次 Docker Hub/GHCR)。任何登录用户可读。
|
||||
app.get('/api/version', async (req, reply) => {
|
||||
if (!requireAuth(req, reply)) return;
|
||||
ensureChecked(); // 刚启动还没首检时,触发一次后台检查(不阻塞本次响应)
|
||||
return versionInfo();
|
||||
});
|
||||
// 立即重新检查(管理员,用于「检查更新」按钮)。
|
||||
app.post('/api/admin/version/check', async (req, reply) => {
|
||||
if (!requireAdmin(req, reply)) return;
|
||||
return await checkForUpdate();
|
||||
});
|
||||
|
||||
// ---------- 自助改密 ----------
|
||||
app.post('/api/account/password', async (req, reply) => {
|
||||
const u = requireAuth(req, reply);
|
||||
@@ -1083,4 +1097,5 @@ if (WATCHDOG_ENABLED) {
|
||||
}
|
||||
|
||||
await app.listen({ port: PORT, host: HOST });
|
||||
console.log(`[panel] 监听 http://${HOST}:${PORT} (多实例反代已就绪)`);
|
||||
console.log(`[panel] 监听 http://${HOST}:${PORT} (多实例反代已就绪)· 版本 ${CURRENT_VERSION}`);
|
||||
startUpdateChecker(); // 后台检测新版(best-effort,失败静默)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// 面板版本与更新检测。
|
||||
// 构建时把版本号烤进镜像(Dockerfile: ARG/ENV WOC_VERSION,由 CI 用 git tag 注入,本地构建为 dev);
|
||||
// 运行时查询 Docker Hub 与 GHCR 上 woc-panel 的最新语义化标签,比对后给前端「有新版」红点。
|
||||
// 全程 best-effort:离线 / 被墙 / 私有源拉取失败时不报错、不打扰,仅不显示红点(记 error 供「上次检查」提示)。
|
||||
|
||||
export const CURRENT_VERSION = (process.env.WOC_VERSION || 'dev').trim();
|
||||
|
||||
export interface VersionInfo {
|
||||
current: string; // 当前构建版本(如 v1.2.0 / dev)
|
||||
latest: string | null; // 仓库上最新发布版(如 v1.2.1);查不到为 null
|
||||
hasUpdate: boolean; // 当前能解析为语义化版本且 latest > current 时为 true
|
||||
checkedAt: number; // 上次检查时间戳(ms);0 = 尚未检查
|
||||
source: string | null; // 数据来源:dockerhub / ghcr / dockerhub+ghcr
|
||||
error: string | null; // 检查失败原因(两个源都拉不到时)
|
||||
}
|
||||
|
||||
// 镜像命名空间:从 WOC_WECHAT_IMAGE 推断(面板与实例镜像同账号)。
|
||||
// 例 docker.io/gloridust/wechat-on-cloud:latest → gloridust;ghcr.io/gloridust/... 同理。
|
||||
function imageOwner(): string {
|
||||
const img = (process.env.WOC_WECHAT_IMAGE || 'docker.io/gloridust/wechat-on-cloud').split('@')[0];
|
||||
const segs = img.split('/'); // [registry?, owner, image[:tag]]
|
||||
return segs.length >= 2 ? segs[segs.length - 2] : 'gloridust';
|
||||
}
|
||||
const PANEL_REPO = process.env.WOC_PANEL_REPO || 'woc-panel';
|
||||
|
||||
function parseSemver(s: string): [number, number, number] | null {
|
||||
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(s.trim());
|
||||
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
||||
}
|
||||
function cmpSemver(a: [number, number, number], b: [number, number, number]): number {
|
||||
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
||||
}
|
||||
// 从一堆标签里挑出最大的 x.y.z(忽略 latest / x / x.y 等非完整语义化标签)。
|
||||
function maxSemver(tags: string[]): string | null {
|
||||
let best: [number, number, number] | null = null;
|
||||
for (const t of tags) {
|
||||
const v = parseSemver(t);
|
||||
if (v && (!best || cmpSemver(v, best) > 0)) best = v;
|
||||
}
|
||||
return best ? `${best[0]}.${best[1]}.${best[2]}` : null;
|
||||
}
|
||||
|
||||
async function getJson(url: string, headers: Record<string, string>, timeoutMs: number): Promise<any> {
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'user-agent': 'woc-panel-update-check', ...headers }, signal: ctrl.signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return await res.json();
|
||||
} finally {
|
||||
clearTimeout(t);
|
||||
}
|
||||
}
|
||||
|
||||
// Docker Hub 公共 API:免鉴权列标签。
|
||||
async function dockerHubTags(owner: string): Promise<string[]> {
|
||||
const d = await getJson(`https://hub.docker.com/v2/repositories/${owner}/${PANEL_REPO}/tags?page_size=100`, {}, 8000);
|
||||
return Array.isArray(d?.results) ? d.results.map((r: any) => String(r?.name || '')) : [];
|
||||
}
|
||||
// GHCR 公共镜像:先取匿名 pull token,再走 registry v2 tags/list。
|
||||
async function ghcrTags(owner: string): Promise<string[]> {
|
||||
const tok = await getJson(`https://ghcr.io/token?scope=repository:${owner}/${PANEL_REPO}:pull&service=ghcr.io`, {}, 8000);
|
||||
const d = await getJson(`https://ghcr.io/v2/${owner}/${PANEL_REPO}/tags/list`, { authorization: `Bearer ${tok?.token || ''}` }, 8000);
|
||||
return Array.isArray(d?.tags) ? d.tags.map((t: any) => String(t)) : [];
|
||||
}
|
||||
|
||||
let cache: VersionInfo = { current: CURRENT_VERSION, latest: null, hasUpdate: false, checkedAt: 0, source: null, error: null };
|
||||
let inflight: Promise<VersionInfo> | null = null;
|
||||
|
||||
export function versionInfo(): VersionInfo {
|
||||
return cache;
|
||||
}
|
||||
|
||||
// 查询两个仓库(并行、互不阻塞),取全局最大语义化版本与当前版本比对。失败静默写入 error。
|
||||
export function checkForUpdate(): Promise<VersionInfo> {
|
||||
if (inflight) return inflight; // 合并并发请求,避免重复外呼
|
||||
inflight = (async () => {
|
||||
const owner = imageOwner();
|
||||
const sources: string[] = [];
|
||||
const tags: string[] = [];
|
||||
const [hub, ghcr] = await Promise.allSettled([dockerHubTags(owner), ghcrTags(owner)]);
|
||||
if (hub.status === 'fulfilled' && hub.value.length) {
|
||||
tags.push(...hub.value);
|
||||
sources.push('dockerhub');
|
||||
}
|
||||
if (ghcr.status === 'fulfilled' && ghcr.value.length) {
|
||||
tags.push(...ghcr.value);
|
||||
sources.push('ghcr');
|
||||
}
|
||||
const latestBare = maxSemver(tags);
|
||||
const cur = parseSemver(CURRENT_VERSION);
|
||||
const latestV = latestBare ? parseSemver(latestBare) : null;
|
||||
const hasUpdate = !!(latestV && cur && cmpSemver(latestV, cur) > 0);
|
||||
cache = {
|
||||
current: CURRENT_VERSION,
|
||||
latest: latestBare ? `v${latestBare}` : null,
|
||||
hasUpdate,
|
||||
checkedAt: Date.now(),
|
||||
source: sources.join('+') || null,
|
||||
error: tags.length ? null : '无法连接镜像仓库(Docker Hub / GHCR)',
|
||||
};
|
||||
return cache;
|
||||
})().finally(() => {
|
||||
inflight = null;
|
||||
});
|
||||
return inflight;
|
||||
}
|
||||
|
||||
// 尚未检查过则触发一次后台检查(不等待)。GET /api/version 调用,保证刚启动也能尽快填上缓存。
|
||||
export function ensureChecked(): void {
|
||||
if (!cache.checkedAt && !inflight) void checkForUpdate().catch(() => {});
|
||||
}
|
||||
|
||||
// 启动后延迟首检(让监听就绪)+ 每 6 小时复检;定时器 unref,不阻止进程退出。
|
||||
export function startUpdateChecker(): void {
|
||||
setTimeout(() => void checkForUpdate().catch(() => {}), 4_000).unref();
|
||||
setInterval(() => void checkForUpdate().catch(() => {}), 6 * 60 * 60 * 1000).unref();
|
||||
}
|
||||
@@ -157,6 +157,17 @@ function Sidebar({ collapsed, onToggleCollapsed }: { collapsed: boolean; onToggl
|
||||
const isAdmin = user?.role === 'admin';
|
||||
const go = (p: string) => nav(p);
|
||||
|
||||
// 有新版时在「管理」入口点个红点(仅管理员,因为升级面板需管理员在宿主操作)。
|
||||
// 依赖 loc.pathname:导航时复查一次(服务端有缓存、开销极小),保证刚启动时首检完成后红点能及时出现。
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!isAdmin) return;
|
||||
api
|
||||
.getVersion()
|
||||
.then((v) => setHasUpdate(!!v.hasUpdate))
|
||||
.catch(() => {});
|
||||
}, [isAdmin, loc.pathname]);
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div className="sb-top">
|
||||
@@ -196,9 +207,17 @@ function Sidebar({ collapsed, onToggleCollapsed }: { collapsed: boolean; onToggl
|
||||
</div>
|
||||
|
||||
<div className="sb-footer">
|
||||
<button className={'sb-item' + (loc.pathname === '/admin' ? ' on' : '')} onClick={() => go('/admin')} title={isAdmin ? '管理' : '设置'}>
|
||||
<span className="sb-ic">{Icon.gear}</span>
|
||||
<button
|
||||
className={'sb-item' + (loc.pathname === '/admin' ? ' on' : '')}
|
||||
onClick={() => go('/admin')}
|
||||
title={isAdmin && hasUpdate ? '管理 · 有新版本可用' : isAdmin ? '管理' : '设置'}
|
||||
>
|
||||
<span className="sb-ic">
|
||||
{Icon.gear}
|
||||
{isAdmin && hasUpdate && <span className="sb-updot" />}
|
||||
</span>
|
||||
{!collapsed && <span className="sb-label">{isAdmin ? '管理' : '设置'}</span>}
|
||||
{!collapsed && isAdmin && hasUpdate && <span className="sb-updot-text">新版</span>}
|
||||
</button>
|
||||
<button
|
||||
className="sb-item"
|
||||
|
||||
@@ -75,6 +75,15 @@ export interface VolEntry {
|
||||
mtime: number; // epoch ms
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
current: string; // 当前构建版本(如 v1.2.0 / dev)
|
||||
latest: string | null; // 仓库上最新发布版(如 v1.2.1);查不到为 null
|
||||
hasUpdate: boolean; // 有更高的语义化版本可用
|
||||
checkedAt: number; // 上次检查时间戳(ms);0=尚未检查
|
||||
source: string | null; // 数据来源:dockerhub / ghcr / dockerhub+ghcr
|
||||
error: string | null; // 检查失败原因
|
||||
}
|
||||
|
||||
// 原始二进制上传(File 直传 application/octet-stream),用于数据卷上传/解压/恢复
|
||||
async function rawUpload(url: string, file: File): Promise<any> {
|
||||
const res = await fetch(url, {
|
||||
@@ -116,6 +125,10 @@ export const api = {
|
||||
changePassword: (oldPassword: string, newPassword: string) =>
|
||||
req('/api/account/password', { method: 'POST', body: JSON.stringify({ oldPassword, newPassword }) }),
|
||||
|
||||
// 版本与更新检测
|
||||
getVersion: () => req<VersionInfo>('/api/version'),
|
||||
checkUpdate: () => req<VersionInfo>('/api/admin/version/check', { method: 'POST' }),
|
||||
|
||||
// 子账号
|
||||
listUsers: () => req<{ users: PanelUser[] }>('/api/admin/users'),
|
||||
createUser: (username: string, password: string, allowedInstances: string[] = []) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Cropper from 'react-easy-crop';
|
||||
import { api, APP_LABELS, appProfile, type PanelUser, type InstanceWithStatus, type VolEntry, type AppType } from '../api';
|
||||
import { api, APP_LABELS, appProfile, type PanelUser, type InstanceWithStatus, type VolEntry, type AppType, type VersionInfo } from '../api';
|
||||
import { InstanceIcon, ICON_CHOICES } from '../AppIcon';
|
||||
import { useUI, PasswordInput } from '../ui';
|
||||
import { useAuth } from '../auth';
|
||||
@@ -80,6 +80,86 @@ function EmptyState({ icon, title, sub, action }: { icon: string; title: string;
|
||||
);
|
||||
}
|
||||
|
||||
const RELEASES_URL = 'https://github.com/Gloridust/WechatOnCloud/releases';
|
||||
|
||||
// 「关于」:显示真实构建版本号 + 检测新版(后台已每 6h 查 Docker Hub/GHCR;这里读缓存并可手动重查)。
|
||||
function AboutSection({ isAdmin }: { isAdmin: boolean }) {
|
||||
const { toast } = useUI();
|
||||
const [info, setInfo] = useState<VersionInfo | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.getVersion().then(setInfo).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const check = async () => {
|
||||
setChecking(true);
|
||||
try {
|
||||
const r = await api.checkUpdate();
|
||||
setInfo(r);
|
||||
if (r.hasUpdate) toast(`发现新版本 ${r.latest}`, 'ok');
|
||||
else if (r.error) toast('检查失败:' + r.error, 'error');
|
||||
else toast('已是最新版本', 'ok');
|
||||
} catch (e: any) {
|
||||
toast(e.message || '检查失败', 'error');
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section-row" style={{ marginTop: 22 }}>
|
||||
<span className="section-title">关于</span>
|
||||
</div>
|
||||
<div className="inst-grid">
|
||||
<div className="inst-card">
|
||||
<div className="inst-head">
|
||||
<span className="inst-name">云微 · WechatOnCloud</span>
|
||||
{info?.hasUpdate && <span className="tag tag-warn">有新版</span>}
|
||||
</div>
|
||||
<div className="inst-sub">
|
||||
当前版本 <b>{info?.current ?? '…'}</b>
|
||||
{info?.hasUpdate && info.latest && (
|
||||
<>
|
||||
{' · '}最新 <b>{info.latest}</b>
|
||||
</>
|
||||
)}
|
||||
{info && !info.hasUpdate && info.latest && !info.error && <>{' · '}已是最新</>}
|
||||
</div>
|
||||
{info?.hasUpdate && (
|
||||
<div className="ver-hint">
|
||||
在宿主执行 <code>docker compose pull && docker compose up -d</code> 升级面板;各实例镜像可在「管理 → 升级」单独更新。
|
||||
</div>
|
||||
)}
|
||||
<div className="inst-actions">
|
||||
{isAdmin && (
|
||||
<button className="btn" disabled={checking} onClick={check}>
|
||||
{checking ? '检查中…' : '检查更新'}
|
||||
</button>
|
||||
)}
|
||||
<a className="btn" href={RELEASES_URL} target="_blank" rel="noreferrer">
|
||||
发布日志
|
||||
</a>
|
||||
{info?.hasUpdate && (
|
||||
<a className="btn btn-primary" href={RELEASES_URL + '/latest'} target="_blank" rel="noreferrer">
|
||||
查看新版
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{info && (
|
||||
<div className="muted small ver-checked">
|
||||
{info.checkedAt ? `上次检查 ${fmtDate(info.checkedAt)}` : '尚未检查'}
|
||||
{info.source && ` · 来源 ${info.source}`}
|
||||
{info.error && ` · ${info.error}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Admin({ onOpenMenu, onChangePassword }: { onOpenMenu: () => void; onChangePassword: () => void }) {
|
||||
const nav = useNavigate();
|
||||
const { user } = useAuth();
|
||||
@@ -446,6 +526,8 @@ export default function Admin({ onOpenMenu, onChangePassword }: { onOpenMenu: ()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AboutSection isAdmin={isAdmin} />
|
||||
</main>
|
||||
|
||||
{creatingUser && (
|
||||
|
||||
@@ -970,6 +970,31 @@ button {
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
/* 「关于」版本卡:有新版时的升级提示 + 上次检查脚注 */
|
||||
.ver-hint {
|
||||
margin-top: 10px;
|
||||
padding: 9px 11px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
color: var(--text);
|
||||
background: rgba(245 158 11 / 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.ver-hint code {
|
||||
font-size: 12px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 5px;
|
||||
background: rgba(var(--shadow) / 0.08);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ver-checked {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.inst-actions a.btn {
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.inst-enter {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1371,6 +1396,7 @@ button {
|
||||
color: var(--wx-green-dark);
|
||||
}
|
||||
.sb-ic {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: 24px;
|
||||
display: flex;
|
||||
@@ -1378,6 +1404,23 @@ button {
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
/* 「有新版」红点:贴在「管理」齿轮图标右上角,折叠/展开都可见 */
|
||||
.sb-updot {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -1px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: #f5483b;
|
||||
border: 2px solid var(--surface);
|
||||
}
|
||||
.sb-updot-text {
|
||||
flex: none;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #f5483b;
|
||||
}
|
||||
.sb-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -10,13 +10,15 @@ set -euo pipefail
|
||||
|
||||
OWNER="${WOC_IMAGE_OWNER:-gloridust}"
|
||||
TAG="${WOC_VERSION:-latest}"
|
||||
# 烤进面板镜像的版本号:设了 WOC_VERSION 就用它(如 v1.2.0),否则 dev(dev 不会触发「有新版」红点)。
|
||||
VER="${WOC_VERSION:-dev}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
PANEL_IMAGE="ghcr.io/${OWNER}/woc-panel:${TAG}"
|
||||
WECHAT_IMAGE="ghcr.io/${OWNER}/wechat-on-cloud:${TAG}"
|
||||
|
||||
echo "==> 构建面板镜像 ${PANEL_IMAGE}"
|
||||
docker build -t "${PANEL_IMAGE}" "${ROOT}/panel"
|
||||
echo "==> 构建面板镜像 ${PANEL_IMAGE} (版本号 ${VER})"
|
||||
docker build --build-arg "WOC_VERSION=${VER}" -t "${PANEL_IMAGE}" "${ROOT}/panel"
|
||||
|
||||
echo "==> 构建微信实例镜像 ${WECHAT_IMAGE}"
|
||||
docker build -t "${WECHAT_IMAGE}" "${ROOT}/docker"
|
||||
|
||||
Reference in New Issue
Block a user