feat: add multi-arch image publishing
This commit is contained in:
+2
-1
@@ -7,13 +7,14 @@ COPY frontend/index.html ./index.html
|
|||||||
RUN npm ci && npm run build
|
RUN npm ci && npm run build
|
||||||
|
|
||||||
FROM golang:1.26.0-bookworm AS backend
|
FROM golang:1.26.0-bookworm AS backend
|
||||||
|
ARG APP_VERSION=0.2.0
|
||||||
WORKDIR /src/backend
|
WORKDIR /src/backend
|
||||||
COPY backend/go.mod backend/go.sum* ./
|
COPY backend/go.mod backend/go.sum* ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY backend/cmd ./cmd
|
COPY backend/cmd ./cmd
|
||||||
COPY backend/internal ./internal
|
COPY backend/internal ./internal
|
||||||
COPY --from=frontend /src/backend/internal/web/dist ./internal/web/dist
|
COPY --from=frontend /src/backend/internal/web/dist ./internal/web/dist
|
||||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/codex-helper ./cmd/server
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X codex-helper/internal/app.Version=${APP_VERSION}" -o /out/codex-helper ./cmd/server
|
||||||
|
|
||||||
FROM node:24.19.0-bookworm-slim AS codex
|
FROM node:24.19.0-bookworm-slim AS codex
|
||||||
ARG CODEX_VERSION=0.147.0
|
ARG CODEX_VERSION=0.147.0
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
| 方法与路径 | 鉴权 | 行为 |
|
| 方法与路径 | 鉴权 | 行为 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `GET /api/v1/system/status` | 匿名 | `200 {initialized,version,appServer}`。当前版本字段为 `0.2.0`。 |
|
| `GET /api/v1/system/status` | 匿名 | `200 {initialized,version,appServer}`。`version` 为镜像构建时注入的应用版本,未注入时默认为 `0.2.0`。 |
|
||||||
| `POST /api/v1/setup` | 匿名、仅未初始化 | body `{username,password,timezone}`;用户名至少 3 位、密码至少 10 位,否则 400;时区有效时写入,否则使用默认 UTC。事务创建唯一管理员和通用设置,设置 session,返回 `201 {ok:true}`;已初始化返回 409。 |
|
| `POST /api/v1/setup` | 匿名、仅未初始化 | body `{username,password,timezone}`;用户名至少 3 位、密码至少 10 位,否则 400;时区有效时写入,否则使用默认 UTC。事务创建唯一管理员和通用设置,设置 session,返回 `201 {ok:true}`;已初始化返回 409。 |
|
||||||
| `POST /api/v1/auth/login` | 匿名 | body `{username,password}`;未初始化返回 409,错误凭据返回 401,成功设置 session 并返回 `200 {ok:true}`,限流返回 429。 |
|
| `POST /api/v1/auth/login` | 匿名 | body `{username,password}`;未初始化返回 409,错误凭据返回 401,成功设置 session 并返回 `200 {ok:true}`,限流返回 429。 |
|
||||||
| `任意方法 /api/v1/auth/me` | session;非 `GET`/`HEAD` 还需来源头 | `200 {username}`。前端使用 `GET`。 |
|
| `任意方法 /api/v1/auth/me` | session;非 `GET`/`HEAD` 还需来源头 | `200 {username}`。前端使用 `GET`。 |
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func (a *App) api(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.mu.RUnlock()
|
a.mu.RUnlock()
|
||||||
jsonOut(w, 200, map[string]any{"initialized": a.store.Initialized(), "version": "0.2.0", "appServer": connected})
|
jsonOut(w, 200, map[string]any{"initialized": a.store.Initialized(), "version": Version, "appServer": connected})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if p == "setup" && r.Method == "POST" {
|
if p == "setup" && r.Method == "POST" {
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import (
|
|||||||
webassets "codex-helper/internal/web"
|
webassets "codex-helper/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Version is overridden at build time for release images.
|
||||||
|
var Version = "0.2.0"
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
dataDir string
|
dataDir string
|
||||||
store *store.Store
|
store *store.Store
|
||||||
|
|||||||
@@ -23,6 +23,28 @@ func TestSystemStatusRejectsNonGETMethods(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSystemStatusReturnsBuildVersion(t *testing.T) {
|
||||||
|
a := newReminderTestApp(t)
|
||||||
|
originalVersion := Version
|
||||||
|
Version = "1.2.3-test"
|
||||||
|
t.Cleanup(func() { Version = originalVersion })
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/system/status", nil)
|
||||||
|
a.api(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body.Version != "1.2.3-test" {
|
||||||
|
t.Fatalf("version = %q; want %q", body.Version, "1.2.3-test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDashboardSerializesNilListsAsEmptyArrays(t *testing.T) {
|
func TestDashboardSerializesNilListsAsEmptyArrays(t *testing.T) {
|
||||||
a := newReminderTestApp(t)
|
a := newReminderTestApp(t)
|
||||||
a.runtimes[1] = &accountRuntime{}
|
a.runtimes[1] = &accountRuntime{}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 状态与路由
|
## 状态与路由
|
||||||
|
|
||||||
应用启动先请求 `system/status`,已初始化时再请求 `auth/me`。未初始化渲染安装页;未登录渲染登录页;登录后由 `BrowserRouter` 提供 `/` 总览和 `/settings` 设置,未知路径回到 `/`。这些分支只负责交互,服务端 session 才是安全边界。
|
应用启动先请求 `system/status`,已初始化时再请求 `auth/me`。未初始化渲染安装页;未登录渲染登录页;登录后由 `BrowserRouter` 提供 `/` 总览和 `/settings` 设置,未知路径回到 `/`。状态响应中的构建版本以 `v<version>` 徽标显示在安装页、登录页和登录后侧栏的品牌区域。这些分支只负责交互,服务端 session 才是安全边界。
|
||||||
|
|
||||||
主题和当前账号 ID 保存到 `localStorage`。总览先加载账号列表,为当前账号加载 Dashboard,并每 30 秒刷新内存数据;切换账号必须清空旧 Dashboard,避免短暂展示另一账号信息。手动刷新调用账号级 sync 后重新读取 Dashboard。
|
主题和当前账号 ID 保存到 `localStorage`。总览先加载账号列表,为当前账号加载 Dashboard,并每 30 秒刷新内存数据;切换账号必须清空旧 Dashboard,避免短暂展示另一账号信息。手动刷新调用账号级 sync 后重新读取 Dashboard。
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,22 @@
|
|||||||
|
|
||||||
## 镜像结构
|
## 镜像结构
|
||||||
|
|
||||||
`Dockerfile` 有四个阶段:Node 24.19.0 构建 React 静态资源;Go 1.26.0 以 `CGO_ENABLED=0` 构建后端;Node 阶段安装固定 `@openai/codex`;最终 Debian bookworm 镜像只包含 CA、时区、后端、Node runtime 和 Codex 包。
|
`Dockerfile` 有四个阶段:Node 24.19.0 构建 React 静态资源;Go 1.26.0 以 `CGO_ENABLED=0` 构建后端;Node 阶段安装固定 `@openai/codex`;最终 Debian bookworm 镜像只包含 CA、时区、后端、Node runtime 和 Codex 包。`APP_VERSION` 构建参数通过 Go linker 注入状态 API,未指定时默认为 `0.2.0`,前端在品牌区域显示该版本。
|
||||||
|
|
||||||
前端产物复制到 `backend/internal/web/dist` 后嵌入二进制。运行层使用 UID `10001` 的 system 用户 `helper`,默认 `DATA_DIR=/data`、`LISTEN_ADDR=:8080`,并暴露 `/data` volume 和 8080。健康检查调用二进制自身的 `healthcheck` 子命令。
|
前端产物复制到 `backend/internal/web/dist` 后嵌入二进制。运行层使用 UID `10001` 的 system 用户 `helper`,默认 `DATA_DIR=/data`、`LISTEN_ADDR=:8080`,并暴露 `/data` volume 和 8080。健康检查调用二进制自身的 `healthcheck` 子命令。
|
||||||
|
|
||||||
|
## 发布镜像
|
||||||
|
|
||||||
|
仓库根目录的 `push-image.sh` 构建 `linux/amd64`、`linux/arm64` 镜像并推送至 Docker Hub。版本号必须形如 `0.3.0` 或 `0.3.0-beta.1`,同时作为应用版本和镜像标签;脚本还会更新 `latest`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login -u koalalove
|
||||||
|
bash push-image.sh 0.3.0
|
||||||
|
# 完整重新构建:bash push-image.sh 0.3.0 --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会创建并选用 `codex-helper-builder` buildx builder,并尝试通过 `tonistiigi/binfmt` 注册跨架构模拟器。发布前应先完成本地镜像验证;脚本执行成功即会推送远端标签。
|
||||||
|
|
||||||
## Compose 与持久化
|
## Compose 与持久化
|
||||||
|
|
||||||
`docker-compose.yml` 本地构建 `codex-helper:latest`,将宿主机 8180 映射到容器 8080,并把命名卷 `codex-helper-data` 挂载到 `/data`。全部数据库、密钥、Codex 配置和多账号凭据都依赖这个卷。
|
`docker-compose.yml` 本地构建 `codex-helper:latest`,将宿主机 8180 映射到容器 8080,并把命名卷 `codex-helper-data` 挂载到 `/data`。全部数据库、密钥、Codex 配置和多账号凭据都依赖这个卷。
|
||||||
|
|||||||
+12
-1
@@ -1 +1,12 @@
|
|||||||
<div id="root"></div><script type="module" src="/src/main.tsx"></script>
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Codex Helper</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
+26
-14
@@ -90,6 +90,7 @@ function App() {
|
|||||||
if (!s.initialized)
|
if (!s.initialized)
|
||||||
return (
|
return (
|
||||||
<Setup
|
<Setup
|
||||||
|
version={s.version}
|
||||||
onDone={() => {
|
onDone={() => {
|
||||||
setS({ ...s, initialized: true });
|
setS({ ...s, initialized: true });
|
||||||
setAuth(true);
|
setAuth(true);
|
||||||
@@ -100,9 +101,15 @@ function App() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
{auth ? (
|
{auth ? (
|
||||||
<Route path="*" element={<Shell logout={() => setAuth(false)} />} />
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={<Shell version={s.version} logout={() => setAuth(false)} />}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Route path="*" element={<Login done={() => setAuth(true)} />} />
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={<Login version={s.version} done={() => setAuth(true)} />}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
@@ -116,7 +123,15 @@ const Splash = () => (
|
|||||||
<div className="spinner" />
|
<div className="spinner" />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
function Setup({ onDone }: { onDone: () => void }) {
|
function Brand({ version }: { version?: string }) {
|
||||||
|
return (
|
||||||
|
<div className="logo">
|
||||||
|
<Zap /> Codex Helper
|
||||||
|
{version && <span className="version-badge">v{version}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function Setup({ version, onDone }: { version: string; onDone: () => void }) {
|
||||||
const [form, set] = useState({
|
const [form, set] = useState({
|
||||||
username: "admin",
|
username: "admin",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -135,9 +150,7 @@ function Setup({ onDone }: { onDone: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<main className="setup">
|
<main className="setup">
|
||||||
<section className="hero">
|
<section className="hero">
|
||||||
<div className="logo">
|
<Brand version={version} />
|
||||||
<Zap /> Codex Helper
|
|
||||||
</div>
|
|
||||||
<h1>
|
<h1>
|
||||||
连接你的 Codex
|
连接你的 Codex
|
||||||
<br />
|
<br />
|
||||||
@@ -146,6 +159,9 @@ function Setup({ onDone }: { onDone: () => void }) {
|
|||||||
<p>一个容器内完成用量洞察、重置提醒与账户管理。</p>
|
<p>一个容器内完成用量洞察、重置提醒与账户管理。</p>
|
||||||
</section>
|
</section>
|
||||||
<form className="panel form" onSubmit={submit}>
|
<form className="panel form" onSubmit={submit}>
|
||||||
|
<div className="setup-mobile-brand">
|
||||||
|
<Brand version={version} />
|
||||||
|
</div>
|
||||||
<small>首次初始化 · 1 / 1</small>
|
<small>首次初始化 · 1 / 1</small>
|
||||||
<h2>创建管理员</h2>
|
<h2>创建管理员</h2>
|
||||||
<label>
|
<label>
|
||||||
@@ -181,7 +197,7 @@ function Setup({ onDone }: { onDone: () => void }) {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function Login({ done }: { done: () => void }) {
|
function Login({ version, done }: { version: string; done: () => void }) {
|
||||||
const [u, setU] = useState("admin"),
|
const [u, setU] = useState("admin"),
|
||||||
[p, setP] = useState(""),
|
[p, setP] = useState(""),
|
||||||
[e, setE] = useState("");
|
[e, setE] = useState("");
|
||||||
@@ -199,9 +215,7 @@ function Login({ done }: { done: () => void }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="logo">
|
<Brand version={version} />
|
||||||
<Zap /> Codex Helper
|
|
||||||
</div>
|
|
||||||
<h2>欢迎回来</h2>
|
<h2>欢迎回来</h2>
|
||||||
<label>
|
<label>
|
||||||
用户名
|
用户名
|
||||||
@@ -221,7 +235,7 @@ function Login({ done }: { done: () => void }) {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function Shell({ logout }: { logout: () => void }) {
|
function Shell({ version, logout }: { version: string; logout: () => void }) {
|
||||||
const nav = useNavigate();
|
const nav = useNavigate();
|
||||||
const [theme, setTheme] = useState(localStorage.theme || "system");
|
const [theme, setTheme] = useState(localStorage.theme || "system");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -236,9 +250,7 @@ function Shell({ logout }: { logout: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<aside>
|
<aside>
|
||||||
<div className="logo">
|
<Brand version={version} />
|
||||||
<Zap /> Codex Helper
|
|
||||||
</div>
|
|
||||||
<nav>
|
<nav>
|
||||||
<button onClick={() => nav("/")}>
|
<button onClick={() => nav("/")}>
|
||||||
<Activity />
|
<Activity />
|
||||||
|
|||||||
@@ -76,6 +76,17 @@ select {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
width: 22px;
|
width: 22px;
|
||||||
}
|
}
|
||||||
|
.version-badge {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.setup {
|
.setup {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -84,6 +95,9 @@ select {
|
|||||||
gap: 8vw;
|
gap: 8vw;
|
||||||
padding: 7vw;
|
padding: 7vw;
|
||||||
}
|
}
|
||||||
|
.setup-mobile-brand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: clamp(42px, 5vw, 76px);
|
font-size: clamp(42px, 5vw, 76px);
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
@@ -576,6 +590,9 @@ a {
|
|||||||
.hero {
|
.hero {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.setup-mobile-brand {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
.app {
|
.app {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,40 @@ async function openSettings(page: Page) {
|
|||||||
await expect(page.locator(".settings-loading")).toHaveCount(0);
|
await expect(page.locator(".settings-loading")).toHaveCount(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("shows the build version in the authenticated brand", async ({ page }) => {
|
||||||
|
await openSettings(page);
|
||||||
|
const badge = page.locator("aside .version-badge");
|
||||||
|
await expect(badge).toBeVisible();
|
||||||
|
await expect(badge).toHaveText("vtest");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows the build version before setup", async ({ page }) => {
|
||||||
|
await page.route("**/api/v1/system/status", (route) =>
|
||||||
|
route.fulfill({ json: { initialized: false, appServer: false, version: "test" } }),
|
||||||
|
);
|
||||||
|
await page.goto("/");
|
||||||
|
const badge = page.locator(
|
||||||
|
(page.viewportSize()?.width ?? 0) <= 800
|
||||||
|
? ".setup-mobile-brand .version-badge"
|
||||||
|
: ".hero .version-badge",
|
||||||
|
);
|
||||||
|
await expect(badge).toBeVisible();
|
||||||
|
await expect(badge).toHaveText("vtest");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows the build version on login", async ({ page }) => {
|
||||||
|
await page.route("**/api/v1/system/status", (route) =>
|
||||||
|
route.fulfill({ json: { initialized: true, appServer: false, version: "test" } }),
|
||||||
|
);
|
||||||
|
await page.route("**/api/v1/auth/me", (route) =>
|
||||||
|
route.fulfill({ status: 401, json: { error: "unauthorized" } }),
|
||||||
|
);
|
||||||
|
await page.goto("/");
|
||||||
|
const badge = page.locator(".login .version-badge");
|
||||||
|
await expect(badge).toBeVisible();
|
||||||
|
await expect(badge).toHaveText("vtest");
|
||||||
|
});
|
||||||
|
|
||||||
async function layout(page: Page) {
|
async function layout(page: Page) {
|
||||||
return page.evaluate(() => {
|
return page.evaluate(() => {
|
||||||
const box = (selector: string) => {
|
const box = (selector: string) => {
|
||||||
|
|||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Codex Helper 单容器多架构镜像构建并推送到 Docker Hub。
|
||||||
|
# 用法: bash push-image.sh <version> [--no-cache]
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
OWNER="koalalove"
|
||||||
|
IMAGE="codex-helper"
|
||||||
|
BUILDER="codex-helper-builder"
|
||||||
|
VERSION="${1:-}"
|
||||||
|
NO_CACHE=0
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ] || [ "${VERSION:0:2}" = "--" ]; then
|
||||||
|
echo "✗ 缺少版本号。用法: bash push-image.sh <version> [--no-cache]" >&2
|
||||||
|
echo " 例: bash push-image.sh 0.3.0 --no-cache" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'; then
|
||||||
|
echo "✗ 版本号格式非法: '$VERSION'(应形如 0.3.0 或 0.3.0-beta.1)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
shift
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-cache) NO_CACHE=1 ;;
|
||||||
|
*)
|
||||||
|
echo "✗ 未知参数: $arg" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=================================================="
|
||||||
|
echo " Codex Helper 单容器多架构镜像构建"
|
||||||
|
echo " 版本号: v${VERSION}(镜像 tag + 界面显示)"
|
||||||
|
echo " 目标仓库: ${OWNER}/${IMAGE}"
|
||||||
|
echo " 目标平台: linux/amd64, linux/arm64"
|
||||||
|
if [ "$NO_CACHE" = "1" ]; then
|
||||||
|
echo " 缓存策略: --no-cache(完整重新构建)"
|
||||||
|
else
|
||||||
|
echo " 缓存策略: 使用缓存(加 --no-cache 参数可禁用)"
|
||||||
|
fi
|
||||||
|
echo "=================================================="
|
||||||
|
echo ""
|
||||||
|
echo "请确保已执行: docker login -u ${OWNER}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[准备] 注册 QEMU/binfmt 多架构模拟..."
|
||||||
|
docker run --rm --privileged tonistiigi/binfmt --install all >/dev/null 2>&1 \
|
||||||
|
|| echo "[警告] binfmt 注册失败(若宿主已内置模拟可忽略),继续尝试构建。"
|
||||||
|
|
||||||
|
if ! docker buildx inspect "$BUILDER" >/dev/null 2>&1; then
|
||||||
|
echo "[准备] 创建多架构 buildx builder: ${BUILDER}..."
|
||||||
|
docker buildx create --name "$BUILDER" --driver docker-container --bootstrap
|
||||||
|
fi
|
||||||
|
docker buildx use "$BUILDER"
|
||||||
|
|
||||||
|
BUILD_ARGS=(
|
||||||
|
--platform linux/amd64,linux/arm64
|
||||||
|
--file Dockerfile
|
||||||
|
--build-arg "APP_VERSION=${VERSION}"
|
||||||
|
--tag "${OWNER}/${IMAGE}:${VERSION}"
|
||||||
|
--tag "${OWNER}/${IMAGE}:latest"
|
||||||
|
--push
|
||||||
|
)
|
||||||
|
[ "$NO_CACHE" = "1" ] && BUILD_ARGS+=(--no-cache)
|
||||||
|
|
||||||
|
echo "[1/1] 构建并推送 Codex Helper 镜像 v${VERSION}..."
|
||||||
|
docker buildx build "${BUILD_ARGS[@]}" .
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=================================================="
|
||||||
|
echo " ✓ 构建推送完成!"
|
||||||
|
echo " 镜像: ${OWNER}/${IMAGE}:${VERSION}"
|
||||||
|
echo " ${OWNER}/${IMAGE}:latest"
|
||||||
|
echo ""
|
||||||
|
echo " 快速启动:"
|
||||||
|
echo " docker run -d \\"
|
||||||
|
echo " -p 8180:8080 \\"
|
||||||
|
echo " -v codex-helper-data:/data \\"
|
||||||
|
echo " --name codex-helper \\"
|
||||||
|
echo " ${OWNER}/${IMAGE}:${VERSION}"
|
||||||
|
echo "=================================================="
|
||||||
Reference in New Issue
Block a user