From c4ba01be0d9c7c98a694d3f9a4eaeb39ed2b0308 Mon Sep 17 00:00:00 2001 From: jubaoliang Date: Thu, 27 Aug 2026 07:35:37 +0000 Subject: [PATCH 1/2] ci: publish Docker to GHCR+Hub and auto-build FnOS after Release Cascade FnOS FPK from Release (reuse GHCR image and release wheel), keep Docker Hub sync, and fix workflow_dispatch version tags for Auto Tag. Co-authored-by: Cursor --- .cursor/skills/publish/SKILL.md | 2 +- .github/workflows/auto-tag-on-release.yml | 2 + .github/workflows/docker-publish.yml | 69 +++++++-- .github/workflows/fnos-build-fpk.yml | 172 ++++++++++----------- .github/workflows/release.yml | 24 +++ CHANGELOG.md | 8 +- fnos/README.md | 15 +- fnos/docker/Dockerfile | 2 +- fnos/docker/app/docker/docker-compose.yaml | 2 +- fnos/docker/manifest | 2 +- 10 files changed, 179 insertions(+), 119 deletions(-) diff --git a/.cursor/skills/publish/SKILL.md b/.cursor/skills/publish/SKILL.md index 684d7967..9b0039bc 100644 --- a/.cursor/skills/publish/SKILL.md +++ b/.cursor/skills/publish/SKILL.md @@ -3,7 +3,7 @@ name: publish description: >- Publish the Octop Python package: cut a release branch from develop, bump version, update CHANGELOG, open a PR to main; after merge, Actions tag on - main (PyPI / Docker Hub) and sync main into develop. Use when the user asks + main (PyPI / Docker Hub + GHCR) and sync main into develop. Use when the user asks to publish, release, bump version, cut a release, or run /publish. disable-model-invocation: true --- diff --git a/.github/workflows/auto-tag-on-release.yml b/.github/workflows/auto-tag-on-release.yml index 08e2f254..0606cbcc 100644 --- a/.github/workflows/auto-tag-on-release.yml +++ b/.github/workflows/auto-tag-on-release.yml @@ -65,6 +65,8 @@ jobs: # GITHUB_TOKEN tag pushes do not cascade to other workflows. Explicitly # dispatch Release / Docker Publish (workflow_dispatch is exempt). + # FnOS FPK is cascaded from Release after the GitHub Release exists + # (reuses Release wheel + waits for GHCR image from Docker Publish). - name: Trigger Release and Docker Publish if: steps.check_tag.outputs.should_publish == 'true' env: diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 5e37cad7..dd787522 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,26 +1,59 @@ name: Docker Publish -# 仅在推送版本标签(v*)时构建并发布镜像到 Docker Hub。 -# 与 .github/workflows/release.yml(发 PyPI)相互独立、可并行。 -# 普通 push / PR 不会触发,CI 开销保持在最低。 +# 仅在版本标签(v*)上构建并同步发布镜像到 Docker Hub 与 GHCR。 +# 文档与对外引用统一使用:ghcr.io/tencentcloud/octop +# (Hub 仍用 DOCKERHUB_* secrets 同步推送。) +# +# 触发路径: +# 1. 人工推送 v* tag → on.push.tags +# 2. Auto Tag 用 GITHUB_TOKEN 推 tag 后显式 workflow_dispatch(token push 不会触发 push 工作流) +# +# 注意:workflow_dispatch 下 metadata-action 的 type=semver 不可靠, +# 必须从 GITHUB_REF_NAME 显式解析版本号。 +# +# FnOS FPK 由 Release 成功后自动 dispatch,并等待本工作流推送的 :{version} 就绪。 +# +# 拉取示例: +# docker pull ghcr.io/tencentcloud/octop:latest +# docker pull ghcr.io/tencentcloud/octop:0.9.28 on: push: tags: - "v*" - # Allow Auto Tag On Release Merge to cascade after a GITHUB_TOKEN tag push - # (token-authored pushes do not retrigger push workflows). workflow_dispatch: +permissions: + contents: read + packages: write + jobs: docker: name: Build and push image runs-on: ubuntu-latest + # 拒绝在非 v* ref 上误跑(例如 UI 里对 main 点 Run workflow) + if: startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Resolve version from tag ref + id: ver + run: | + set -euo pipefail + ref="${GITHUB_REF_NAME}" + case "$ref" in + v*) + echo "version=${ref#v}" >> "$GITHUB_OUTPUT" + echo "tag=$ref" >> "$GITHUB_OUTPUT" + ;; + *) + echo "::error::Expected refs/tags/v*, got ref_name=$ref" + exit 1 + ;; + esac + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -30,16 +63,29 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image names + id: image + run: | + echo "ghcr=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + echo "hub=${{ secrets.DOCKERHUB_USERNAME }}/octop" >> "$GITHUB_OUTPUT" + - name: Docker metadata id: meta uses: docker/metadata-action@v5 with: - # 镜像名 = /octop(个人或组织命名空间均可)。 - images: ${{ secrets.DOCKERHUB_USERNAME }}/octop + images: | + ${{ steps.image.outputs.hub }} + ${{ steps.image.outputs.ghcr }} + # 显式 raw tag:兼容 push.tags 与 Auto Tag 的 workflow_dispatch tags: | - # v1.2.3 -> 1.2.3 - type=semver,pattern={{version}} - # 每个 tag 同时打 latest + type=raw,value=${{ steps.ver.outputs.version }} type=raw,value=latest - name: Build and push @@ -48,11 +94,8 @@ jobs: context: . file: ./docker/Dockerfile push: true - # 默认仅 amd64,构建最快最稳。如需 ARM64(Apple Silicon / Graviton), - # 改为 "linux/amd64,linux/arm64" 并加上 docker/setup-qemu-action@v3 步骤。 platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - # 复用 GitHub Actions 缓存加速后续构建 cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/fnos-build-fpk.yml b/.github/workflows/fnos-build-fpk.yml index 05c6a338..9ce4060a 100644 --- a/.github/workflows/fnos-build-fpk.yml +++ b/.github/workflows/fnos-build-fpk.yml @@ -1,35 +1,30 @@ -name: Build Octop FPK (template) +name: Build Octop FPK -# TEMPLATE — FnOS (飞牛 NAS) app packaging pipeline for fork maintainers. -# # This workflow builds the Octop Docker image, the Docker .fpk and the native .fpk, -# # then publishes GitHub releases (fixed version + rolling latest). -# # -# # To enable on your fork: -# # 1. Replace jubaoliang/octop with your own image namespace (or fork a copy). -# # 2. Optionally add a push trigger below (e.g. branches: [main]) so a rebuild happens -# # automatically on every upstream sync. It is intentionally workflow_dispatch-only -# # here so it never runs on the upstream repository. -# # -# image job:从仓库源码构建 Octop Docker 镜像,推送到 Docker Hub(jubaoliang/octop:vanilla) -# fpk job:构建「Docker 版」飞牛安装包 Fnos-octop-vanilla--.fpk(依赖 image,确保镜像已就绪) -# native job:构建「本地版(非 Docker)」飞牛安装包 Fnos-octop-vanilla-native--.fpk。 -# 不内置 Python 运行时(依赖飞牛应用商店 Python 3.12), -# 仅内置 Octop 核心 whl + 核心依赖 site-packages,不含浏览器二进制与预装技能。 -# release job: -# - 创建固定版本 release(如 fnos-0.9.16-01),保留历史、带更新日志; -# - 同步更新滚动 release fnos-vanilla-latest,方便用户始终从同一 URL 下载最新版。 +# FnOS (飞牛 NAS) 安装包流水线。 # -# 镜像包随公开仓库默认即为 public,无需额外设置可见性。 +# 触发: +# - Release 工作流在 GitHub Release 创建成功后自动 dispatch(推荐) +# - 亦可手动 workflow_dispatch(需对应版本镜像已在 GHCR) +# +# 制品复用: +# - Docker 镜像:不再重建,等待 docker-publish 推送的 +# ghcr.io/tencentcloud/octop:{version|latest} +# - Native wheel:优先从同版本 GitHub Release(v*)下载; +# 缺失时再回退到本仓源码构建 +# +# Jobs: +# version → ensure-image → fpk(Docker 版 .fpk,仅打包 compose) +# ↘ native(本地版 .fpk)→ release(固定版 + 滚动 latest) on: workflow_dispatch: permissions: contents: write - packages: write + packages: read concurrency: - group: build-octop-vanilla + group: build-octop-fnos cancel-in-progress: false jobs: @@ -40,17 +35,20 @@ jobs: iter: ${{ steps.iter.outputs.iter }} tag: ${{ steps.iter.outputs.tag }} pkg_ver: ${{ steps.iter.outputs.pkg_ver }} + image: ${{ steps.ver.outputs.image }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - - name: Read version + - name: Read version and image name id: ver run: | VER=$(grep -m1 '^version' pyproject.toml | sed -E 's/.*"([0-9][0-9.]*[0-9])".*/\1/') + IMAGE="ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" echo "version=$VER" >> "$GITHUB_OUTPUT" + echo "image=$IMAGE" >> "$GITHUB_OUTPUT" - name: Compute iteration id: iter @@ -58,10 +56,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VER=${{ steps.ver.outputs.version }} - # 查询已存在的 fnos--NN releases,取最大序号 +1 作为本次迭代号。 - # 之前用 GitHub API / git ls-remote / git tag -l 都在 CI 中因认证、 - # 限流或本地 tag 未拉取而回退成 0,导致每次都覆盖 fnos--01。 - # GitHub CLI 在 Actions runner 中已预装且自动使用 GITHUB_TOKEN,最稳。 + # 查询已存在的 fnos-vanilla--NN releases,取最大序号 +1。 echo "[version] existing releases matching fnos-vanilla-${VER}-NN:" gh release list --repo "${{ github.repository }}" --limit 100 --json tagName | python3 -c 'import sys,json; [print(" "+r.get("tagName","")) for r in json.load(sys.stdin)]' || true EXISTING=$(gh release list --repo "${{ github.repository }}" --limit 100 --json tagName | python3 -c 'import sys,json,re; d=json.load(sys.stdin); pat=re.compile(r"fnos-vanilla-[0-9]+\.[0-9]+\.[0-9]+-([0-9]+)$"); nums=[int(pat.match(r.get("tagName","")).group(1)) for r in d if pat.match(r.get("tagName",""))]; print(max(nums) if nums else 0)') @@ -71,42 +66,45 @@ jobs: echo "iter=$ITER" >> "$GITHUB_OUTPUT" echo "tag=fnos-vanilla-${VER}-${ITER}" >> "$GITHUB_OUTPUT" echo "pkg_ver=${VER}-${ITER}" >> "$GITHUB_OUTPUT" - echo "[version] VER=$VER, EXISTING_MAX=$MAX, ITER=$ITER, TAG=fnos-${VER}-${ITER}" + echo "[version] VER=$VER, EXISTING_MAX=$MAX, ITER=$ITER, TAG=fnos-vanilla-${VER}-${ITER}" - image: + # 复用 docker-publish 已推送的镜像,不在此重新 build/push。 + ensure-image: needs: version runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - fetch-tags: true - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub + - name: Log in to GHCR uses: docker/login-action@v3 with: - # Docker Hub(与 docker-publish.yml 同一凭据,DOCKERHUB_USERNAME 为 jubaoliang 时推送 jubaoliang/octop) - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push image - uses: docker/build-push-action@v6 - with: - context: . - file: fnos/docker/Dockerfile - push: true - tags: | - jubaoliang/octop:latest - jubaoliang/octop:vanilla - jubaoliang/octop:vanilla-v${{ needs.version.outputs.version }} - cache-from: type=gha - cache-to: type=gha,mode=max + - name: Wait for GHCR image from docker-publish + env: + IMAGE: ${{ needs.version.outputs.image }} + VER: ${{ needs.version.outputs.version }} + run: | + set -euo pipefail + TAG="${IMAGE}:${VER}" + echo "Waiting for ${TAG} (published by docker-publish)..." + for i in $(seq 1 90); do + if docker buildx imagetools inspect "$TAG" >/dev/null 2>&1; then + echo "Image ready: $TAG" + docker buildx imagetools inspect "$TAG" | head -20 + exit 0 + fi + echo "[$i/90] not ready yet..." + sleep 20 + done + echo "::error::Timed out waiting for ${TAG}. Ensure Docker Publish finished for v${VER}." + exit 1 fpk: - needs: [image, version] + needs: [ensure-image, version] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -128,8 +126,7 @@ jobs: if-no-files-found: error native: - # 即使原生版构建失败也不阻塞整体发布:Docker 版(fpk)始终会发布; - # 原生版成功则一并附带。continue-on-error 让 needs 视为通过。 + # 即使原生版构建失败也不阻塞整体发布:Docker 版(fpk)始终会发布。 continue-on-error: true needs: version runs-on: ubuntu-latest @@ -141,22 +138,40 @@ jobs: fetch-depth: 0 fetch-tags: true - - name: Set up Python 3.12 (build-only) + - name: Try download wheel from GitHub Release + id: wheel + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VER: ${{ needs.version.outputs.version }} + run: | + set -euo pipefail + mkdir -p dist + REL_TAG="v${VER}" + if gh release download "$REL_TAG" --repo "${{ github.repository }}" -p 'octop-*.whl' -D dist 2>/tmp/wheel-dl.err; then + ls -lh dist/octop-*.whl + echo "source=release" >> "$GITHUB_OUTPUT" + echo "Reusing wheel from GitHub Release ${REL_TAG}" + else + echo "Release wheel not available yet:" + cat /tmp/wheel-dl.err || true + echo "source=build" >> "$GITHUB_OUTPUT" + fi + + - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set up Node.js 20 (frontend build) + if: steps.wheel.outputs.source == 'build' uses: actions/setup-node@v4 with: node-version: "20" cache: "npm" cache-dependency-path: dashboard/package-lock.json - # native 包必须包含当前 fork 的源码修改(如 /admin/providers/fetch-models)。 - # 上游 TencentCloud/Octop 的 release wheel 不包含这些修改,因此改为从当前 - # 仓库源码构建 wheel:先构建前端产物到 src/octop/dashboard,再打包 wheel。 - name: Build Octop wheel from current source + if: steps.wheel.outputs.source == 'build' run: | set -e echo "=== build frontend ===" @@ -184,25 +199,11 @@ jobs: mkdir -p "${{ github.workspace }}/fnos/native/app/site-packages" SP="${{ github.workspace }}/fnos/native/app/site-packages" { - # 用 uv 安装核心 whl 及其依赖到包内 site-packages。 - # pip 对 orcakit-harness-agent[all] 的复杂依赖树会报 resolution-too-deep, - # uv 的回溯能力强得多(旧方案已验证)。 python3 -m pip install --quiet uv - # 第一步:让 uv 自由解析并安装核心 whl(含其完整依赖树)。 - # 自由解析会拉到最新的 mcp==2.0.0 + langchain-mcp-adapters==0.3.1,这两者在结构上 - # 不兼容(见第二步),但本步只负责把依赖装齐,不要求版本正确。 - # 注意:不能把钉版与 octop.whl 放在同一条 uv 命令里——会和 whl 的传递依赖约束 - # 发生 resolution 冲突,导致整个 native 构建失败(fdca4c6 的教训)。 python3 -m uv pip install --python python3.12 \ --target "$SP" --no-cache "$(ls dist/octop-*.whl | head -1)" - # 第二步:强制覆盖为仓库 uv.lock 锁定的兼容组合 mcp==1.28.1 + langchain-mcp-adapters==0.3.0。 - # mcp 2.0 移除了 mcp.server.fastmcp / mcp.shared.context.RequestContext 等符号, - # 而 langchain-mcp-adapters 0.3.x 仍硬导入它们,导致 octop 启动即 ImportError。 - # 先清掉旧版目录,防止多版本共存导致 Python 仍加载坏的 mcp 2.0。 rm -rf "$SP/mcp" "$SP"/mcp-*.dist-info \ "$SP/langchain_mcp_adapters" "$SP"/langchain_mcp_adapters-*.dist-info - # 用 pip download 拿到钉版 whl,再 unzip 进 site-packages。 - # 这比 pip install --target --force-reinstall 更可控,也不会触发 resolution。 mkdir -p /tmp/octop-pinned python3.12 -m pip download --no-deps --no-cache-dir -d /tmp/octop-pinned \ "mcp==1.28.1" "langchain-mcp-adapters==0.3.0" @@ -210,13 +211,9 @@ jobs: echo "Extracting pinned wheel: $whl" python3.12 -m zipfile -e "$whl" "$SP" done - # 把核心 whl 也内置进去,供飞牛主机离线重装/回滚使用。 cp dist/octop-*.whl "$SP/octop.whl" - # 校验:必须能走通真正的启动导入链(octop.launch → octop.infra.server → - # harness_agent.mcp → langchain_mcp_adapters)。仅 import octop 不会触发该链路, - # 故这里显式导入启动入口,确保 mcp/langchain-mcp-adapters 组合确实可用。 PYTHONPATH="$SP" python3.12 \ - -c "from octop.launch import run_foreground_blocking; import mcp, langchain_mcp_adapters; print('octop', octop.__version__, '| mcp', mcp.__version__, '| lcma', langchain_mcp_adapters.__version__)" + -c "import octop, mcp, langchain_mcp_adapters; from octop.launch import run_foreground_blocking; print('octop', octop.__version__, '| mcp', mcp.__version__, '| lcma', langchain_mcp_adapters.__version__)" } 2>&1 | tee -a "$LOG" - name: Build native FPK @@ -246,8 +243,7 @@ jobs: if-no-files-found: ignore release: - # 即使原生版失败(continue-on-error)也要发布 Docker 版; - # 原生版成功时 artifacts/native-fpk 存在,会被一并打包进发布。 + # 即使原生版失败(continue-on-error)也要发布 Docker 版。 if: always() needs: [fpk, native, version] runs-on: ubuntu-latest @@ -257,8 +253,6 @@ jobs: fetch-depth: 0 fetch-tags: true - # 分别按名称下载,单个大产物(native-fpk ~560MB)单独下载比「下载全部」更稳定, - # 避免 GitHub artifact 服务对“全部”模式偶发 5 次重试失败。 - name: Download docker fpk artifact (required) uses: actions/download-artifact@v4 with: @@ -267,7 +261,6 @@ jobs: if-no-files-found: error - name: Download native fpk artifact (best-effort) - # 原生版即使下载/解压偶发失败也不阻塞发布:Docker 版始终会发布。 continue-on-error: true uses: actions/download-artifact@v4 with: @@ -287,10 +280,7 @@ jobs: run: | VER="${{ needs.version.outputs.version }}" ITER="${{ needs.version.outputs.iter }}" - TAG="fnos-vanilla-${VER}-${ITER}" - # 找本分支历史中可达的最近固定版本 tag(排除滚动 tag)。 - # 不能直接 git describe:若历史 tag 误指向其它分支(如 main)的 commit, - # describe 会失败并回退到仓库根,导致更新说明把整个历史都列出来。 + IMAGE="${{ needs.version.outputs.image }}" PREV_TAG="" for t in $(git tag --list "fnos-vanilla-*" --sort=-creatordate); do [ "$t" = "fnos-vanilla-latest" ] && continue @@ -301,11 +291,10 @@ jobs: done echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT" { - # 安装说明置顶,用户一眼看到下载文件 echo "## 安装说明" echo "" echo "1. 飞牛应用中心 → 设置 → 手动安装应用,选择对应 .fpk。" - echo "2. Docker 版 **Fnos-octop-vanilla-${VER}-${ITER}.fpk**(约 80KB):飞牛自动从 Docker Hub 拉取镜像运行,镜像为 \\"jubaoliang/octop:vanilla\\"。" + echo "2. Docker 版 **Fnos-octop-vanilla-${VER}-${ITER}.fpk**(约 80KB):飞牛自动从 GHCR 拉取 \`${IMAGE}:latest\`。" echo "3. 本地版 **Fnos-octop-vanilla-native-${VER}-${ITER}.fpk**(约 200MB):非 Docker,原生运行在飞牛主机;安装时自动关联系统 Python 3.12 开发工具;浏览器、远程桌面等附加组件在 Octop 应用内按需安装。" echo "" echo "---" @@ -313,7 +302,6 @@ jobs: echo "" echo "## 本次更新(v${VER}-${ITER})" echo "" - # 只列相对上一版的非 merge commit,最多 30 条,避免说明冗长 if [ -n "$PREV_TAG" ]; then git log --oneline --no-merges "$PREV_TAG"..HEAD | head -30 else @@ -322,15 +310,12 @@ jobs: } > /tmp/release-body.md cat /tmp/release-body.md - # --- 固定版本 release:保留历史 --- - name: Publish fixed release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.version.outputs.tag }} - # 显式指向构建所用 commit,避免 GitHub 默认指向 main 分支 HEAD - # (此前 fnos-vanilla-* tag 全部错误指向 main 的 commit) target_commitish: ${{ github.sha }} - name: Octop (FnOS Vanilla) v${{ needs.version.outputs.pkg_ver }} + name: Octop (FnOS) v${{ needs.version.outputs.pkg_ver }} body_path: /tmp/release-body.md files: dist/*.fpk draft: false @@ -338,7 +323,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # --- 滚动 release fnos-vanilla-latest:始终指向最新版,只保留最新 assets --- - name: Delete rolling release if exists env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -359,7 +343,7 @@ jobs: with: tag_name: fnos-vanilla-latest target_commitish: ${{ github.sha }} - name: Octop (FnOS Vanilla) latest + name: Octop (FnOS) latest body_path: /tmp/release-body.md files: dist/*.fpk draft: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de83ed74..82d9dc07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,11 @@ name: Release +# 版本标签 v* → 测包、发 PyPI、建 GitHub Release,再级联: +# - Sync Main Into Develop +# - Build Octop FPK(复用本 Release 的 wheel;等待 docker-publish 的 GHCR 镜像) +# +# 与 docker-publish.yml 并行(由 Auto Tag 同时 dispatch,或人工 push tag 同时触发)。 + on: push: tags: @@ -20,6 +26,7 @@ jobs: build: name: Build distributions runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/checkout@v4 with: @@ -141,3 +148,20 @@ jobs: gh workflow run sync-main-to-develop.yml \ --repo "${{ github.repository }}" \ --ref main + + # FnOS FPK:复用本 Release 的 wheel + docker-publish 的 GHCR 镜像。 + # 与 sync-develop 并行;失败不阻断发版。 + trigger-fnos: + name: Trigger FnOS FPK build + needs: github-release + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Dispatch Build Octop FPK + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # --ref 用版本 tag,保证 FPK 打在与 PyPI / 镜像相同的 commit 上 + gh workflow run fnos-build-fpk.yml \ + --repo "${{ github.repository }}" \ + --ref "${{ github.ref_name }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index f75bddd5..0249b283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,19 @@ ## [Unreleased] +### 变更 + +- Docker / FnOS 镜像对外统一为 `ghcr.io/tencentcloud/octop`(仍同步推送 Docker Hub;移除 vanilla 镜像标签) +- FnOS FPK 在 Release 成功后自动构建:复用 GHCR 镜像与 Release wheel,不再重复编镜像 +- 修复 Auto Tag → `workflow_dispatch` 时 Docker 镜像可能只有 `:latest`、缺少 `:{version}` 的问题(显式从 tag 解析版本) + ## [0.9.28] - 2026-08-26 ### 修复 - 无更新权限时隐藏检查更新入口 - `/compact` 兼容 `.octop/conversation_history/` 卸载路径 -- FnOS 镜像改为 Docker Hub `jubaoliang/octop` +- FnOS 镜像发布流程调整 ### 新增 diff --git a/fnos/README.md b/fnos/README.md index 2dfde019..90f534e5 100644 --- a/fnos/README.md +++ b/fnos/README.md @@ -19,10 +19,10 @@ After install, open the app (Docker: `http://:8088`, native: `http:// | 版本 | 包名 | 体积 | 运行方式 | 依赖 | |------|------|------|----------|------| -| **Docker 版** | `octop-.fpk` | ~8 KB | 飞牛自动从 Docker Hub 拉取 `jubaoliang/octop:latest` 镜像运行 | 宿主需有 Docker 运行时 | +| **Docker 版** | `octop-.fpk` | ~8 KB | 飞牛自动从 GHCR 拉取 `ghcr.io/tencentcloud/octop:latest` 镜像运行 | 宿主需有 Docker 运行时 | | **本地版(非 Docker)** | `octop-native-.fpk` | ~560 MB | 自带 Python 3.12 运行时 + 前端 + 全部附加组件 + Chromium,原生运行在飞牛主机 | 无需 Docker | -- **Docker 版**实现为 FnOS `docker-project`:包体只含 `docker-compose.yaml` 与向导配置,运行时由飞牛从 Docker Hub 拉取镜像。镜像已内置全部附加组件(`browser` 浏览器自动化 + `desktop` 桌面控制)与前端。 +- **Docker 版**实现为 FnOS `docker-project`:包体只含 `docker-compose.yaml` 与向导配置,运行时由飞牛从 GHCR 拉取镜像。镜像已内置全部附加组件(`browser` 浏览器自动化 + `desktop` 桌面控制)与前端。 - **本地版**实现为 FnOS 原生 `app`:包内自带独立 Python 3.12 运行时、Octop 全部依赖、前端构建产物、Playwright Chromium,以及 `data-share` 共享数据目录,直接以进程方式运行,不依赖 Docker。 > 两款包都在滚动发布 **`fnos-latest`**: @@ -44,7 +44,7 @@ fnos/ │ │ └── install # 安装向导(可配置管理员账号/密码、日志级别、LLM 密钥) │ ├── app/ │ │ ├── docker/ -│ │ │ └── docker-compose.yaml # 引用 jubaoliang/octop:latest +│ │ │ └── docker-compose.yaml # 引用 ghcr.io/tencentcloud/octop:latest │ │ └── ui/ │ │ ├── config # 桌面图标入口 │ │ └── images/icon-{64,256}.png @@ -63,9 +63,10 @@ fnos/ ## 工作机制 -1. **源码同步**:`.github/workflows/zz-sync-upstream.yml` 每 6 小时把上游 `TencentCloud/Octop` 的更新合并进本仓 `main` 分支(使用仓库自带 `GITHUB_TOKEN`,无需 PAT)。 -2. **镜像构建**:`.github/workflows/fnos-build-fpk.yml` 的 `image` job 在 `main` 更新时,用 `fnos/docker/Dockerfile` 从仓库源码构建 Octop 镜像并推送到 Docker Hub `jubaoliang/octop:latest`(含全部附加组件)。 -3. **安装包构建**:同一 workflow 的 `fpk` job 用 `scripts/build-fpk.sh` 把 `fnos/docker/` 打成 Docker 版 `.fpk`;`native` job 用 python-build-standalone 构建 Python 3.12 运行时、安装全部依赖与 Playwright Chromium,把 `fnos/native/` 打成本地版 `.fpk`(`continue-on-error`,失败不阻塞 Docker 版)。两者均以滚动发布 `fnos-latest` 提供下载。 +1. **发版链路**:`v*` tag → `release.yml`(PyPI + GitHub Release)与 `docker-publish.yml`(GHCR / Hub)并行;Release 成功后自动 `workflow_dispatch` 本工作流。 +2. **镜像复用**:不再重新 build 镜像;`ensure-image` 轮询等待 `ghcr.io/tencentcloud/octop:{version}`(由 docker-publish 推送)。Docker 版 `.fpk` 仅打包 compose,运行时拉取该镜像。 +3. **Wheel 复用**:Native 版优先从同版本 GitHub Release(`v*`)下载 `octop-*.whl`;若缺失再回退源码构建前端 + wheel。 +4. **安装包构建**:`fpk` / `native` job 用 `scripts/build-fpk.sh` 打包,并以滚动发布 `fnos-vanilla-latest` 提供下载。 ## 本地构建 .fpk(无需 Docker) @@ -84,6 +85,6 @@ bash scripts/build-fpk.sh native # 仅本地版 → dist/octop-native-< - 不想依赖 Docker、希望自带运行时原生运行 → 选 `octop-native-.fpk` 2. 安装向导中设置管理员账号/密码、日志级别、LLM 密钥(可选)。 3. 安装完成后桌面出现「Octop AI 助手」图标,浏览器打开 `http://<设备IP>:8088`。 -4. Docker 版镜像首次会从 Docker Hub `jubaoliang/octop:latest` 拉取;请确保该镜像为公开(workflow 已自动设为 public)。本地版无需联网拉镜像,首次启动会按需要补装 Chromium 系统库(需 root 权限,已尽力处理)。 +4. Docker 版镜像首次会从 GHCR `ghcr.io/tencentcloud/octop:latest` 拉取;请确保该包为 Public(首次推送后可在 GitHub Packages 设置)。本地版无需联网拉镜像,首次启动会按需要补装 Chromium 系统库(需 root 权限,已尽力处理)。 > 服务端口固定为 `8088`(飞牛端口映射与桌面图标均据此)。附加组件(browser 浏览器自动化 + desktop 桌面控制)已在镜像中默认安装。 diff --git a/fnos/docker/Dockerfile b/fnos/docker/Dockerfile index fabbfc4d..4de37b74 100644 --- a/fnos/docker/Dockerfile +++ b/fnos/docker/Dockerfile @@ -7,7 +7,7 @@ # - desktop (mss / pynput / pillow,桌面控制附加组件) # # 构建上下文必须为仓库根目录(fnpack / CI 中以仓库根为 context): -# docker build -f fnos/docker/Dockerfile -t jubaoliang/octop:latest . +# docker build -f fnos/docker/Dockerfile -t ghcr.io/tencentcloud/octop:latest . # # 国内加速(可选 build-arg): # PIP_INDEX_URL / PIP_TRUSTED_HOST / NPM_REGISTRY / APT_MIRROR diff --git a/fnos/docker/app/docker/docker-compose.yaml b/fnos/docker/app/docker/docker-compose.yaml index c3ef0082..4c4510d1 100644 --- a/fnos/docker/app/docker/docker-compose.yaml +++ b/fnos/docker/app/docker/docker-compose.yaml @@ -1,6 +1,6 @@ services: octop: - image: jubaoliang/octop:latest + image: ghcr.io/tencentcloud/octop:latest container_name: octop restart: unless-stopped pull_policy: always diff --git a/fnos/docker/manifest b/fnos/docker/manifest index 7db0b030..7f6e0e8b 100644 --- a/fnos/docker/manifest +++ b/fnos/docker/manifest @@ -1,7 +1,7 @@ appname=octop version=0.9.15 display_name=OCTOP -desc=腾讯开源的多用户多智能体 AI 助手平台,支持 Web 控制台、CLI、IM 渠道与定时任务,数据全部本地存储。

安装后使用:
在应用中心点击「打开」按钮即可进入 Web 控制台,默认地址 http://设备IP:8088。
初始管理员账号:admin,初始密码:Octop123(首次登录后请在头像菜单修改密码)。

首次登录后:
请在控制台「设置」中配置 LLM 模型、IM 渠道、远程桌面、浏览器等组件。

注意:Docker 版通过容器部署,会自动从 jubaoliang/octop:latest 拉取镜像运行,已预装 browser / desktop 附加组件。 +desc=腾讯开源的多用户多智能体 AI 助手平台,支持 Web 控制台、CLI、IM 渠道与定时任务,数据全部本地存储。

安装后使用:
在应用中心点击「打开」按钮即可进入 Web 控制台,默认地址 http://设备IP:8088。
初始管理员账号:admin,初始密码:Octop123(首次登录后请在头像菜单修改密码)。

首次登录后:
请在控制台「设置」中配置 LLM 模型、IM 渠道、远程桌面、浏览器等组件。

注意:Docker 版通过容器部署,会自动从 ghcr.io/tencentcloud/octop:latest 拉取镜像运行,已预装 browser / desktop 附加组件。 source=thirdparty platform=all maintainer=TencentCloud OrcaKit From ace23aca65432fdffb474aad04ffa7d7deec7ffa Mon Sep 17 00:00:00 2001 From: jubaoliang Date: Thu, 27 Aug 2026 09:40:46 +0000 Subject: [PATCH 2/2] ci: cascade desktop package with release and tighten triggers Stop running the six-platform desktop matrix on every branch push. Auto Tag dispatches Octop Desktop alongside Release and Docker so artifacts attach to the same v* GitHub Release. Co-authored-by: Cursor --- .github/workflows/auto-tag-on-release.yml | 12 +- .github/workflows/octop-desktop.yml | 304 ++++++++++++++++++++++ .github/workflows/release.yml | 3 +- 3 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/octop-desktop.yml diff --git a/.github/workflows/auto-tag-on-release.yml b/.github/workflows/auto-tag-on-release.yml index 0606cbcc..280d23b0 100644 --- a/.github/workflows/auto-tag-on-release.yml +++ b/.github/workflows/auto-tag-on-release.yml @@ -64,10 +64,11 @@ jobs: git push origin "$tag" # GITHUB_TOKEN tag pushes do not cascade to other workflows. Explicitly - # dispatch Release / Docker Publish (workflow_dispatch is exempt). - # FnOS FPK is cascaded from Release after the GitHub Release exists - # (reuses Release wheel + waits for GHCR image from Docker Publish). - - name: Trigger Release and Docker Publish + # dispatch Release / Docker Publish / Desktop Package (workflow_dispatch + # is exempt). FnOS FPK is cascaded from Release after the GitHub Release + # exists (reuses Release wheel + waits for GHCR image from Docker Publish). + # Desktop builds in parallel; its release job upserts zips onto the same v*. + - name: Trigger Release, Docker Publish, and Desktop Package if: steps.check_tag.outputs.should_publish == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -75,3 +76,6 @@ jobs: tag="${{ steps.version.outputs.tag }}" gh workflow run release.yml --ref "$tag" gh workflow run docker-publish.yml --ref "$tag" + gh workflow run octop-desktop.yml --ref "$tag" \ + -f platforms=all \ + -f attach_release=true diff --git a/.github/workflows/octop-desktop.yml b/.github/workflows/octop-desktop.yml new file mode 100644 index 00000000..4a453911 --- /dev/null +++ b/.github/workflows/octop-desktop.yml @@ -0,0 +1,304 @@ +name: Octop Desktop Package + +# 正式发版:Auto Tag 在打 v* 后与 Release / Docker Publish 一并 dispatch(GITHUB_TOKEN +# 打的 tag 不会触发 push workflow)。产物由本 workflow 的 release job upsert 到同一 +# GitHub Release。workflow_dispatch 保留给手工补包 / 按平台重跑。 +# 不要对任意分支 push 跑六平台矩阵。 + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + platforms: + description: "Comma-separated plats, or 'all'" + required: true + default: "all" + type: string + attach_release: + description: "If running on a v* tag, also upload zips to that GitHub Release" + required: false + default: true + type: boolean + release_tag: + description: "GitHub Release tag to attach to (e.g. v0.9.27). Defaults to the current v* tag ref." + required: false + default: "" + type: string + attach_from_run: + description: "Existing workflow run ID — attach its Octop-* artifacts and skip the 6-platform rebuild" + required: false + default: "" + type: string + +permissions: + contents: read + +concurrency: + group: green-portable-${{ github.ref }} + cancel-in-progress: true + +env: + # Prefer GitHub upstream on Actions runners (npmmirror is for CN local builds). + PBS_BASE_URL: https://github.com/astral-sh/python-build-standalone/releases/download/20251209 + PBS_TAG: "20251209" + PBS_PY: "3.12.12" + +jobs: + frontend: + name: Build dashboard + if: github.event_name != 'workflow_dispatch' || github.event.inputs.attach_from_run == '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: "24" + cache: npm + cache-dependency-path: dashboard/package-lock.json + + - name: Build frontend → src/octop/dashboard + run: make build-frontend + + - uses: actions/upload-artifact@v5 + with: + name: dashboard-dist + path: src/octop/dashboard/ + if-no-files-found: error + retention-days: 7 + + package: + name: "${{ matrix.plat }}" + needs: frontend + runs-on: ${{ matrix.os }} + # Matrix runners are native for each plat; pin host detection so x64 Git Bash + # on windows-11-arm does not mis-classify the job as windows-amd64 cross-build. + env: + GREEN_HOST_PLAT: ${{ matrix.plat }} + strategy: + fail-fast: false + matrix: + include: + - plat: linux-amd64 + arch: amd64 + os: ubuntu-latest + - plat: linux-arm64 + arch: arm64 + os: ubuntu-24.04-arm + - plat: darwin-arm64 + arch: arm64 + os: macos-14 + - plat: darwin-amd64 + arch: amd64 + os: macos-15-intel + - plat: windows-amd64 + arch: amd64 + os: windows-latest + - plat: windows-arm64 + arch: arm64 + os: windows-11-arm + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v5 + + - name: Decide whether to build this platform + id: want + run: | + set -euo pipefail + sel="${{ github.event.inputs.platforms || 'all' }}" + sel="$(echo "$sel" | tr '[:upper:]' '[:lower:]' | tr -d ' ')" + plat="${{ matrix.plat }}" + if [[ "$sel" == "all" || ",$sel," == *",$plat,"* ]]; then + echo "build=true" >> "$GITHUB_OUTPUT" + else + echo "build=false" >> "$GITHUB_OUTPUT" + echo "Skipping ${plat} (selection=${sel})" + fi + + - uses: actions/download-artifact@v5 + if: steps.want.outputs.build == 'true' + with: + name: dashboard-dist + path: src/octop/dashboard + + - uses: astral-sh/setup-uv@v6 + if: steps.want.outputs.build == 'true' + with: + enable-cache: true + python-version: "3.12" + + # GitHub Cache outages must not fail the build — PBS download is cheap enough. + - name: Cache python-build-standalone downloads + if: steps.want.outputs.build == 'true' + continue-on-error: true + uses: actions/cache@v5 + with: + path: green/.cache + key: pbs-${{ env.PBS_TAG }}-${{ env.PBS_PY }}-${{ matrix.plat }} + + - name: Bootstrap portable CPython + if: steps.want.outputs.build == 'true' + env: + GREEN_HOST_PLAT: ${{ matrix.plat }} + run: bash desktop/portable/bootstrap-runtime.sh "${{ matrix.plat }}" + + - name: Assemble green zip + if: steps.want.outputs.build == 'true' + env: + GREEN_HOST_PLAT: ${{ matrix.plat }} + run: | + set -euo pipefail + echo "GREEN_HOST_PLAT=${GREEN_HOST_PLAT} RUNNER_ARCH=${RUNNER_ARCH:-} uname=$(uname -ms)" + bash desktop/portable/package.sh "${{ matrix.plat }}" + + - name: Smoke import (native host only) + if: steps.want.outputs.build == 'true' + run: | + set -euo pipefail + staging="desktop/portable/release/Octop-${{ matrix.plat }}" + if [[ -x "${staging}/runtime/bin/python3" ]]; then + py="${staging}/runtime/bin/python3" + elif [[ -f "${staging}/runtime/python.exe" ]]; then + py="${staging}/runtime/python.exe" + else + echo "python missing under ${staging}/runtime" >&2 + exit 1 + fi + req_file="desktop/portable/requirements-${{ matrix.plat }}.txt" + if [[ ! -f "$req_file" ]]; then + echo "frozen requirements missing: ${req_file}" >&2 + exit 1 + fi + verify_args=( + --packages "${staging}/packages" + --requirements "$req_file" + ) + override_file="desktop/portable/overrides-${{ matrix.plat }}.txt" + if [[ -f "$override_file" ]]; then + verify_args+=(--overrides "$override_file") + fi + PYTHONNOUSERSITE=1 \ + "$py" desktop/portable/verify_imports.py \ + "${verify_args[@]}" + # macOS: fail if any native extension linked Homebrew/MacPorts paths. + if [[ "${{ matrix.plat }}" == darwin-* ]]; then + REPO_ROOT="$PWD" # shellcheck source=desktop/portable/_common.sh + source desktop/portable/_common.sh + verify_no_homebrew_dylibs "${staging}/packages" "${{ matrix.plat }}" + fi + + - uses: actions/setup-go@v6 + if: steps.want.outputs.build == 'true' + with: + go-version: "1.25.x" + cache-dependency-path: desktop/src/go.sum + + - name: Install Linux desktop build dependencies + if: steps.want.outputs.build == 'true' && runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev + + - name: Install Wails v3 CLI + if: steps.want.outputs.build == 'true' + run: go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.13 + + - name: Package desktop app with bundled portable runtime + if: steps.want.outputs.build == 'true' + working-directory: desktop/src + run: >- + wails3 task package + ARCH=${{ matrix.arch }} + PORTABLE_ZIP=../portable/release/Octop-${{ matrix.plat }}.zip + + # archive: false — upload the prebuilt zip as-is. Default archive=true would + # wrap it again, so Actions UI / "Download artifact" becomes zip-in-zip + # (Octop-.zip containing another Octop-.zip). Affects every plat. + # With archive:false, artifact name is the filename (name: is ignored). + - uses: actions/upload-artifact@v7 + if: steps.want.outputs.build == 'true' + with: + path: desktop/portable/release/Octop-${{ matrix.plat }}.zip + archive: false + if-no-files-found: error + retention-days: 14 + + - name: Upload bundled desktop package + if: steps.want.outputs.build == 'true' + uses: actions/upload-artifact@v7 + with: + path: desktop/src/bin/Octop-Desktop-${{ matrix.plat }}.* + archive: false + if-no-files-found: error + retention-days: 14 + + release: + name: Attach zips to GitHub Release + needs: package + # always(): still run when package was skipped (attach_from_run rebuild skip). + if: >- + always() && + !cancelled() && + (needs.package.result == 'success' || needs.package.result == 'skipped') && + ( + (github.event_name == 'workflow_dispatch' && + github.event.inputs.attach_from_run != '') || + (startsWith(github.ref, 'refs/tags/v') && + (github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && + github.event.inputs.attach_release == 'true'))) + ) + runs-on: ubuntu-latest + permissions: + contents: write + actions: read + steps: + - name: Resolve release tag + id: rel + run: | + set -euo pipefail + tag="${{ github.event.inputs.release_tag }}" + if [[ -z "$tag" && "${GITHUB_REF}" == refs/tags/v* ]]; then + tag="${GITHUB_REF_NAME}" + fi + if [[ "$tag" != v* ]]; then + echo "Need a v* tag (run on a version tag, or pass release_tag)." >&2 + exit 1 + fi + echo "tag=${tag}" >> "$GITHUB_OUTPUT" + + # v8 required for archive:false artifacts. skip-decompress keeps the + # uploaded .zip / .tar / .dmg / .exe intact — default unzip turns portable + # zips into directories, so files: release-assets/* would skip them. + - uses: actions/download-artifact@v8 + with: + pattern: Octop-* + path: release-assets + merge-multiple: true + skip-decompress: true + repository: ${{ github.repository }} + run-id: ${{ github.event.inputs.attach_from_run || github.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: List release assets + run: | + set -euo pipefail + ls -lh release-assets/ + if find release-assets -mindepth 1 -maxdepth 1 -type d | grep -q .; then + echo "Download extracted archives into directories; refuse incomplete attach." >&2 + find release-assets -mindepth 1 -maxdepth 1 -print + exit 1 + fi + + - name: Upload to GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.rel.outputs.tag }} + files: release-assets/* + fail_on_unmatched_files: true + # Upsert: works whether Release workflow already created the release. + generate_release_notes: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82d9dc07..48fbe6b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,8 @@ name: Release # - Sync Main Into Develop # - Build Octop FPK(复用本 Release 的 wheel;等待 docker-publish 的 GHCR 镜像) # -# 与 docker-publish.yml 并行(由 Auto Tag 同时 dispatch,或人工 push tag 同时触发)。 +# 与 docker-publish.yml / octop-desktop.yml 并行(由 Auto Tag 同时 dispatch, +# 或人工 push v* tag 同时触发)。桌面产物由 Desktop Package 挂到同一 GitHub Release。 on: push: