From ef20c8c54cdafd89a4e4dcfcee4679781e233fa5 Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Mon, 21 Sep 2026 12:17:27 +0100 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=8C=89=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E7=9A=84=E6=9C=AC=E5=9C=B0=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E4=B8=8E=20fzc=20=E5=8F=91=E5=B8=83=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 7 ++++++ AGENTS.md | 8 +++++++ Makefile | 26 ++++++++++++++++++++++ README.md | 6 +++++ scripts/release_remote.py | 46 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 .dockerignore create mode 100644 AGENTS.md create mode 100644 Makefile create mode 100644 scripts/release_remote.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0cedc48 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +**/node_modules +**/.env* +**/config.yaml +**/data +**/*.bundle +.claude diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..030d911 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# Local development agreement + +- Source lives locally under ~/projects/fzc-src/cube; fzc is the deployment target, not an interactive development workspace. +- Use the root Makefile for routine deployment. Preserve existing data, credentials, ingress, service names and schedule. +- Never bulk-deploy, trigger real notifications, or import production data merely to verify a migration. +- Original unrelated dirty files belong to the user; preserve them and review before committing/publishing. +- Do not commit local config files, caches, login images, backup credentials or runtime records. +- Read README.md for the exact deployment boundary. Other projects/hosts are outside this repository's scope. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7318fbd --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +SHELL := /bin/bash +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := help +DEPLOY_HOST ?= fzc +APP ?= cube +APPS := cube articulate karaoke notes simpleasm video2slides webgl werewolf llm-proxy +NAMESPACE = $(if $(filter llm-proxy,$(APP)),llm-proxy,cube-$(APP)) +RELEASE_TAG := $(shell git rev-parse --short=12 HEAD)-$(shell date -u +%Y%m%d%H%M%S) +IMAGE ?= cube-local-$(APP):$(RELEASE_TAG) +.PHONY: help guard build deploy deploy-write +help: + @echo 'make deploy APP=<$(APPS)>; make deploy-write' +guard: + @case ' $(APPS) ' in *' $(APP) '*) ;; *) echo 'Unknown/decommissioned app'; exit 1;; esac + @test -z "$$(git status --porcelain)" || { echo 'Review/commit source changes first'; exit 1; } +build: guard + cargo test -p $(APP) + CC_x86_64_unknown_linux_musl=musl-gcc cargo build --release --target x86_64-unknown-linux-musl -p $(APP) + @if [ -f apps/$(APP)/frontend/package.json ]; then cd apps/$(APP)/frontend && npm ci && npm run build; fi + docker build -f apps/$(APP)/Dockerfile -t $(IMAGE) . +deploy: guard + $(MAKE) build APP=$(APP) IMAGE=$(IMAGE) + docker save $(IMAGE) | ssh $(DEPLOY_HOST) 'sudo k3s ctr -n k8s.io images import -' + python3 scripts/release_remote.py --host $(DEPLOY_HOST) --namespace $(NAMESPACE) --deployment $(APP) --container $(APP) --image $(IMAGE) +deploy-write: + $(MAKE) -C apps/write deploy HOST=$(DEPLOY_HOST) diff --git a/README.md b/README.md index a0d6a84..c0e18a4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # cube +## 本地开发与发布(2026-09-21) + +源码迁至本机 `~/projects/fzc-src/cube`。使用 `make deploy APP=<应用名>` 在本机测试/构建,再通过 SSH fzc 更新单个现有工作负载;`make deploy-write` 沿用 user systemd 方式。旧 CI 配置保留,不在这次迁移中批量重发各应用。 + +部署脚本已经做命令及语法检查;迁移本身不重启现有服务。 + Fam 的小 app 平台。 ## 是什么 diff --git a/scripts/release_remote.py b/scripts/release_remote.py new file mode 100644 index 0000000..4f712a3 --- /dev/null +++ b/scripts/release_remote.py @@ -0,0 +1,46 @@ +"""Local-built image release to an existing fzc workload; never provisions infrastructure.""" +import argparse +import json +import shlex +import subprocess +import urllib.request + +def run(args, capture=False): + return subprocess.run(args, check=True, text=True, capture_output=capture).stdout + +def main(): + parser=argparse.ArgumentParser() + parser.add_argument('--host', default='fzc') + parser.add_argument('--namespace', required=True) + parser.add_argument('--deployment', required=True) + parser.add_argument('--container', required=True) + parser.add_argument('--image', required=True) + parser.add_argument('--cron', action='append', default=[], help='name:container') + parser.add_argument('--health') + args=parser.parse_args() + def kube(*parts, capture=False): + return run(['ssh',args.host,shlex.join(['kubectl','-n',args.namespace,*parts])],capture) + targets=[('deployment',args.deployment,args.container)]+[('cronjob',*c.split(':')) for c in args.cron] + previous=[] + for kind,name,container in targets: + obj=json.loads(kube('get',kind,name,'-o','json',capture=True)) + spec=obj['spec']['template']['spec'] if kind=='deployment' else obj['spec']['jobTemplate']['spec']['template']['spec'] + image=next(c['image'] for c in spec['containers'] if c['name']==container) + previous.append((kind,name,container,image)) + changed=[] + try: + for kind,name,container,image in previous: + changed.append((kind,name,container,image)) + kube('set','image',kind+'/'+name,container+'='+args.image) + if kind=='deployment': kube('rollout','status',kind+'/'+name,'--timeout=180s') + if args.health: + with urllib.request.urlopen(args.health,timeout=20) as response: + assert response.status == 200 + except Exception: + for kind,name,container,image in reversed(changed): + kube('set','image',kind+'/'+name,container+'='+image) + kube('rollout','status','deployment/'+args.deployment,'--timeout=180s') + raise + print('Released',args.image,'to',args.host,args.namespace, '; previous images:',previous) + +if __name__ == '__main__': main()