Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This guide is for maintainers who build and publish OpenAgentCore. To install Co

## Build a distribution

A distribution is a matched set of release assets built from one commit: the control archive (the installer, the `oac` command, and the Core, Web, ingress and PostgreSQL images), the Runtime image and node artifacts as separate files, and the native installers.
A distribution is a matched set of build outputs from one commit: the control archive (the installer, the `oac` command, and the Core, Web, ingress and PostgreSQL images), the Runtime image and node artifacts as separate files, and the native installers.

Core, Web and ingress images are published as verified Linux amd64/arm64 indexes. The arm64 control archive contains those three images; Node, hosted Runtime and offline payloads use Linux amd64. Release builders use QEMU for ARM image steps, including the E2B helper. Host `oac` binaries are built from the same command for Linux amd64/arm64, macOS amd64/arm64 and Windows amd64; launchers only select, verify and invoke them. Each version index is checked against its platform archives before floating tags move.

Expand Down Expand Up @@ -143,6 +143,8 @@ The workflow runs `check` on the tagged commit, including the full local gate, o

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.

The control archives `oac-<commit>-linux-amd64.tar.gz` and `oac-<commit>-linux-arm64.tar.gz`, and their `.sha256` files, stay in the build output and the Actions recovery artifact; they are not uploaded to GitHub Releases, including manual drafts. The publisher still verifies them and reads their native catalog and image payloads. Online installers download the host binary and Compose files and pull images from GHCR; offline installs use the offline archive. Runtime, node and native installer assets remain separate downloads.

### Container registry

Version releases and manual `build-<full SHA>` drafts publish `ghcr.io/minimax-ai/openagentcore/<component>:<version>`, where `<component>` is `core`, `web`, `runtime`, `ingress` or `postgres`. Core, Web and ingress indexes contain Linux amd64 and arm64 images; Runtime contains Linux amd64. Platform images use `<version>-<architecture>` tags and are loaded from the release archives. Existing version tags must match the release images and platform set. The publisher verifies every version index before updating `latest` for a stable release; prereleases and drafts leave `latest` unchanged. `postgres` is the official PostgreSQL index pinned in `deploy/compose/database-image.txt`, copied with all its platforms and its digest; the publisher refuses a registry tag that names another digest. To move PostgreSQL, change that file to a new digest of the same major version, which is the one place the template, the offline bundle and the mirror read. SemVer build metadata uses `_` in place of `+` in container tags; version strings are limited to 128 characters. After verifying the images, the publisher uploads the release's `compose.yaml` and checksum list. Compose pins Core, Web, ingress and PostgreSQL by their release index digests, and initialization checks its build revision against the Compose revision.
Expand Down
6 changes: 4 additions & 2 deletions docs/zh/maintainers.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
title: "构建并发布 OpenAgentCore"
source: docs/maintainers.md
source_hash: f66f608093cc824add3215a9df520082b1679af1852ee213b7a6adfca40221ed
source_hash: 25fc91df22355fa1dcbd148586b57eeb84ec641ce882e82386d50c3d5a01ba1d
---

本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请按 [安装 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)。

## 构建分发包 {#build-a-distribution}

分发包是从同一个提交构建的一组相互匹配的发布资源:控制归档(安装程序、`oac` 命令,以及 Core、Web、ingress 和 PostgreSQL 镜像)、作为独立文件的 Runtime 镜像和节点构件,以及原生安装程序。
分发包是从同一个提交生成的一组相互匹配的构建输出:控制归档(安装程序、`oac` 命令,以及 Core、Web、ingress 和 PostgreSQL 镜像)、作为独立文件的 Runtime 镜像和节点构件,以及原生安装程序。

Core、Web 和 ingress 镜像发布为经过校验的 Linux amd64/arm64 多架构索引。arm64 控制归档包含这三个镜像;Node、托管 Runtime 和离线包使用 Linux amd64。发行构建使用 QEMU 执行 ARM 镜像步骤,包括 E2B helper。主机 `oac` 从同一份实现构建为 Linux amd64/arm64、macOS amd64/arm64 和 Windows amd64 二进制;启动脚本只选择、校验并运行它们。所有版本索引校验通过后才更新浮动标签。

Expand Down Expand Up @@ -145,6 +145,8 @@ flowchart LR

分发归档和 Runtime 归档使用 `pigz` 级别 6,最多使用四个压缩工作线程,并且 gzip 头部中不包含文件名或时间戳。发布器会验证归档和原生安装程序校验和、解析仓库身份、拒绝使用该标签已有的 Release 或草稿,并创建一个具有固定 ID 的草稿。最多四个资源可并发上传,按从大到小的顺序进行,且不会重试。确认完整的远程资源清单后,发布器会验证所有镜像归档和现有注册表标签,再并发推送最多四个镜像。每个镜像配置和注册表清单都会接受验证;任何错误都会使 Release 保持未发布状态。失败操作返回前,正在进行的传输会完成。发布器会按 ID 发布草稿,并在发布前重新检查版本标签。

控制归档 `oac-<commit>-linux-amd64.tar.gz` 和 `oac-<commit>-linux-arm64.tar.gz` 及其 `.sha256` 文件保留在构建输出和 Actions 恢复构件中,不上传到 GitHub Releases,手动草稿也一样。发布器仍会校验它们,并读取其中的原生安装目录和镜像内容。在线安装程序下载主机二进制与 Compose 文件,并从 GHCR 拉取镜像;离线安装使用离线归档。Runtime、节点和原生安装程序资源仍作为独立文件下载。

### 容器注册表 {#container-registry}

版本发布和手动 `build-<full SHA>` 草稿使用 `ghcr.io/minimax-ai/openagentcore/<component>:<version>`,其中 `<component>` 为 `core`、`web`、`runtime`、`ingress` 或 `postgres`。Core、Web 和 ingress 索引包含 Linux amd64 和 arm64 镜像,Runtime 包含 Linux amd64。各平台镜像使用 `<version>-<architecture>` 标签,从发行归档加载。已有版本标签必须与发行镜像及平台集合一致。发布器校验全部版本索引后,才为稳定版更新 `latest`;预发布版和草稿保持 `latest` 不变。`postgres` 是固定在 `deploy/compose/database-image.txt` 中的官方 PostgreSQL 索引,连同全部平台和原摘要一并复制;注册表中的标签若指向其他摘要,发布器会拒绝。要升级 PostgreSQL,请把该文件改为同一主版本的新摘要,模板、离线包和镜像复制都读取这一处。容器标签中的 SemVer 构建元数据用 `_` 替换 `+`,版本字符串上限为 128 个字符。镜像校验后,发布器上传该版本的 `compose.yaml` 和校验和清单。Compose 用对应发行版的索引摘要固定 Core、Web、ingress 和 PostgreSQL,初始化时检查其构建版本与 Compose 版本一致。
Expand Down
8 changes: 7 additions & 1 deletion scripts/publish-core-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ def publish(assets, repository, revision, tag, mode):
# The builder validates the manifest and Runtime assets. Verify archives again
# after the Actions artifact transfer between jobs.
stem = "oac-" + revision + "-linux-amd64"
archives = [assets / (stem + ".tar.gz"), assets / ("oac-" + revision + "-linux-arm64.tar.gz"), assets / "install.sh", assets / "install.ps1"]
control_archives = [assets / f"oac-{revision}-linux-{arch}.tar.gz" for arch in ("amd64", "arm64")]
archives = control_archives + [assets / "install.sh", assets / "install.ps1"]
archives.extend(assets / name for name in ("oac-linux-amd64", "oac-linux-arm64", "oac-darwin-amd64", "oac-darwin-arm64", "oac-windows-amd64.exe"))
if mode == "publish" or (assets / (stem + "-offline.tar.gz")).exists():
archives.append(assets / (stem + "-offline.tar.gz"))
Expand All @@ -276,6 +277,11 @@ def publish(assets, repository, revision, tag, mode):
or path.with_name(path.name + ".sha256").read_text() != entry["sha256"] + " " + path.name + "\n"):
raise ValueError("Native installer checksum mismatch")

# Keep control archives for catalog verification, image publication and recovery,
# but installers do not download them from Releases.
internal_files = {path.name + suffix for path in control_archives for suffix in ("", ".sha256")}
files = [path for path in files if path.name not in internal_files]

# Actions can retain the old repository name after a rename. Resolve it
# before any writes; binary uploads must not depend on POST redirects.
canonical = api(repository, "https://api.github.com/repos/" + repository).get("full_name")
Expand Down
64 changes: 57 additions & 7 deletions scripts/publish-core-release.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,57 @@ def test_draft_publishes_images_and_stays_unpublished(self):
self.publish(tag="build-" + self.revision, mode="draft")
self.images.assert_called_once()
self.assertTrue(self.release["draft"])
self.assertEqual(len(self.release["assets"]), 28)
self.assertEqual(len(self.release["assets"]), 24)

def test_missing_native_asset_refuses_release_creation(self):
(self.assets / f"oac-native-{self.revision}-windows-amd64-codex.tar.gz").unlink()
with self.assertRaises(FileNotFoundError):
self.publish()
self.assertEqual(self.writes(), [])

def test_control_archives_stay_local_for_images_and_recovery(self):
before = {p.name: publisher.distribution.sha256(p) for p in self.assets.iterdir()}
internal = {f"oac-{self.revision}-linux-{arch}.tar.gz{suffix}"
for arch in ("amd64", "arm64") for suffix in ("", ".sha256")}
# Execution payloads and their checksums must still be downloadable.
for suffix in ("runtime.tar.gz", "sandbox-node"):
path = self.assets / (self.stem + "-" + suffix)
path.write_bytes(b"node payload")
path.with_name(path.name + ".sha256").write_text(
publisher.distribution.sha256(path) + " " + path.name + "\n")
expected = {p.name for p in self.assets.iterdir()} - internal
images = self.images.return_value
def publish_images(assets, repository, revision, tag, **kwargs):
self.assertEqual(assets, self.assets)
self.assertEqual(revision, self.revision)
for name, checksum in before.items():
self.assertEqual(publisher.distribution.sha256(assets / name), checksum)
return images
self.images.side_effect = publish_images
for mode, tag in (("publish", "v1.2.3"), ("draft", "build-" + self.revision)):
with self.subTest(mode=mode):
for name in ("compose.yaml", "compose-sha256sums.txt"):
(self.assets / name).unlink(missing_ok=True)
self.publish(tag=tag, mode=mode)
self.assertEqual({a["name"] for a in self.release["assets"]},
expected | {"compose.yaml", "compose-sha256sums.txt"})
for name, checksum in before.items():
self.assertEqual(publisher.distribution.sha256(self.assets / name), checksum)

def test_missing_control_inputs_refuse_release_creation(self):
for arch in ("amd64", "arm64"):
for suffix in ("", ".sha256"):
path = self.assets / f"oac-{self.revision}-linux-{arch}.tar.gz{suffix}"
with self.subTest(name=path.name):
original = path.read_bytes()
path.unlink()
try:
with self.assertRaises(FileNotFoundError):
self.publish()
self.assertEqual(self.writes(), [])
finally:
path.write_bytes(original)

def test_corrupt_native_asset_refuses_release_creation(self):
(self.assets / f"oac-native-{self.revision}-linux-amd64.tar.gz").write_bytes(b"corrupt")
with self.assertRaisesRegex(ValueError, "Native installer checksum"):
Expand Down Expand Up @@ -142,7 +185,7 @@ def response(repo, endpoint, *args):
return result
if endpoint == "releases/7":
self.assertEqual(active, 0)
self.assertIn(len(self.release["assets"]), (26, 28))
self.assertIn(len(self.release["assets"]), (22, 24))
return self.response(repo, endpoint, *args)
self.api.side_effect = response
self.publish()
Expand All @@ -153,7 +196,7 @@ def test_version_tag_publishes_complete_fixed_id(self):
self.publish()
self.assertFalse(self.release["draft"])
self.assertFalse(self.release["prerelease"])
self.assertEqual(len(self.release["assets"]), 28)
self.assertEqual(len(self.release["assets"]), 24)
self.assertEqual({a["name"] for a in self.release["assets"] if a["name"].endswith(".yaml")}, {"compose.yaml"})
self.assertEqual(self.api.call_args.args[1:],
("releases/7", "--method", "PATCH", "-F", "draft=false"))
Expand Down Expand Up @@ -305,10 +348,17 @@ def test_missing_offline_archive_is_refused(self):
self.assertEqual(self.writes(), [])

def test_changed_archive_is_refused(self):
(self.assets / (self.stem + ".tar.gz")).write_bytes(b"changed")
with self.assertRaisesRegex(ValueError, "checksum"):
self.publish()
self.assertEqual(self.writes(), [])
for arch in ("amd64", "arm64"):
path = self.assets / f"oac-{self.revision}-linux-{arch}.tar.gz"
with self.subTest(arch=arch):
original = path.read_bytes()
path.write_bytes(b"changed")
try:
with self.assertRaisesRegex(ValueError, "checksum"):
self.publish()
self.assertEqual(self.writes(), [])
finally:
path.write_bytes(original)

def test_empty_or_linked_payload_is_refused(self):
for p in self.assets.iterdir():
Expand Down