From 790f63c657d9aac9d0a2dbf1eda1d5df3796747e Mon Sep 17 00:00:00 2001 From: yuanhe Date: Sat, 3 Oct 2026 12:39:29 +0800 Subject: [PATCH 1/4] Cache the pinned MiniMax companion in native CI The companion is a pure function of packages/mcode-harness, its build script and the platform; rebuilding it took about 50 seconds per Unix native job. --- .github/workflows/native.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index a11811c6..459effc2 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -108,8 +108,15 @@ jobs: rm -rf "$RUNNER_TEMP/claude-runtime/node_modules" pnpm --dir "$RUNNER_TEMP/claude-runtime" install --prod --frozen-lockfile --config.node-linker=hoisted --ignore-scripts npm install --prefix "$RUNNER_TEMP/native-tools" --no-save @openai/codex@0.153.4 - - name: Build the pinned MiniMax companion on Unix + - name: Restore the pinned MiniMax companion + id: mcode-cache if: runner.os != 'Windows' + uses: actions/cache@v6 + with: + path: ${{ runner.temp }}/minimax-runtime + key: mcode-harness-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/mcode-harness/**', 'scripts/build-mcode-harness.sh') }} + - name: Build the pinned MiniMax companion on Unix + if: runner.os != 'Windows' && steps.mcode-cache.outputs.cache-hit != 'true' run: | source_repository="$(node -p 'require("./packages/mcode-harness/source.json").repository')" source_revision="$(node -p 'require("./packages/mcode-harness/source.json").revision')" From 07da2d2662bf5b2d366185803af8932fe99f7f15 Mon Sep 17 00:00:00 2001 From: yuanhe Date: Sat, 3 Oct 2026 12:45:23 +0800 Subject: [PATCH 2/4] Share the Runtime base layer and skip the release artifact on success The Codex image used debian with Debian's nodejs and npm, a 44 second package layer that the combined Runtime image discards. It now uses the same node base and package layer as the Claude and MiniMax images, so one build reuses that layer. The recovery artifact is uploaded only when publication fails or a manual build does not publish. --- .github/workflows/release.yml | 16 +++++++++------- docs/maintainers.md | 2 +- docs/zh/maintainers.md | 4 ++-- services/core/deploy/codex/Dockerfile | 11 ++++++----- services/core/deploy/codex/README.md | 4 ++-- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c370b5fe..f9c8494b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,13 +134,6 @@ jobs: done cp deploy/install.sh "$HOME/.oac/build/release-upload/install.sh" (cd "$HOME/.oac/build/release-upload" && sha256sum install.sh > install.sh.sha256) - - uses: actions/upload-artifact@v6 - with: - name: core-release-${{ steps.source.outputs.revision }} - path: ~/.oac/build/release-upload/* - compression-level: 0 - if-no-files-found: error - - name: Sign in to GHCR if: github.event_name == 'push' || inputs.draft_release env: @@ -150,6 +143,7 @@ jobs: echo "DOCKER_CONFIG=$DOCKER_CONFIG" >> "$GITHUB_ENV" printf '%s' "$GHCR_TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin - name: Publish the version tag or create a manual draft + id: publish if: github.event_name == 'push' || inputs.draft_release env: GH_TOKEN: ${{ github.token }} @@ -161,3 +155,11 @@ jobs: - name: Remove registry credentials if: always() && (github.event_name == 'push' || inputs.draft_release) run: rm -f "$RUNNER_TEMP/oac-release-docker/config.json" + # Recovering a failed publication reuses these exact assets. + - uses: actions/upload-artifact@v6 + if: (failure() && steps.publish.outcome == 'failure') || (success() && steps.publish.outcome == 'skipped') + with: + name: core-release-${{ steps.source.outputs.revision }} + path: ~/.oac/build/release-upload/* + compression-level: 0 + if-no-files-found: error diff --git a/docs/maintainers.md b/docs/maintainers.md index e6cdaa05..1b382dbb 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -124,7 +124,7 @@ git push origin v1.2.3 Tags use `vMAJOR.MINOR.PATCH`, optionally with a prerelease suffix such as `-rc.1` and build metadata such as `+build.1`. A prerelease suffix creates a GitHub prerelease. Pushing the tag is the release decision. Automated checks establish build and test results, not real-model qualification: assess live execution evidence before you push the tag. Model credentials and private certificate authorities never enter CI or release inputs, including acceptance images that contain them. -The workflow runs `check` on the tagged commit, including the full local gate, official-client and image acceptance, and the native matrix with its packaging artifacts enabled. After checks succeed, one `build` job on GitHub-hosted `ubuntu-22.04` prepares the pinned Runtime inputs, reuses the native installers, builds the distribution and publishes directly from its local files. This combined job has `contents: write` and `packages: write`; checkout does not persist credentials. It retains an uncompressed Actions artifact before publication for recovery, without downloading that artifact again during normal publication. +The workflow runs `check` on the tagged commit, including the full local gate, official-client and image acceptance, and the native matrix with its packaging artifacts enabled. After checks succeed, one `build` job on `blacksmith-4vcpu-ubuntu-2204` prepares the pinned Runtime inputs, reuses the native installers, builds the distribution and publishes directly from its local files. This combined job has `contents: write` and `packages: write`; checkout does not persist credentials. It retains the assets as an uncompressed Actions artifact only when publication fails, for recovery, or when a manual build does not publish. Distribution and Runtime archives use `pigz` level 6 with at most four compression workers and no filename or timestamp in the gzip header. The publisher verifies archive and native installer checksums, resolves the repository identity, refuses an existing Release or draft for the tag and creates one draft with a fixed ID. Up to four assets upload concurrently, largest first, without retries. After confirming the complete remote inventory, the publisher validates all image archives and existing registry tags before pushing up to four images concurrently. Each image config and registry manifest is verified; any error leaves the Release unpublished. In-flight transfers finish before a failed operation returns. The publisher rechecks the version tag before publishing the draft by its ID. diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index c2f18137..a0fbe34b 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: ac744d8df2682f0c19eb6b05c1ef50a9e7c7a9d214316317458669f4cb00f477 +source_hash: 461f757bab0a3b39cd51f41f64880015822e42ba156d6c3596bb30b87524baef --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -126,7 +126,7 @@ git push origin v1.2.3 标签使用 `vMAJOR.MINOR.PATCH` 格式,可选用 `-rc.1` 等预发布后缀以及 `+build.1` 等构建元数据。预发布后缀会创建 GitHub 预发布版。推送标签即表示发布决定。自动检查用于确定构建和测试结果,而不是真实模型资格:推送标签前应评估实际执行证据。模型凭据和私有证书颁发机构绝不能进入 CI 或发布输入,包含它们的验收镜像也不例外。 -工作流会在带标签的提交上运行 `check`,包括完整的本地门禁、官方客户端和镜像验收,以及启用打包构件的原生平台矩阵。检查成功后,GitHub 托管的 `ubuntu-22.04` 上的一个 `build` 作业会准备固定的 Runtime 输入、复用原生安装器、构建分发包,并直接从本地文件发布。此合并作业具有 `contents: write` 和 `packages: write` 权限;检出过程不会保留凭据。发布前会保留一份未压缩的 Actions 构建产物以供恢复使用,正常发布期间不会再次下载该构建产物。 +工作流会在带标签的提交上运行 `check`,包括完整的本地门禁、官方客户端和镜像验收,以及启用打包构件的原生平台矩阵。检查成功后,`blacksmith-4vcpu-ubuntu-2204` 上的一个 `build` 作业会准备固定的 Runtime 输入、复用原生安装器、构建分发包,并直接从本地文件发布。此合并作业具有 `contents: write` 和 `packages: write` 权限;检出过程不会保留凭据。只有发布失败或手动构建不发布时,才会把这些文件保留为未压缩的 Actions 构建产物,供恢复使用。 分发归档和 Runtime 归档使用 `pigz` 级别 6,最多使用四个压缩工作线程,并且 gzip 头部中不包含文件名或时间戳。发布器会验证归档和原生安装器校验和、解析仓库身份、拒绝使用该标签已有的 Release 或草稿,并创建一个具有固定 ID 的草稿。最多四个资源可并发上传,按从大到小的顺序进行,且不会重试。确认完整的远程资源清单后,发布器会验证所有镜像归档和现有注册表标签,再并发推送最多四个镜像。每个镜像配置和注册表清单都会接受验证;任何错误都会使 Release 保持未发布状态。失败操作返回前,正在进行的传输会完成。发布器会按 ID 发布草稿,并在发布前重新检查版本标签。 diff --git a/services/core/deploy/codex/Dockerfile b/services/core/deploy/codex/Dockerfile index 79ef9471..e79a93cd 100644 --- a/services/core/deploy/codex/Dockerfile +++ b/services/core/deploy/codex/Dockerfile @@ -1,12 +1,13 @@ # Build context is a prepared binary bundle, never the product checkout. # Native package and resources must both be Codex 0.153.4 (linux/amd64). -FROM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251 +# The base and package layer match the Claude and MiniMax images, so one build +# shares that layer and the combined Runtime image keeps the same base. +FROM node:22.23.1-bookworm-slim@sha256:8607a9064d4a571140998ae9e52a3b3fcf9cff361d04642d5971e6cd76d39e27 +USER root RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates bash git python3 python3-pip nodejs npm ripgrep \ + ca-certificates bash git python3 python3-pip ripgrep \ && rm -rf /var/lib/apt/lists/* \ - && mkdir -p /environment/workspace /workspace /home/runtime \ - && groupadd --gid 1000 runtime \ - && useradd --uid 1000 --gid 1000 --home-dir /home/runtime --no-create-home runtime + && mkdir -p /environment/workspace /workspace /home/runtime COPY --chmod=0555 oac-daemon codex /usr/local/bin/ COPY codex-resources /usr/local/codex-resources ENV HOME=/home/runtime OAC_RUNTIME_HOME=/home/runtime/.oac \ diff --git a/services/core/deploy/codex/README.md b/services/core/deploy/codex/README.md index 61cb9bc9..42ef1092 100644 --- a/services/core/deploy/codex/README.md +++ b/services/core/deploy/codex/README.md @@ -63,9 +63,9 @@ Every other variant stays unclassified. | Item | Value | | --- | --- | -| Base | Digest-pinned `debian:bookworm-slim` with `ca-certificates`, `bash`, `git`, `python3`, `python3-pip`, `nodejs`, `npm` and `ripgrep` | +| Base | Digest-pinned `node:22.23.1-bookworm-slim` with `ca-certificates`, `bash`, `git`, `python3`, `python3-pip` and `ripgrep`, the same base and package layer as the Claude and MiniMax images | | Programs | `/usr/local/bin/oac-daemon`, `/usr/local/bin/codex` (mode 0555) and `/usr/local/codex-resources` | -| User | `runtime`, UID/GID 1000, home `/home/runtime` | +| User | UID/GID 1000 with `HOME=/home/runtime` | | Environment | `OAC_RUNTIME_HOME=/home/runtime/.oac`, `OAC_RUNTIME_CODEX_BIN=/usr/local/bin/codex`, `OAC_RUNTIME_WORKSPACE=/environment/workspace`, `OAC_RUNTIME_INITIALIZATION_DIRECTORY=/environment/initialization`, `OAC_RUNTIME_PACKAGE_DIRECTORY=/environment/packages` | | Entry point | `oac-daemon connect --profile default`, working directory `/environment/workspace` | From bf7a0ab23ed6093cff51089a8f5dc29f3d90c5cf Mon Sep 17 00:00:00 2001 From: yuanhe Date: Sat, 3 Oct 2026 13:03:22 +0800 Subject: [PATCH 3/4] Cache input-addressed CI builds and warm them on main The E2B helper manifest no longer records the source revision, so the helper is a pure function of its inputs and is cached by their hash. Composite actions own the companion and E2B cache keys, and a build-only cache-warm workflow saves them and the pnpm store on main, the only ref whose caches every pull request and release tag can restore. --- .github/actions/e2b-provider/action.yml | 9 ++++ .github/actions/mcode-companion/action.yml | 25 ++++++++++ .github/actions/node/action.yml | 2 + .github/workflows/api-acceptance.yml | 2 + .github/workflows/cache-warm.yml | 58 ++++++++++++++++++++++ .github/workflows/native.yml | 23 +-------- .github/workflows/release.yml | 1 + docs/maintainers.md | 4 +- docs/zh/maintainers.md | 6 +-- scripts/build-core-distribution.sh | 2 +- scripts/build-e2b-provider.sh | 36 +++++++++----- scripts/ci_plan.py | 5 +- scripts/ci_plan_test.py | 7 +++ services/core/tools/e2b-provider/build.py | 3 +- 14 files changed, 141 insertions(+), 42 deletions(-) create mode 100644 .github/actions/e2b-provider/action.yml create mode 100644 .github/actions/mcode-companion/action.yml create mode 100644 .github/workflows/cache-warm.yml diff --git a/.github/actions/e2b-provider/action.yml b/.github/actions/e2b-provider/action.yml new file mode 100644 index 00000000..478747d9 --- /dev/null +++ b/.github/actions/e2b-provider/action.yml @@ -0,0 +1,9 @@ +name: E2B helper cache +description: Restore the input-addressed E2B helper builds that scripts/build-e2b-provider.sh reuses. +runs: + using: composite + steps: + - uses: actions/cache@v6 + with: + path: ~/.oac/cache/e2b-provider + key: e2b-provider-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('services/core/tools/e2b-provider/**', 'LICENSE', 'scripts/build-e2b-provider.sh') }} diff --git a/.github/actions/mcode-companion/action.yml b/.github/actions/mcode-companion/action.yml new file mode 100644 index 00000000..d8dfe7fa --- /dev/null +++ b/.github/actions/mcode-companion/action.yml @@ -0,0 +1,25 @@ +name: MiniMax companion +description: Restore the pinned MiniMax companion into $RUNNER_TEMP/minimax-runtime, building it only when its inputs change. +runs: + using: composite + steps: + - id: cache + uses: actions/cache@v6 + with: + path: ${{ runner.temp }}/minimax-runtime + key: mcode-harness-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/mcode-harness/**', 'scripts/build-mcode-harness.sh') }} + - if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: | + source_repository="$(node -p 'require("./packages/mcode-harness/source.json").repository')" + source_revision="$(node -p 'require("./packages/mcode-harness/source.json").revision')" + source_version="$(node -p 'require("./packages/mcode-harness/source.json").version')" + git init --quiet "$RUNNER_TEMP/mcode-source" + git -C "$RUNNER_TEMP/mcode-source" remote add origin "$source_repository" + git -C "$RUNNER_TEMP/mcode-source" fetch --depth 1 origin "$source_revision" + git -C "$RUNNER_TEMP/mcode-source" checkout --detach FETCH_HEAD + npm install --prefix "$RUNNER_TEMP/mcode-native" --no-audit --no-fund --include=optional --install-strategy=nested "@minimax-ai/code@$source_version" + MCODE_NATIVE_SOURCE="$RUNNER_TEMP/mcode-source" \ + MCODE_CLI_DIR="$RUNNER_TEMP/mcode-native/node_modules/@minimax-ai/code" \ + MCODE_HARNESS_BUILD_DIR="$RUNNER_TEMP/minimax-runtime" \ + bash scripts/build-mcode-harness.sh diff --git a/.github/actions/node/action.yml b/.github/actions/node/action.yml index 081efaa9..dd4c1b5c 100644 --- a/.github/actions/node/action.yml +++ b/.github/actions/node/action.yml @@ -25,3 +25,5 @@ runs: with: path: ${{ steps.store.outputs.path }} key: pnpm-v1-${{ runner.os }}-${{ runner.arch }}-node${{ inputs.node-version }}-10.30.3-${{ hashFiles(inputs.lockfiles) }} + # The store is content-addressed; another lockfile's store still supplies shared packages. + restore-keys: pnpm-v1-${{ runner.os }}-${{ runner.arch }}-node${{ inputs.node-version }}-10.30.3- diff --git a/.github/workflows/api-acceptance.yml b/.github/workflows/api-acceptance.yml index 4c881c7d..a90f1be0 100644 --- a/.github/workflows/api-acceptance.yml +++ b/.github/workflows/api-acceptance.yml @@ -64,6 +64,8 @@ jobs: run: | python services/core/tests/official_client.py go test ./services/core/internal/store -run '^(TestFunctionStateOfficialClientReadsAndLiveEvents|TestSavedReferenceRetryOfficialClient|TestAgentUpdateOfficialClient|TestAgentDeletionOfficialClient|TestSessionAgentFilterOfficialClient|TestSessionDeletionOfficialClient|TestEnvironmentInitialFailureOfficialClient|TestSelfHostedInitialCreationOfficialClient|TestSelfHostedCancellationOfficialClient|TestSelfHostedFunctionsOfficialClient|TestSelfHostedSteeringOfficialClient)$' -count=1 + - uses: ./.github/actions/e2b-provider + if: inputs.container - name: Verify the distribution's Core image if: inputs.container env: diff --git a/.github/workflows/cache-warm.yml b/.github/workflows/cache-warm.yml new file mode 100644 index 00000000..bcec6b40 --- /dev/null +++ b/.github/workflows/cache-warm.yml @@ -0,0 +1,58 @@ +name: cache-warm + +# Caches saved on main are the only ones every pull request and release tag can +# restore. This builds the input-addressed caches; it runs no tests. +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: cache-warm + cancel-in-progress: false + +jobs: + companion: + strategy: + fail-fast: false + matrix: + include: + - runner: blacksmith-2vcpu-ubuntu-2204 + github-runner: ubuntu-22.04 + - runner: macos-15 + github-runner: macos-15 + runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && matrix.github-runner || matrix.runner }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/node + with: + node-version: '22.22.0' + lockfiles: | + packages/claude-sdk-adapter/pnpm-lock.yaml + packages/mcode-harness/package-lock.json + - run: pnpm --dir packages/claude-sdk-adapter fetch + - uses: ./.github/actions/mcode-companion + + linux: + runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && 'ubuntu-22.04' || 'blacksmith-2vcpu-ubuntu-2204' }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/node + with: + lockfiles: | + pnpm-lock.yaml + apps/web/pnpm-lock.yaml + packages/agents-client/pnpm-lock.yaml + packages/claude-sdk-adapter/pnpm-lock.yaml + website/pnpm-lock.yaml + example/parsar/pnpm-lock.yaml + - name: Fetch every pnpm lockfile into the store + run: for dir in . apps/web packages/agents-client packages/claude-sdk-adapter website example/parsar; do pnpm --dir "$dir" fetch; done + - uses: ./.github/actions/e2b-provider + - name: Build the E2B helper + run: E2B_PROVIDER_BUILD_DIR="$RUNNER_TEMP/e2b-provider" bash scripts/build-e2b-provider.sh diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index 459effc2..7be750d3 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -108,28 +108,9 @@ jobs: rm -rf "$RUNNER_TEMP/claude-runtime/node_modules" pnpm --dir "$RUNNER_TEMP/claude-runtime" install --prod --frozen-lockfile --config.node-linker=hoisted --ignore-scripts npm install --prefix "$RUNNER_TEMP/native-tools" --no-save @openai/codex@0.153.4 - - name: Restore the pinned MiniMax companion - id: mcode-cache + - name: Restore or build the pinned MiniMax companion on Unix if: runner.os != 'Windows' - uses: actions/cache@v6 - with: - path: ${{ runner.temp }}/minimax-runtime - key: mcode-harness-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('packages/mcode-harness/**', 'scripts/build-mcode-harness.sh') }} - - name: Build the pinned MiniMax companion on Unix - if: runner.os != 'Windows' && steps.mcode-cache.outputs.cache-hit != 'true' - run: | - source_repository="$(node -p 'require("./packages/mcode-harness/source.json").repository')" - source_revision="$(node -p 'require("./packages/mcode-harness/source.json").revision')" - source_version="$(node -p 'require("./packages/mcode-harness/source.json").version')" - git init --quiet "$RUNNER_TEMP/mcode-source" - git -C "$RUNNER_TEMP/mcode-source" remote add origin "$source_repository" - git -C "$RUNNER_TEMP/mcode-source" fetch --depth 1 origin "$source_revision" - git -C "$RUNNER_TEMP/mcode-source" checkout --detach FETCH_HEAD - npm install --prefix "$RUNNER_TEMP/mcode-native" --no-audit --no-fund --include=optional --install-strategy=nested "@minimax-ai/code@$source_version" - MCODE_NATIVE_SOURCE="$RUNNER_TEMP/mcode-source" \ - MCODE_CLI_DIR="$RUNNER_TEMP/mcode-native/node_modules/@minimax-ai/code" \ - MCODE_HARNESS_BUILD_DIR="$RUNNER_TEMP/minimax-runtime" \ - bash scripts/build-mcode-harness.sh + uses: ./.github/actions/mcode-companion - name: Package and exercise CLI-only installation, additions and reuse id: package run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9c8494b..72bcd1f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -114,6 +114,7 @@ jobs: path: ${{ runner.temp }}/native-artifacts - name: Assemble the native installation catalog run: node scripts/build-native-catalog.mjs "$RUNNER_TEMP/native-artifacts" "$RUNNER_TEMP/native-installers" + - uses: ./.github/actions/e2b-provider - name: Build matched artifacts env: OAC_NATIVE_INSTALLER_BUILD_DIR: ${{ runner.temp }}/native-installers diff --git a/docs/maintainers.md b/docs/maintainers.md index 1b382dbb..0f6e82a2 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -94,7 +94,7 @@ The distribution combines the three Harness images into one Runtime image (`depl make build-e2b-provider ``` -Docker builds the Linux amd64 helper with the pinned CPython and Debian 12 image. The Python dependency closure, including PyInstaller, is hash-locked in `services/core/tools/e2b-provider/requirements.lock`; no E2B account key is needed. Set `E2B_PROVIDER_BUILD_DIR` for another output directory and `E2B_SOURCE_REVISION` when building from an exported source tree. The output is `oac-e2b-provider-linux-amd64.tar.gz` with its `.sha256`; it extracts to `oac-e2b-provider/` with the executable, `_internal/`, `licenses/`, `requirements.lock` and `manifest.json`. The Core image uses that tree; the host needs a compatible glibc and CA certificates, not Python. +Docker builds the Linux amd64 helper with the pinned CPython and Debian 12 image. The Python dependency closure, including PyInstaller, is hash-locked in `services/core/tools/e2b-provider/requirements.lock`; no E2B account key is needed. Set `E2B_PROVIDER_BUILD_DIR` for another output directory. The build is a pure function of the helper sources, `LICENSE` and the build script, so it is cached under `~/.oac/cache/e2b-provider/` by their hash and rebuilt only when they change. The output is `oac-e2b-provider-linux-amd64.tar.gz` with its `.sha256`; it extracts to `oac-e2b-provider/` with the executable, `_internal/`, `licenses/`, `requirements.lock` and `manifest.json`. The Core image uses that tree; the host needs a compatible glibc and CA certificates, not Python. **microsandbox helper.** Linux only, with a C compiler: @@ -156,7 +156,7 @@ With `draft_release=true` the result is an unpublished `build-` draft ## Continuous integration -Every PR runs `core-check` and reports the required status `check`. Main uses GitHub branch protection requiring this check and an up-to-date branch before merging, so merging does not start another copy of the test suite. Changes must enter through checked PRs; an administrator bypass does not establish CI success. Main pushes publish the website when its inputs change. Version tags and manual release builds run the full release gate at their exact source commit. `scripts/ci_plan.py` owns the only input-to-check map. Component rules require both a matching directory or script prefix and a matching file suffix; exact dependency, workflow and shared build inputs have explicit rules. Rules accumulate across shared consumers and mixed changes. Paths with no matching build/test rule run hygiene only. Add the corresponding rule when introducing a new component, language, build input or resource location. +Every PR runs `core-check` and reports the required status `check`. Main uses GitHub branch protection requiring this check and an up-to-date branch before merging, so merging does not start another copy of the test suite. Changes must enter through checked PRs; an administrator bypass does not establish CI success. Main pushes publish the website when its inputs change and run `cache-warm`, which builds the MiniMax companion, the E2B helper and the pnpm store without running tests, because only caches saved on main can be restored by every PR and release tag. Version tags and manual release builds run the full release gate at their exact source commit. `scripts/ci_plan.py` owns the only input-to-check map. Component rules require both a matching directory or script prefix and a matching file suffix; exact dependency, workflow and shared build inputs have explicit rules. Rules accumulate across shared consumers and mixed changes. Paths with no matching build/test rule run hygiene only. Add the corresponding rule when introducing a new component, language, build input or resource location. The planner compares the PR event's tested merge commit with its verified first parent. NUL-delimited Git output and disabled rename detection retain both old and new paths. The plan and reasons appear in the run summary. Missing or inconsistent history, mismatched checkouts, invalid paths, planner/orchestration changes and shared build inputs select the full gate. A verified empty diff selects hygiene only. Release, manual and explicit-ref calls always select every group. diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index a0fbe34b..e5030fc3 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: 461f757bab0a3b39cd51f41f64880015822e42ba156d6c3596bb30b87524baef +source_hash: ed4a16eca4710a2bc8be67b07d465e599d77638cb53ef34059fe4635671f18a0 --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -96,7 +96,7 @@ docker build --platform linux/amd64 -t oac-runtime:mcode "${OAC_DEV_HOME:-$HOME/ make build-e2b-provider ``` -Docker 使用固定版本的 CPython 和 Debian 12 镜像构建 Linux amd64 辅助程序。Python 依赖闭包(including PyInstaller)在 `services/core/tools/e2b-provider/requirements.lock` 中按哈希锁定;不需要 E2B 账户密钥。要使用其他输出目录,请设置 `E2B_PROVIDER_BUILD_DIR`;从导出的源代码树构建时,请设置 `E2B_SOURCE_REVISION`。输出为 `oac-e2b-provider-linux-amd64.tar.gz` 及其 `.sha256`;解压后会得到 `oac-e2b-provider/`,其中包含可执行文件、`_internal/`、`licenses/`、`requirements.lock` 和 `manifest.json`。Core 镜像使用该目录树;主机需要兼容的 glibc 和 CA 证书,而不需要 Python。 +Docker 使用固定版本的 CPython 和 Debian 12 镜像构建 Linux amd64 辅助程序。Python 依赖闭包(including PyInstaller)在 `services/core/tools/e2b-provider/requirements.lock` 中按哈希锁定;不需要 E2B 账户密钥。要使用其他输出目录,请设置 `E2B_PROVIDER_BUILD_DIR`。构建结果完全由辅助程序源代码、`LICENSE` 和构建脚本决定,因此会按它们的哈希缓存在 `~/.oac/cache/e2b-provider/` 下,仅在它们变化时重新构建。输出为 `oac-e2b-provider-linux-amd64.tar.gz` 及其 `.sha256`;解压后会得到 `oac-e2b-provider/`,其中包含可执行文件、`_internal/`、`licenses/`、`requirements.lock` 和 `manifest.json`。Core 镜像使用该目录树;主机需要兼容的 glibc 和 CA 证书,而不需要 Python。 **microsandbox 辅助程序。** 仅支持 Linux,并且需要 C 编译器: @@ -158,7 +158,7 @@ gh workflow run core-release --repo MiniMax-AI/OpenAgentCore --ref main \ ## 持续集成 {#continuous-integration} -每个 PR 都会运行 `core-check` 并报告必需状态 `check`。main 使用 GitHub 分支保护,合并前必须通过此检查且分支必须为最新状态,因此合并不会启动另一份测试套件。所有更改都必须通过经过检查的 PR 提交;管理员绕过检查并不代表 CI 成功。推送到 main 时,如果输入发生变化,就会发布网站。版本标签和手动发布构建会在其确切源代码提交上运行完整的发布门禁。`scripts/ci_plan.py` 管理唯一的输入到检查映射。组件规则要求同时匹配目录或脚本前缀以及文件后缀;确切的依赖项、工作流和共享构建输入都有明确规则。规则会在共享使用方和混合变更之间累加。没有匹配构建/测试规则的路径仅运行 hygiene。引入新组件、语言、构建输入或资源位置时,请添加相应规则。 +每个 PR 都会运行 `core-check` 并报告必需状态 `check`。main 使用 GitHub 分支保护,合并前必须通过此检查且分支必须为最新状态,因此合并不会启动另一份测试套件。所有更改都必须通过经过检查的 PR 提交;管理员绕过检查并不代表 CI 成功。推送到 main 时,如果输入发生变化,就会发布网站,并运行 `cache-warm`:它构建 MiniMax companion、E2B 辅助程序和 pnpm 存储,不运行测试,因为只有 main 上保存的缓存能被每个 PR 和发布标签恢复。版本标签和手动发布构建会在其确切源代码提交上运行完整的发布门禁。`scripts/ci_plan.py` 管理唯一的输入到检查映射。组件规则要求同时匹配目录或脚本前缀以及文件后缀;确切的依赖项、工作流和共享构建输入都有明确规则。规则会在共享使用方和混合变更之间累加。没有匹配构建/测试规则的路径仅运行 hygiene。引入新组件、语言、构建输入或资源位置时,请添加相应规则。 计划器会将 PR 事件所测试的合并提交与其已验证的第一个父提交进行比较。NUL 分隔的 Git 输出和禁用重命名检测会同时保留旧路径和新路径。计划及原因会显示在运行摘要中。历史记录缺失或不一致、检出不匹配、路径无效、计划器/编排发生变更以及共享构建输入发生变化时,都会选择完整门禁。经过验证的空差异仅选择 hygiene。发布、手动和显式 ref 调用始终选择所有组。 diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index 2fd3b476..2024eb2e 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -109,7 +109,7 @@ mkdir -p "$bundle/runtime" cp services/core/deploy/codex/seccomp.json "$bundle/runtime/" cp LICENSE "$bundle/" -OAC_DEV_BUILD_REVISION="$revision" E2B_SOURCE_REVISION="$revision" scripts/build-core-image-context.sh "$stage/core" +OAC_DEV_BUILD_REVISION="$revision" scripts/build-core-image-context.sh "$stage/core" ( cd services/core/tools/microsandbox-provider GOWORK=off CGO_ENABLED=1 go build -mod=readonly -trimpath \ diff --git a/scripts/build-e2b-provider.sh b/scripts/build-e2b-provider.sh index 68660a79..cd91fb5e 100755 --- a/scripts/build-e2b-provider.sh +++ b/scripts/build-e2b-provider.sh @@ -8,15 +8,27 @@ case "$output_dir" in *) printf 'E2B_PROVIDER_BUILD_DIR must be absolute\n' >&2; exit 1 ;; esac mkdir -p "$output_dir" -source_revision="${E2B_SOURCE_REVISION:-$(git -C "$repo_root" rev-parse HEAD)}" -image="oac-e2b-provider-build:${source_revision:0:12}" -# Proxy values are build-only operator settings; no account key is needed. -docker build --platform linux/amd64 --build-arg HTTP_PROXY --build-arg HTTPS_PROXY \ - --build-arg ALL_PROXY --build-arg NO_PROXY \ - --file "$repo_root/services/core/tools/e2b-provider/Build.Dockerfile" \ - --tag "$image" "$repo_root/services/core/tools/e2b-provider" -docker run --rm --platform linux/amd64 \ - --env HTTP_PROXY --env HTTPS_PROXY --env ALL_PROXY --env NO_PROXY \ - --env "E2B_SOURCE_REVISION=$source_revision" \ - --mount "type=bind,src=$repo_root,dst=/source,readonly" \ - --mount "type=bind,src=$output_dir,dst=/output" "$image" +archive=oac-e2b-provider-linux-amd64.tar.gz +# The helper is a pure function of these files, so a build is reused by their hash. +inputs="$(cd "$repo_root" && { + find services/core/tools/e2b-provider -type f ! -path '*/__pycache__/*' -print0 | sort -z | xargs -0 sha256sum + sha256sum LICENSE scripts/build-e2b-provider.sh +} | sha256sum | cut -c1-64)" +cache="${OAC_DEV_HOME:-$HOME/.oac}/cache/e2b-provider/$inputs" +if [[ ! -f "$cache/$archive.sha256" ]]; then + mkdir -p "${cache%/*}" + build="$(mktemp -d "${cache%/*}/.build.XXXXXX")" + image="oac-e2b-provider-build:${inputs:0:12}" + # Proxy values are build-only operator settings; no account key is needed. + docker build --platform linux/amd64 --build-arg HTTP_PROXY --build-arg HTTPS_PROXY \ + --build-arg ALL_PROXY --build-arg NO_PROXY \ + --file "$repo_root/services/core/tools/e2b-provider/Build.Dockerfile" \ + --tag "$image" "$repo_root/services/core/tools/e2b-provider" + docker run --rm --platform linux/amd64 \ + --env HTTP_PROXY --env HTTPS_PROXY --env ALL_PROXY --env NO_PROXY \ + --mount "type=bind,src=$repo_root,dst=/source,readonly" \ + --mount "type=bind,src=$build,dst=/output" "$image" + mv -T "$build" "$cache" 2>/dev/null || rm -rf "$build" +fi +(cd "$cache" && sha256sum --check --quiet "$archive.sha256") +cp "$cache/$archive" "$cache/$archive.sha256" "$output_dir/" diff --git a/scripts/ci_plan.py b/scripts/ci_plan.py index 6aee79b5..30d0f61b 100644 --- a/scripts/ci_plan.py +++ b/scripts/ci_plan.py @@ -21,6 +21,9 @@ ".github/workflows/ci-review.yml": ("lint",), ".github/workflows/website.yml": ("website", "lint"), ".github/actions/node/action.yml": (*NODE_JOBS, "lint"), + ".github/actions/mcode-companion/action.yml": ("native", "lint"), + ".github/actions/e2b-provider/action.yml": ("api", "lint"), + ".github/workflows/cache-warm.yml": ("lint",), "scripts/ci_plan.py": JOBS, "scripts/ci_plan_test.py": ("hygiene",), "scripts/ci_metrics.py": ("hygiene",), @@ -99,7 +102,7 @@ "services/core/internal/sandbox/e2b/testdata/configuration-selectors.json": ("distribution",), } FULL_INPUTS = {"Makefile", ".gitignore", ".gitattributes", ".dockerignore"} -IMAGE_FILES = {"go.mod", "go.sum", "go.work", "go.work.sum", ".github/workflows/api-acceptance.yml"} +IMAGE_FILES = {"go.mod", "go.sum", "go.work", "go.work.sum", ".github/workflows/api-acceptance.yml", ".github/actions/e2b-provider/action.yml"} IMAGE_INPUTS = ("scripts/build-core", "scripts/build-e2b-provider", "deploy/distribution/", "services/core/tools/e2b-provider/", "services/core/deploy/e2b/") # Generated outputs retain freshness checks even when the file is documentation. diff --git a/scripts/ci_plan_test.py b/scripts/ci_plan_test.py index cd306819..8784e371 100644 --- a/scripts/ci_plan_test.py +++ b/scripts/ci_plan_test.py @@ -88,12 +88,19 @@ def test_workflow_changes_select_only_their_consumers(self): "website": {"hygiene", "website", "lint"}, "native": {"hygiene", "native", "lint"}, "api-acceptance": {"hygiene", "api", "lint"}, + "cache-warm": {"hygiene", "lint"}, }.items(): with self.subTest(workflow=workflow): plan = ci.select([f".github/workflows/{workflow}.yml"]) self.assertEqual(set(plan["jobs"]), selected) self.assertEqual(plan["image"], workflow == "api-acceptance") + def test_cache_actions_select_their_consumers(self): + self.assertEqual(self.jobs(".github/actions/mcode-companion/action.yml"), {"hygiene", "native", "lint"}) + plan = ci.select([".github/actions/e2b-provider/action.yml"]) + self.assertEqual(set(plan["jobs"]), {"hygiene", "api", "lint"}) + self.assertTrue(plan["image"]) + def test_node_action_selects_all_direct_consumers_and_lint(self): root = Path(__file__).resolve().parents[1] workflow = (root / ".github/workflows/check.yml").read_text() diff --git a/services/core/tools/e2b-provider/build.py b/services/core/tools/e2b-provider/build.py index b9780ca3..8d6f9bad 100644 --- a/services/core/tools/e2b-provider/build.py +++ b/services/core/tools/e2b-provider/build.py @@ -55,8 +55,7 @@ def main(): report = json.loads(subprocess.check_output([str(exported / NAME), '--check'], text=True)) if report != {'Version': PROTOCOL_VERSION, 'SDKVersion': SDK_VERSION}: raise RuntimeError('Unexpected helper readiness report') - manifest = {'format_version': 1, 'source_revision': os.environ['E2B_SOURCE_REVISION'], - 'sdk_version': report['SDKVersion'], 'python_version': platform.python_version(), + manifest = {'format_version': 1, 'sdk_version': report['SDKVersion'], 'python_version': platform.python_version(), 'platform': 'linux-amd64', 'libc': platform.libc_ver(), 'build_image': BASE, 'entrypoint': NAME, 'source_sha256': {file.name: hashlib.sha256(file.read_bytes()).hexdigest() From c5ad302214ff32930c78ec175225f79b1e8ce4b0 Mon Sep 17 00:00:00 2001 From: yuanhe Date: Sat, 3 Oct 2026 13:08:48 +0800 Subject: [PATCH 4/4] Discover pnpm lockfiles in the cache warm workflow Listing tracked lockfiles keeps the warm in step with new packages and avoids naming the example application. --- .github/workflows/cache-warm.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cache-warm.yml b/.github/workflows/cache-warm.yml index bcec6b40..94646690 100644 --- a/.github/workflows/cache-warm.yml +++ b/.github/workflows/cache-warm.yml @@ -44,15 +44,9 @@ jobs: - uses: actions/checkout@v7 - uses: ./.github/actions/node with: - lockfiles: | - pnpm-lock.yaml - apps/web/pnpm-lock.yaml - packages/agents-client/pnpm-lock.yaml - packages/claude-sdk-adapter/pnpm-lock.yaml - website/pnpm-lock.yaml - example/parsar/pnpm-lock.yaml + lockfiles: '**/pnpm-lock.yaml' - name: Fetch every pnpm lockfile into the store - run: for dir in . apps/web packages/agents-client packages/claude-sdk-adapter website example/parsar; do pnpm --dir "$dir" fetch; done + run: git ls-files '*pnpm-lock.yaml' | while read -r lockfile; do pnpm --dir "$(dirname "$lockfile")" fetch; done - uses: ./.github/actions/e2b-provider - name: Build the E2B helper run: E2B_PROVIDER_BUILD_DIR="$RUNNER_TEMP/e2b-provider" bash scripts/build-e2b-provider.sh