Skip to content
Merged
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
10 changes: 5 additions & 5 deletions deploy/compose/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Release template. scripts/render-compose.py fills the __OAC_*__ tokens with this
# release's source revision and node-metadata checksum. Images default to the
# floating latest tags; set OAC_IMAGE_CORE, OAC_IMAGE_WEB or OAC_IMAGE_INGRESS
# release's source revision, node-metadata checksum, and the image repository and
# tag the release published. Set OAC_IMAGE_CORE, OAC_IMAGE_WEB or OAC_IMAGE_INGRESS
# to select another reference. Do not run this file until it has been rendered.
# Data is bind-mounted from ${OAC_DATA_DIR:-./data}. Set OAC_PUBLIC_URL when the
# platform domain is ready; startup defaults to localhost. Other process
# settings pass through unchanged; Core owns their defaults.
x-ingress-image: &ingress-image ${OAC_IMAGE_INGRESS:-ghcr.io/minimax-ai/openagentcore/ingress:latest}
x-ingress-image: &ingress-image ${OAC_IMAGE_INGRESS:-__OAC_IMAGE_REPOSITORY__/ingress:__OAC_IMAGE_TAG__}
services:
init:
image: *ingress-image
Expand Down Expand Up @@ -47,7 +47,7 @@ services:
retries: 30

core:
image: ${OAC_IMAGE_CORE:-ghcr.io/minimax-ai/openagentcore/core:latest}
image: ${OAC_IMAGE_CORE:-__OAC_IMAGE_REPOSITORY__/core:__OAC_IMAGE_TAG__}
platform: linux/amd64
user: "65532:65532"
restart: unless-stopped
Expand Down Expand Up @@ -90,7 +90,7 @@ services:
target: /state

web:
image: ${OAC_IMAGE_WEB:-ghcr.io/minimax-ai/openagentcore/web:latest}
image: ${OAC_IMAGE_WEB:-__OAC_IMAGE_REPOSITORY__/web:__OAC_IMAGE_TAG__}
platform: linux/amd64
user: "65532:65532"
restart: unless-stopped
Expand Down
34 changes: 34 additions & 0 deletions deploy/compose/test_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,39 @@ def test_platform_network_injection_keeps_the_file_valid(self):
input=json.dumps(transformed), text=True, check=True)


class RenderImageReferenceTests(unittest.TestCase):
"""Rendered image defaults must match the repository and tag a release published."""

values = {
'REVISION': 'd' * 40,
'RELEASE_BASE': 'https://example.com/releases/v1/',
'ARCHIVE_CHECKSUM': 'e' * 64,
}

def rendered(self, **extra):
return render_compose.render({**self.values, **extra})

def test_defaults_keep_the_upstream_latest_images(self):
text = self.rendered()
for name in ('core', 'web', 'ingress'):
self.assertIn('${OAC_IMAGE_' + name.upper() + ':-ghcr.io/minimax-ai/openagentcore/' + name + ':latest}', text)

def test_fork_repository_and_release_tag_replace_the_defaults(self):
tag = 'build-' + 'a' * 40
text = self.rendered(IMAGE_REPOSITORY='ghcr.io/sunyalou/openagentcore', IMAGE_TAG=tag)
for name in ('core', 'web', 'ingress'):
self.assertIn('${OAC_IMAGE_' + name.upper() + ':-ghcr.io/sunyalou/openagentcore/' + name + ':' + tag + '}', text)

def test_invalid_image_repository_or_tag_is_refused(self):
for repository in ('', 'ghcr.io', 'ghcr.io/Fork/Repo', 'ghcr.io/fork/repo:tag'):
with self.subTest(repository=repository):
with self.assertRaisesRegex(ValueError, 'IMAGE_REPOSITORY'):
self.rendered(IMAGE_REPOSITORY=repository)
for tag in ('', ':bad', 'has space', 'a' * 129):
with self.subTest(tag=tag):
with self.assertRaisesRegex(ValueError, 'IMAGE_TAG'):
self.rendered(IMAGE_TAG=tag)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion docs/getting-started/install-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ With the one-line command, append them after `bash -s --`. `--version TAG` selec

## Docker Compose and hosting platforms

Use the `compose.yaml` from a release with Docker Compose 2.26 or newer on Linux amd64. The release renders node metadata into the [Compose template](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml). Core and Web use the `latest` images, and PostgreSQL uses `postgres:16-alpine`. It starts PostgreSQL, Core and Web. Web forwards `/v1` and `/api/v1` to Core. Data is bind-mounted from a directory. The one-time initialization service generates random secrets there and prepares the node installer; Core applies database migrations when it starts. [Compose configuration](../configuration.md#compose-installations) owns the settings and the data directory.
Use the `compose.yaml` from a release with Docker Compose 2.26 or newer on Linux amd64. The release renders node metadata into the [Compose template](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml). Core and Web use images from the release repository at the tag that release published, which is `latest` for a stable release, and PostgreSQL uses `postgres:16-alpine`. It starts PostgreSQL, Core and Web. Web forwards `/v1` and `/api/v1` to Core. Data is bind-mounted from a directory. The one-time initialization service generates random secrets there and prepares the node installer; Core applies database migrations when it starts. [Compose configuration](../configuration.md#compose-installations) owns the settings and the data directory.

For a local trial, download `compose.yaml` and `ports.yaml` from the same release into one directory, then run:

Expand Down
2 changes: 1 addition & 1 deletion docs/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Distribution and Runtime archives use `pigz` level 6 with at most four compressi

### Container registry

Version releases and manual `build-<full SHA>` drafts publish Linux amd64 images as `ghcr.io/minimax-ai/openagentcore/<component>:<version>`, where `<component>` is `core`, `web`, `runtime` or `ingress`. For example, `ghcr.io/minimax-ai/openagentcore/core:v1.2.3`. A draft uses the tag `build-<full SHA>`. PostgreSQL uses its upstream image and is not republished. The registry images are loaded from the release archives without rebuilding. Existing version tags are reused only when their image config digest matches the release; a different image stops publication. A stable release also moves each component's `latest` tag to that image. Prereleases and drafts leave `latest` unchanged. SemVer build metadata uses `_` in place of `+` in container tags; version strings longer than 128 characters cannot be published to GHCR. After the images are verified, the publisher uploads `compose.yaml` and `ports.yaml`, with checksums, rendered for that release. A draft Release stays unpublished.
Version releases and manual `build-<full SHA>` drafts publish Linux amd64 images to GHCR under the release repository as `ghcr.io/<owner>/<repository>/<component>:<version>`, where `<component>` is `core`, `web`, `runtime` or `ingress`. For example, the upstream repository publishes `ghcr.io/minimax-ai/openagentcore/core:v1.2.3`. A draft uses the tag `build-<full SHA>`. PostgreSQL uses its upstream image and is not republished. The registry images are loaded from the release archives without rebuilding. Existing version tags are reused only when their image config digest matches the release; a different image stops publication. A stable release also moves each component's `latest` tag to that image. Prereleases and drafts leave `latest` unchanged. SemVer build metadata uses `_` in place of `+` in container tags; version strings longer than 128 characters cannot be published to GHCR. After the images are verified, the publisher uploads `compose.yaml` and `ports.yaml`, with checksums, rendered for that release; each `OAC_IMAGE_{CORE,WEB,INGRESS}` default names that release's repository and the tag it published, so a fork install needs no image override. A draft Release stays unpublished.

The combined build/publication job uses `GITHUB_TOKEN` with `packages: write`. On the first publication, GitHub creates each container package as private: a package administrator must change all four packages to **Public** in their package settings before users can pull anonymously. See [GitHub container visibility](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry). Verify an unauthenticated pull after changing visibility. Repository visibility alone does not make a new container package public.

Expand Down
4 changes: 2 additions & 2 deletions docs/zh/getting-started/install-options.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "安装选项与高级部署"
source: docs/getting-started/install-options.md
source_hash: 53468efe866d0774ec60d015eb6c168dd5ae5170f6b7af0c1505a63e32362d11
source_hash: 4aafadb9c477e3c7518920fae3a1d75b95fd00c8e430fccf59cea3cb1515919e
---

[默认安装](install.md)无需任何选项。使用本页可以在现有反向代理后运行,或者在无法访问互联网时进行安装。
Expand All @@ -18,7 +18,7 @@ source_hash: 53468efe866d0774ec60d015eb6c168dd5ae5170f6b7af0c1505a63e32362d11

## Docker Compose 与托管平台 {#docker-compose-and-hosting-platforms}

在 Linux amd64 上使用发行版中的 `compose.yaml` 和 Docker Compose 2.26 或更高版本。发行流程会把节点元数据渲染进 [Compose 模板](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml)。Core 和 Web 使用 `latest` 镜像,PostgreSQL 使用 `postgres:16-alpine`。它会启动 PostgreSQL、Core 和 Web。Web 把 `/v1` 和 `/api/v1` 转发到 Core。数据通过目录 bind mount 挂载。一次性初始化服务会在该目录中生成随机机密信息并准备节点安装程序;Core 启动时执行数据库迁移。[Compose 配置](../configuration.md#compose-installations)负责管理各项设置和数据目录。
在 Linux amd64 上使用发行版中的 `compose.yaml` 和 Docker Compose 2.26 或更高版本。发行流程会把节点元数据渲染进 [Compose 模板](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/compose/compose.yaml)。Core 和 Web 使用该 release 所属仓库发布的 tag 镜像(稳定版为 `latest`),PostgreSQL 使用 `postgres:16-alpine`。它会启动 PostgreSQL、Core 和 Web。Web 把 `/v1` 和 `/api/v1` 转发到 Core。数据通过目录 bind mount 挂载。一次性初始化服务会在该目录中生成随机机密信息并准备节点安装程序;Core 启动时执行数据库迁移。[Compose 配置](../configuration.md#compose-installations)负责管理各项设置和数据目录。

进行本地试用时,请将同一发行版的 `compose.yaml` 和 `ports.yaml` 下载到同一个目录,然后运行:

Expand Down
4 changes: 2 additions & 2 deletions docs/zh/maintainers.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "构建并发布 OpenAgentCore"
source: docs/maintainers.md
source_hash: ac744d8df2682f0c19eb6b05c1ef50a9e7c7a9d214316317458669f4cb00f477
source_hash: adf81c3f9d3e7c00e941c9756def0a22f93a866a23ed287fbfd7da85caa4b348
---

本指南面向负责构建和发布 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)。
Expand Down Expand Up @@ -132,7 +132,7 @@ git push origin v1.2.3

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

版本发布和手动的 `build-<full SHA>` 草稿都会将 Linux amd64 镜像发布为 `ghcr.io/minimax-ai/openagentcore/<component>:<version>`,其中 `<component>` 为 `core`、`web`、`runtime` 或 `ingress`。例如,`ghcr.io/minimax-ai/openagentcore/core:v1.2.3`。草稿使用标签 `build-<full SHA>`。PostgreSQL 使用其上游镜像,不会重新发布。注册表镜像从发布归档中加载,不会重新构建。仅当现有版本标签的镜像配置摘要与本次发布相同时才复用该标签;如果镜像不同,则停止发布。稳定版还会把每个组件的 `latest` 标签移到该镜像。预发布和草稿不会改动 `latest`。SemVer 构建元数据在容器标签中使用 `_` 代替 `+`;长度超过 128 个字符的版本字符串无法发布到 GHCR。镜像验证之后,发布器会上传为该发行版渲染的 `compose.yaml` 和 `ports.yaml` 及其校验和。草稿 Release 保持未发布。
版本发布和手动的 `build-<full SHA>` 草稿都会将 Linux amd64 镜像发布到发布仓库在 GHCR 下的命名空间:`ghcr.io/<owner>/<repository>/<component>:<version>`,其中 `<component>` 为 `core`、`web`、`runtime` 或 `ingress`。例如上游仓库会发布 `ghcr.io/minimax-ai/openagentcore/core:v1.2.3`。草稿使用标签 `build-<full SHA>`。PostgreSQL 使用其上游镜像,不会重新发布。注册表镜像从发布归档中加载,不会重新构建。仅当现有版本标签的镜像配置摘要与本次发布相同时才复用该标签;如果镜像不同,则停止发布。稳定版还会把每个组件的 `latest` 标签移到该镜像。预发布和草稿不会改动 `latest`。SemVer 构建元数据在容器标签中使用 `_` 代替 `+`;长度超过 128 个字符的版本字符串无法发布到 GHCR。镜像验证之后,发布器会上传为该发行版渲染的 `compose.yaml` 和 `ports.yaml` 及其校验和;每个 `OAC_IMAGE_{CORE,WEB,INGRESS}` 默认值都指向该发行版所属仓库及其发布的标签,因此 fork 安装无需覆盖镜像。草稿 Release 保持未发布。

合并的构建/发布作业使用具有 `packages: write` 权限的 `GITHUB_TOKEN`。首次发布时,GitHub 会将每个容器软件包创建为私有:软件包管理员必须先在各自的软件包设置中将全部四个软件包改为 **Public**,用户才能匿名拉取。请参阅 [GitHub container visibility](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)。更改可见性后,请验证未认证拉取。仅更改仓库可见性并不会使新的容器软件包变为公开。

Expand Down
6 changes: 6 additions & 0 deletions scripts/publish-core-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,16 @@ def upload(path):
raise ValueError("Release asset inventory differs from the build")
stable = re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+(?:\+[0-9A-Za-z.-]+)?", tag) is not None
images = publish_images(assets, repository, revision, tag, floating_latest=mode == "publish" and stable)
# A stable publication also moves each component's `latest` tag, which keeps the
# rendered default that upstream always shipped. Every other release renders the
# exact tag it published (for example a manual `build-<full SHA>` draft).
image_tag = "latest" if mode == "publish" and stable else tag.replace("+", "_")
compose_files = render_compose.write_assets(assets, {
"REVISION": revision,
"RELEASE_BASE": "https://github.com/" + repository + "/releases/download/" + tag + "/",
"ARCHIVE_CHECKSUM": distribution.sha256(assets / (stem + ".tar.gz")),
"IMAGE_REPOSITORY": "ghcr.io/" + repository.lower(),
"IMAGE_TAG": image_tag,
})
expected.update({path.name: path.stat().st_size for path in compose_files})
parallel_each(upload, compose_files)
Expand Down
23 changes: 23 additions & 0 deletions scripts/publish-core-release.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,29 @@ def test_manual_draft_does_not_publish(self):
self.assertTrue(self.release["draft"])
self.assertFalse(any(c.args[1].startswith("git/") for c in self.api.call_args_list))

def test_rendered_compose_uses_the_release_repository_and_tag(self):
self.canonical_repository = "sunyalou/OpenAgentCore"
self.publish(tag="build-" + self.revision, mode="draft")
text = (self.assets / "compose.yaml").read_text()
for name in ("core", "web", "ingress"):
self.assertIn("ghcr.io/sunyalou/openagentcore/" + name + ":build-" + self.revision, text)

def test_stable_release_renders_the_upstream_latest_default(self):
self.publish()
text = (self.assets / "compose.yaml").read_text()
for name in ("core", "web", "ingress"):
self.assertIn("ghcr.io/minimax-ai/openagentcore/" + name + ":latest", text)

def test_prerelease_release_renders_the_tag_it_publishes(self):
self.publish("v1.2.3-rc.1")
self.images.assert_called_once()
self.assertEqual(self.images.call_args.args[3], "v1.2.3-rc.1")
self.assertFalse(self.images.call_args.kwargs["floating_latest"])
text = (self.assets / "compose.yaml").read_text()
for name in ("core", "web", "ingress"):
self.assertIn("ghcr.io/minimax-ai/openagentcore/" + name + ":v1.2.3-rc.1", text)
self.assertNotIn(":latest", text)

def test_existing_public_or_draft_release_is_refused(self):
for draft in (True, False):
with self.subTest(draft=draft):
Expand Down
23 changes: 19 additions & 4 deletions scripts/render-compose.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#!/usr/bin/env python3
"""Fill the Compose template with one release's node metadata.

The template is deploy/compose/compose.yaml. Images stay on their default
latest tags. A release publishes the rendered file; this script does not run Docker.
The template is deploy/compose/compose.yaml. The rendered image references default
to the release repository and tag; callers that omit IMAGE_REPOSITORY and
IMAGE_TAG keep the upstream latest tags. A release publishes the rendered file;
this script does not run Docker.
"""
import hashlib
import pathlib
Expand All @@ -13,6 +15,10 @@
TEMPLATE = ROOT / "deploy/compose/compose.yaml"
PORTS = ROOT / "deploy/compose/ports.yaml"
TOKENS = ("REVISION", "RELEASE_BASE", "ARCHIVE_CHECKSUM")
DEFAULT_IMAGE_REPOSITORY = "ghcr.io/minimax-ai/openagentcore"
DEFAULT_IMAGE_TAG = "latest"
IMAGE_REPOSITORY = re.compile(r"[a-z0-9]+(?:[.-][a-z0-9]+)*(?::[0-9]+)?(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)+")
IMAGE_TAG = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}")


def render(values):
Expand All @@ -27,12 +33,21 @@ def render(values):
base = values["RELEASE_BASE"]
if not base.startswith("https://") or not base.endswith("/") or " " in base:
raise ValueError("RELEASE_BASE must be an https URL ending with /")
repository = values.get("IMAGE_REPOSITORY", DEFAULT_IMAGE_REPOSITORY)
tag = values.get("IMAGE_TAG", DEFAULT_IMAGE_TAG)
if not IMAGE_REPOSITORY.fullmatch(repository):
raise ValueError("IMAGE_REPOSITORY must be a lowercase registry repository")
if not IMAGE_TAG.fullmatch(tag):
raise ValueError("IMAGE_TAG must be a container tag")
replacements = {name: values[name] for name in TOKENS}
replacements["IMAGE_REPOSITORY"] = repository
replacements["IMAGE_TAG"] = tag
text = TEMPLATE.read_text()
for name in TOKENS:
for name, value in replacements.items():
token = "__OAC_" + name + "__"
if token not in text:
raise ValueError("Compose template is missing " + token)
text = text.replace(token, values[name])
text = text.replace(token, value)
leftover = sorted(set(re.findall(r"__OAC_[A-Z_]+__", text)))
if leftover:
raise ValueError("Unreplaced Compose tokens: " + ", ".join(leftover))
Expand Down
Loading