app #0: cube.famzheng.me 入口门户 + 平台脚手架
deploy cube / build-and-deploy (push) Has been cancelled
deploy cube / build-and-deploy (push) Has been cancelled
monorepo 第一刀: - workspace + crates/cube-core(base router / healthz / ServeDir SPA fallback / JSON tracing / SIGTERM shutdown) - apps/cube:axum 主程序 + Vite + Vue 3 + TS 门户(暗色调 + 渐变 logo + app 卡片网格) - Dockerfile:scratch + musl 静态二进制,镜像 2.6MB - k8s/:cube-cube ns + Deployment + Service + Ingress(cube.famzheng.me,traefik LE 自动签) - registry:新增 registry.famzheng.me ingress 反代到 gitea 内置 container registry, 自动化身份用 mochi(registry.famzheng.me/mochi/cube) - CI:.gitea/workflows/deploy-cube.yml,host shell runner(gnoc), build → push → kubectl rollout 五步流水 - README:把宪法段改成 monorepo 模式 + monorepo 目录结构 - 新增宪法条款:前端视图状态走 URL(path + query)保证可 bookmark
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
name: deploy cube
|
||||
# app #0:cube.famzheng.me 入口门户。host shell runner(gnoc 用户)。
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'apps/cube/**'
|
||||
- 'crates/cube-core/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- '.gitea/workflows/deploy-cube.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest:host # host shell executor,PATH 需要手动加 ~/.cargo/bin
|
||||
env:
|
||||
APP: cube
|
||||
IMAGE: registry.famzheng.me/mochi/cube
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Resolve image tag
|
||||
id: tag
|
||||
run: |
|
||||
echo "sha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build rust (musl static)
|
||||
run: |
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
cargo build --release --target x86_64-unknown-linux-musl -p "$APP"
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
cd "apps/$APP/frontend"
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Build & push image
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
echo "$REGISTRY_TOKEN" | docker login registry.famzheng.me -u mochi --password-stdin
|
||||
docker build -f "apps/$APP/Dockerfile" -t "$IMAGE:${{ steps.tag.outputs.sha }}" .
|
||||
docker push "$IMAGE:${{ steps.tag.outputs.sha }}"
|
||||
|
||||
- name: Roll out to k3s
|
||||
# runner 是 gnoc 用户 host shell 模式,直接用 ~/.kube/config(已配好),无需 secret
|
||||
run: |
|
||||
kubectl -n "cube-$APP" set image "deploy/$APP" "$APP=$IMAGE:${{ steps.tag.outputs.sha }}"
|
||||
kubectl -n "cube-$APP" rollout status "deploy/$APP" --timeout=120s
|
||||
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
**/node_modules
|
||||
**/dist
|
||||
.DS_Store
|
||||
Generated
+805
@@ -0,0 +1,805 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cube"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cube-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cube-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-serde"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/cube-core",
|
||||
"apps/cube",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Fam Zheng <fam@euphon.net>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["fs", "trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
@@ -16,10 +16,35 @@ Fam 的小 app 平台。
|
||||
|
||||
> 这是 cube 上所有 app 的"宪法",所有 app 必须遵守,破例必须改这份文档并经 Fam 拍板。
|
||||
|
||||
### 仓库结构:monorepo + cargo workspace
|
||||
|
||||
整个 cube 是**一个 git 仓库** `fam/cube`,cargo workspace 模式。
|
||||
|
||||
```
|
||||
cube/
|
||||
├── Cargo.toml # workspace manifest
|
||||
├── crates/
|
||||
│ └── cube-core/ # 共享样板:base router / healthz / tracing / shutdown / config
|
||||
├── apps/
|
||||
│ ├── cube/ # app #0 = cube.famzheng.me 入口门户
|
||||
│ │ ├── Cargo.toml # path 依赖 cube-core
|
||||
│ │ ├── src/main.rs
|
||||
│ │ ├── frontend/ # vite + vue 3 + ts
|
||||
│ │ ├── Dockerfile
|
||||
│ │ └── k8s/ # deployment / service / ingress
|
||||
│ ├── portfolio/
|
||||
│ ├── repo-vis/
|
||||
│ └── ...
|
||||
├── doc/
|
||||
└── .gitea/workflows/ # CI;按 path 触发,per-app build/deploy
|
||||
```
|
||||
|
||||
`cube-core` 是 **path 依赖**(`cube-core = { path = "../../crates/cube-core" }`),不发布、不打 tag、不走 git 依赖。改 `cube-core` 时所有 app 一起 rebuild,monorepo 的整个意义就在于"一次重构,全员同步"。
|
||||
|
||||
### 部署目标
|
||||
|
||||
单一目标:famzheng.me 节点的 k3s(`kubectl context default`),不双轨。
|
||||
- 每 app 一个 k8s namespace(ns 名 = app 名,不加 cube- 前缀)
|
||||
- 每 app 一个 k8s namespace,ns 名 = `cube-<app>`(如 `cube-cube` / `cube-portfolio`),便于跟其他 ns 隔离 + 一眼可见归属
|
||||
- traefik ingress + 通配符 LE 证书自动签
|
||||
|
||||
### 域名
|
||||
@@ -28,23 +53,24 @@ Fam 的小 app 平台。
|
||||
- 不嵌 `cube` 子域(冲突检查 = 起新 ingress 时 traefik 自然报错,不需要额外心智)
|
||||
- 旧域名(如 `portfolio.oci.euphon.net`)让 oci ingress 兜底 308 redirect 到新地址,过渡期后下掉
|
||||
|
||||
### 后端:Rust + Axum,每 app 独立仓库
|
||||
### 后端:Rust + Axum
|
||||
|
||||
- gitea repo `fam/<app>`,单 axum 服务
|
||||
- 公共代码通过 `cube-core` crate 复用:`cube-core = { git = "https://famzheng.me/gitea/fam/cube-core", tag = "v0.x" }`
|
||||
- `cube-core` 提供:
|
||||
每个 app 是 workspace 里的一个 bin crate,单 axum 服务。
|
||||
|
||||
`cube-core` 提供:
|
||||
- `/healthz` router(200 = ok)
|
||||
- `ServeDir` 静态前端 fallback 到 `index.html`
|
||||
- `ServeDir` 静态前端 fallback 到 `index.html`(SPA 路由兼容)
|
||||
- `tracing` 配 JSON stdout
|
||||
- SIGTERM graceful shutdown
|
||||
- env → struct 配置加载(`envy` crate)
|
||||
- 业务 app 只写 `/api/*` 路由 + handler:
|
||||
- env → struct 配置加载
|
||||
|
||||
业务 app 只写 `/api/*` 路由 + handler:
|
||||
|
||||
```rust
|
||||
Router::new()
|
||||
.merge(cube_core::base("dist"))
|
||||
.nest("/api", api_routes())
|
||||
let app = cube_core::base("dist")
|
||||
.nest("/api", api_routes());
|
||||
cube_core::serve(app).await
|
||||
```
|
||||
- 不上 cargo workspace —— 每 app 一个 repo 彻底分
|
||||
|
||||
### 前端:Vite + Vue 3
|
||||
|
||||
@@ -55,19 +81,19 @@ Fam 的小 app 平台。
|
||||
|
||||
### 构建:host musl + scratch 容器
|
||||
|
||||
host 上直接 cargo build,不在容器里跑 cargo:
|
||||
host 上直接 cargo build,不在容器里跑 cargo。每个 app 的 build 命令固定:
|
||||
|
||||
```bash
|
||||
cargo build --release --target x86_64-unknown-linux-musl
|
||||
(cd frontend && npm ci && npm run build)
|
||||
cargo build --release --target x86_64-unknown-linux-musl -p <app>
|
||||
(cd apps/<app>/frontend && npm ci && npm run build)
|
||||
```
|
||||
|
||||
Dockerfile:
|
||||
Dockerfile(每个 app 自带一份,结构一致):
|
||||
|
||||
```dockerfile
|
||||
FROM scratch
|
||||
COPY target/x86_64-unknown-linux-musl/release/<app> /app
|
||||
COPY frontend/dist /dist
|
||||
COPY apps/<app>/frontend/dist /dist
|
||||
ENTRYPOINT ["/app"]
|
||||
```
|
||||
|
||||
@@ -80,20 +106,20 @@ ENTRYPOINT ["/app"]
|
||||
- 用 **gitea 自带** Container Registry(Packages 功能,gitea 1.20+ 自带)
|
||||
- **坑**:gitea 挂在 `/gitea/` 子路径下,docker daemon 默认拼 `https://famzheng.me/v2/...` 会 404
|
||||
- 方案:加一条 ingress `registry.famzheng.me` 反代 `/v2/*` → `gitea-svc:3000/v2/*`,复用 gitea token 鉴权
|
||||
- 镜像命名:`registry.famzheng.me/fam/<app>:<sha>`
|
||||
- 镜像命名:`registry.famzheng.me/mochi/<app>:<sha>`
|
||||
- **未实施**:第一个 app 上线时再搞 ingress;过渡期可本地 build 直接 `docker save` + `k3s ctr image import`
|
||||
|
||||
### CI/CD
|
||||
|
||||
走 gitea Actions,复用现有 instance-level act_runner(gnoc 用户,host shell 模式,labels `*:host`,**新 repo 不用注册 runner**)。
|
||||
|
||||
每个 app repo 一份 `.gitea/workflows/deploy.yml`,固定 5 步:
|
||||
monorepo 每 app 一份 workflow `.gitea/workflows/deploy-<app>.yml`,按 `paths` 触发(只改 `apps/<app>/**` 和 `crates/cube-core/**` 才跑)。固定 5 步:
|
||||
|
||||
1. `cargo build --release --target x86_64-unknown-linux-musl`
|
||||
2. `(cd frontend && npm ci && npm run build)`
|
||||
3. `docker build -t registry.famzheng.me/fam/<app>:$GITHUB_SHA .`
|
||||
4. `docker push registry.famzheng.me/fam/<app>:$GITHUB_SHA`
|
||||
5. `kubectl -n <app> set image deploy/<app> <app>=registry.famzheng.me/fam/<app>:$GITHUB_SHA`
|
||||
1. `cargo build --release --target x86_64-unknown-linux-musl -p <app>`
|
||||
2. `(cd apps/<app>/frontend && npm ci && npm run build)`
|
||||
3. `docker build -f apps/<app>/Dockerfile -t registry.famzheng.me/mochi/<app>:$GITHUB_SHA .`
|
||||
4. `docker push registry.famzheng.me/mochi/<app>:$GITHUB_SHA`
|
||||
5. `kubectl -n cube-<app> set image deploy/<app> <app>=registry.famzheng.me/mochi/<app>:$GITHUB_SHA`
|
||||
|
||||
host shell PATH 注意:workflow 第一行 `export PATH="$HOME/.cargo/bin:$PATH"`(gnoc 的 rustup 装在 `~/.cargo`)。
|
||||
|
||||
@@ -106,6 +132,7 @@ host shell PATH 注意:workflow 第一行 `export PATH="$HOME/.cargo/bin:$PATH
|
||||
- 配置走 env var + k8s Secret,**禁止** config.yaml 文件挂载(partiverse 那个"空目录顶替 config 文件"的坑别再踩)
|
||||
- 数据持久化只走 PVC + 每天 minio backup CronJob(`cube-core` 提供 base 模板)
|
||||
- 镜像 tag 用 git SHA,不用 `latest`
|
||||
- **前端视图状态走 URL**(path + query params),保证任意视图(筛选、分页、tab、详情)都能被 bookmark / share。可收藏的 state 必须在 URL 里,不准只活在 Pinia / `ref()` 里。Vue Router 的 `useRoute()` + `router.replace({ query: ... })` 是默认搭配。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "cube"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
description = "cube.famzheng.me — cube 平台入口门户(app #0)"
|
||||
|
||||
[dependencies]
|
||||
cube-core = { path = "../../crates/cube-core" }
|
||||
tokio = { workspace = true }
|
||||
@@ -0,0 +1,8 @@
|
||||
# cube app #0 — 入口门户
|
||||
# Build context = repo root(不是 apps/cube/),所以路径都是 apps/cube/...
|
||||
# build 流程在 host 上跑(不在容器里),见 README"构建:host musl + scratch 容器"
|
||||
FROM scratch
|
||||
COPY target/x86_64-unknown-linux-musl/release/cube /cube
|
||||
COPY apps/cube/frontend/dist /dist
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/cube"]
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>cube — Fam's small app platform</title>
|
||||
<meta name="description" content="Fam 的小 app 平台 · cube.famzheng.me" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1500
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "cube-portal",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.5",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#7c3aed"/>
|
||||
<stop offset="100%" stop-color="#06b6d4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="8" y="8" width="48" height="48" rx="10" fill="url(#g)"/>
|
||||
<path d="M20 20 L44 20 L44 44 L20 44 Z M20 20 L32 14 L56 14 L44 20 M44 20 L56 14 L56 38 L44 44" fill="none" stroke="white" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 499 B |
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import AppCard from './components/AppCard.vue'
|
||||
import { apps } from './apps'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 64 64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#7c3aed" />
|
||||
<stop offset="100%" stop-color="#06b6d4" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="8" y="8" width="48" height="48" rx="10" fill="url(#g)" />
|
||||
<path
|
||||
d="M20 20 L44 20 L44 44 L20 44 Z M20 20 L32 14 L56 14 L44 20 M44 20 L56 14 L56 38 L44 44"
|
||||
fill="none" stroke="white" stroke-width="2.4" stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>cube</h1>
|
||||
<p class="tagline">Fam 的小 app 平台 · <code>*.famzheng.me</code></p>
|
||||
</header>
|
||||
|
||||
<section class="grid">
|
||||
<AppCard v-for="app in apps" :key="app.slug" :app="app" />
|
||||
</section>
|
||||
|
||||
<footer class="foot">
|
||||
<span>cube · monorepo at</span>
|
||||
<a href="https://famzheng.me/gitea/fam/cube" target="_blank" rel="noopener">famzheng.me/gitea/fam/cube</a>
|
||||
</footer>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 4rem 1.5rem 3rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
margin: 0 auto 1.25rem;
|
||||
filter: drop-shadow(0 8px 24px rgba(124, 58, 237, 0.35));
|
||||
}
|
||||
.logo svg { width: 100%; height: 100%; }
|
||||
|
||||
h1 {
|
||||
font-size: 3.25rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
background: linear-gradient(135deg, #fff 0%, #b8c0d6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
margin-top: 0.5rem;
|
||||
color: var(--fg-dim);
|
||||
font-size: 1rem;
|
||||
}
|
||||
.tagline code {
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.foot {
|
||||
text-align: center;
|
||||
color: var(--fg-dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.foot a {
|
||||
color: var(--accent-2);
|
||||
margin-left: 0.35rem;
|
||||
}
|
||||
.foot a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
export type AppStatus = 'live' | 'pending' | 'tbd'
|
||||
|
||||
export interface App {
|
||||
slug: string
|
||||
name: string
|
||||
description: string
|
||||
url: string
|
||||
status: AppStatus
|
||||
}
|
||||
|
||||
export const apps: App[] = [
|
||||
{
|
||||
slug: 'cube',
|
||||
name: 'cube',
|
||||
description: '你正在看的这个门户。cube 平台本身的入口。',
|
||||
url: 'https://cube.famzheng.me',
|
||||
status: 'live',
|
||||
},
|
||||
{
|
||||
slug: 'portfolio',
|
||||
name: 'portfolio',
|
||||
description: '投资组合追踪。从 oci 迁移中(原 portfolio.oci.euphon.net)。',
|
||||
url: 'https://portfolio.famzheng.me',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
slug: 'repo-vis',
|
||||
name: 'repo-vis',
|
||||
description: 'git 仓库可视化。从 oci 迁移中。',
|
||||
url: 'https://repo-vis.famzheng.me',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
slug: 'simpleasm',
|
||||
name: 'simpleasm',
|
||||
description: '汇编教学/玩具。从 oci 迁移中(原 asm.oci.euphon.net)。',
|
||||
url: 'https://simpleasm.famzheng.me',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
slug: 'guitar',
|
||||
name: 'guitar',
|
||||
description: '吉他 player。从 oci 迁移中(原 player.oci.euphon.net)。',
|
||||
url: 'https://guitar.famzheng.me',
|
||||
status: 'pending',
|
||||
},
|
||||
{
|
||||
slug: 'pyroblem',
|
||||
name: 'pyroblem',
|
||||
description: '详情待补。',
|
||||
url: 'https://pyroblem.famzheng.me',
|
||||
status: 'tbd',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import type { App } from '../apps'
|
||||
|
||||
defineProps<{ app: App }>()
|
||||
|
||||
const statusLabel: Record<App['status'], string> = {
|
||||
live: '运行中',
|
||||
pending: '迁移中',
|
||||
tbd: '待规划',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="app.status === 'live' ? 'a' : 'div'"
|
||||
:href="app.status === 'live' ? app.url : undefined"
|
||||
:target="app.status === 'live' ? '_blank' : undefined"
|
||||
rel="noopener"
|
||||
class="card"
|
||||
:class="`is-${app.status}`"
|
||||
>
|
||||
<div class="row">
|
||||
<h2>{{ app.name }}</h2>
|
||||
<span class="status">{{ statusLabel[app.status] }}</span>
|
||||
</div>
|
||||
<p>{{ app.description }}</p>
|
||||
<span class="url">{{ app.url.replace(/^https?:\/\//, '') }}</span>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 1.1rem 1.2rem;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.card.is-live {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card.is-live:hover {
|
||||
border-color: var(--accent-2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px -10px rgba(6, 182, 212, 0.3);
|
||||
}
|
||||
|
||||
.card.is-pending,
|
||||
.card.is-tbd {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.is-live .status { color: var(--green); border-color: rgba(52, 211, 153, 0.4); }
|
||||
.is-pending .status { color: var(--amber); border-color: rgba(251, 191, 36, 0.4); }
|
||||
.is-tbd .status { color: var(--rose); border-color: rgba(251, 113, 133, 0.4); }
|
||||
|
||||
p {
|
||||
color: var(--fg-dim);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.url {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--fg-dim);
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,39 @@
|
||||
:root {
|
||||
--bg: #0b0d14;
|
||||
--bg-soft: #11141d;
|
||||
--fg: #e6e8ee;
|
||||
--fg-dim: #8b93a7;
|
||||
--accent: #7c3aed;
|
||||
--accent-2: #06b6d4;
|
||||
--border: #1f2433;
|
||||
--green: #34d399;
|
||||
--amber: #fbbf24;
|
||||
--rose: #fb7185;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, 'Helvetica Neue', sans-serif;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
background-image:
|
||||
radial-gradient(at 12% 8%, rgba(124, 58, 237, 0.18) 0px, transparent 45%),
|
||||
radial-gradient(at 88% 92%, rgba(6, 182, 212, 0.14) 0px, transparent 45%);
|
||||
background-attachment: fixed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue", "vite-env.d.ts"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/apps.ts","./src/main.ts","./src/App.vue","./src/components/AppCard.vue","./vite-env.d.ts"],"version":"5.7.3"}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
target: 'es2020',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
# registry.famzheng.me — 反代到 gitea container registry
|
||||
# Docker daemon 期望 https://<host>/v2/...,gitea 内置 registry 在 gitea pod 的 /v2/ 下,
|
||||
# 所以这条 ingress 不 strip 任何路径,全部 pass-through 到 gitea-svc:3000。
|
||||
# 不属于 cube app #0 本身,但平台基础设施先放在 app #0 目录里,未来可以挪到独立的 platform/ ns。
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: registry
|
||||
namespace: gnoc-gitea
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: registry.famzheng.me
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gitea
|
||||
port:
|
||||
number: 3000
|
||||
@@ -0,0 +1,46 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cube
|
||||
namespace: cube-cube
|
||||
labels:
|
||||
app: cube
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cube
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cube
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: registry-creds
|
||||
containers:
|
||||
- name: cube
|
||||
# tag 由 CI 通过 `kubectl set image` 替换;初次 apply 由 README 部署 runbook 指定
|
||||
image: registry.famzheng.me/mochi/cube:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 1
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 15
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 64Mi
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: cube
|
||||
namespace: cube-cube
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: cube.famzheng.me
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: cube
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: cube-cube
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: cube
|
||||
namespace: cube-cube
|
||||
spec:
|
||||
selector:
|
||||
app: cube
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
@@ -0,0 +1,9 @@
|
||||
//! cube.famzheng.me — 入口门户。纯静态 SPA,没有自己的 /api 路由。
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
cube_core::init_tracing();
|
||||
let dist = std::env::var("CUBE_DIST_DIR").unwrap_or_else(|_| "/dist".into());
|
||||
let app = cube_core::base(dist);
|
||||
cube_core::serve(app, 8080).await
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "cube-core"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
description = "Shared scaffolding for cube apps: base router, healthz, tracing, graceful shutdown"
|
||||
|
||||
[dependencies]
|
||||
axum = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tower-http = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
@@ -0,0 +1,58 @@
|
||||
//! cube-core: 共享脚手架。所有 cube app 通过这个 crate 拿基础 router、tracing、shutdown。
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use axum::{routing::get, Router};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::signal;
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
/// 拼一个带 healthz + 静态前端 SPA fallback 的基础 router。
|
||||
///
|
||||
/// `dist_dir` 是前端 vite build 输出目录(容器内一般是 `/dist`)。
|
||||
pub fn base(dist_dir: impl AsRef<Path>) -> Router {
|
||||
let dist = dist_dir.as_ref().to_path_buf();
|
||||
let index = dist.join("index.html");
|
||||
let static_svc = ServeDir::new(&dist).fallback(ServeFile::new(index));
|
||||
|
||||
Router::new()
|
||||
.route("/healthz", get(healthz))
|
||||
.fallback_service(static_svc)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
|
||||
async fn healthz() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
||||
/// 初始化 tracing:JSON to stdout,吃 RUST_LOG env。
|
||||
pub fn init_tracing() {
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
fmt().json().with_env_filter(filter).init();
|
||||
}
|
||||
|
||||
/// 起服务,绑定 0.0.0.0:port,挂 SIGTERM/SIGINT 优雅 shutdown。
|
||||
pub async fn serve(app: Router, port: u16) -> std::io::Result<()> {
|
||||
let addr = format!("0.0.0.0:{port}");
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!(%addr, "cube app listening");
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async { signal::ctrl_c().await.expect("install ctrl_c handler") };
|
||||
let term = async {
|
||||
signal::unix::signal(signal::unix::SignalKind::terminate())
|
||||
.expect("install SIGTERM handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
tokio::select! {
|
||||
_ = ctrl_c => tracing::info!("ctrl-c received, shutting down"),
|
||||
_ = term => tracing::info!("SIGTERM received, shutting down"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user