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
2 changes: 2 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ jobs:
node-version: '22'
- name: Verify Harness catalog and installer schema
run: make check-harness-catalog
- name: Install distribution compressor
run: sudo apt-get update && sudo apt-get install -y pigz
- name: Test distribution, installer and console packaging
run: make check-distribution

Expand Down
42 changes: 18 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ jobs:

build:
needs: check
runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && 'ubuntu-22.04' || 'blacksmith-2vcpu-ubuntu-2204' }}
runs-on: ubuntu-22.04
timeout-minutes: 120
outputs:
revision: ${{ steps.source.outputs.revision }}
release_tag: ${{ steps.source.outputs.release_tag }}
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -88,7 +88,16 @@ jobs:
- name: Install build prerequisites
run: |
sudo apt-get update
sudo apt-get install -y build-essential pkg-config libssl-dev
sudo apt-get install -y build-essential pkg-config libssl-dev pigz
- name: Cache pinned release downloads
uses: actions/cache@v6
with:
path: |
~/.npm/_cacache
~/.oac/cache/microsandbox-v0.7.2-linux-x86_64.tar.gz
key: release-downloads-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('scripts/prepare-release-runtimes.sh', 'packages/mcode-harness/source.json', 'scripts/core-distribution-manifest.py') }}
restore-keys: |
release-downloads-v1-${{ runner.os }}-${{ runner.arch }}-
- name: Check release metadata and prepare pinned harnesses
run: |
PYTHONDONTWRITEBYTECODE=1 python3 scripts/core-distribution-manifest.test.py
Expand Down Expand Up @@ -127,22 +136,6 @@ jobs:
compression-level: 0
if-no-files-found: error

release:
if: github.event_name == 'push' || inputs.draft_release
needs: [check, build]
runs-on: ${{ vars.OAC_USE_GITHUB_RUNNERS == 'true' && 'ubuntu-22.04' || 'blacksmith-2vcpu-ubuntu-2204' }}
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.build.outputs.revision }}
persist-credentials: false
- uses: actions/download-artifact@v6
with:
name: core-release-${{ needs.build.outputs.revision }}
path: release-upload
- name: Sign in to GHCR for version releases
if: github.event_name == 'push'
env:
Expand All @@ -152,13 +145,14 @@ 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
if: github.event_name == 'push' || inputs.draft_release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_REVISION: ${{ needs.build.outputs.revision }}
RELEASE_TAG: ${{ needs.build.outputs.release_tag }}
RELEASE_REVISION: ${{ steps.source.outputs.revision }}
RELEASE_TAG: ${{ steps.source.outputs.release_tag }}
RELEASE_MODE: ${{ github.event_name == 'push' && 'publish' || 'draft' }}
run: python3 scripts/publish-core-release.py --assets release-upload
run: python3 scripts/publish-core-release.py --assets "$HOME/.oac/build/release-upload"
- name: Remove registry credentials
if: always() && github.event_name == 'push'
run: rm -f "$RUNNER_TEMP/oac-release-docker/config.json"
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ check-sandbox-provider-contract:

.PHONY: check-docs check-ci
check-docs:
PYTHONDONTWRITEBYTECODE=1 python3 scripts/core-distribution-manifest.test.py
PYTHONDONTWRITEBYTECODE=1 python3 scripts/core-distribution-manifest.test.py BundledDocsTests

check-ci:
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s scripts -p 'ci_*test.py'
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ git worktree add ../openagentcore-change -b codex/my-change main
cd ../openagentcore-change
```

Install Go at the version in [go.mod](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/go.mod), Node 22.13 or newer, pnpm at the version in [package.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/package.json), and Python 3.9 or newer. The complete gate runs on Linux and needs a dedicated PostgreSQL database, OpenSSL development libraries for the microsandbox helper, and a Playwright browser. Provider and Runtime builds have additional prerequisites in their component guides.
Install Go at the version in [go.mod](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/go.mod), Node 22.13 or newer, pnpm at the version in [package.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/package.json), and Python 3.9 or newer. The complete gate runs on Linux and needs a dedicated PostgreSQL database, OpenSSL development libraries for the microsandbox helper, pigz for distribution compression, and a Playwright browser. Provider and Runtime builds have additional prerequisites in their component guides.

```sh
pnpm install --frozen-lockfile
Expand Down
16 changes: 9 additions & 7 deletions docs/maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This guide is for maintainers who build and publish OpenAgentCore. To install Co

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

Build on Linux x86_64 with a glibc compatible with Debian 12, Docker, the Go version in `go.mod`, a C compiler (the microsandbox helper is a CGO build), Node, pnpm, Python 3.9 or newer, curl, tar and sha256sum. The source must be clean and committed. First prepare the pinned Codex package and MiniMax Code companion, then build:
Build on Linux x86_64 with a glibc compatible with Debian 12, Docker, the Go version in `go.mod`, a C compiler (the microsandbox helper is a CGO build), Node, pnpm, Python 3.9 or newer, curl, tar, pigz and sha256sum. The source must be clean and committed. First prepare the pinned Codex package and MiniMax Code companion, then build:

```sh
bash scripts/prepare-release-runtimes.sh
Expand Down Expand Up @@ -124,21 +124,23 @@ 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. `build` starts after `check` succeeds and reuses those native artifacts. `build` prepares the pinned Runtime inputs, assembles the native catalog and builds the distribution with the offline archive, and adds `deploy/install-release.sh` as `install.sh` with its checksum. The `release` job runs only after `check` and `build` succeed. It is the only job with `contents: write`. It verifies the archive checksums and the native installer checksums against the catalog, resolves the repository's current name from GitHub before any write (Actions can keep an old name after a rename), refuses an existing Release or draft for the tag, uploads everything to a new draft on `uploads.github.com` bound to that draft's ID without retrying failed uploads, confirms the tag still points at the built commit, and publishes that draft by its ID. Before publishing the draft, it also loads the same release image archives and pushes the Core, Web, Runtime and ingress images to GHCR, verifies their image config digests and records their registry manifest references in the Actions job summary. A registry failure leaves the Release as a draft. Archive downloads remain anonymous.
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.

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.

### Container registry

Version releases 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`. PostgreSQL uses its upstream image and is not republished. The registry images are loaded from the release archives without rebuilding. Existing tags are reused only when their image config digest matches the release; a different image stops publication. No floating `latest` tag is published. SemVer build metadata uses `_` in place of `+` in container tags; version strings longer than 128 characters cannot be published to GHCR. Manual draft builds do not push images.

The release 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.
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.

GHCR and GitHub Releases do not share a transaction. A failed release may leave some matching version tags in GHCR; preserve those images and follow the draft recovery procedure below using the original artifacts. Registry failures other than a missing manifest stop publication. The job summary records digest-pinned references; the installation archives and their checksums remain unchanged. These images still require the configuration, secrets and routing described in [Configuration](./configuration.md); publishing them does not provide a platform deployment template.

`install.sh` resolves the latest stable release once, or the release named by `--version`, verifies the control archive and runs that bundle's installer; the [installation guide](./getting-started/install.md#install) covers its use.

Go check and build jobs share Go module and compiler-cache directories under `~/.oac/cache/`, keyed by runner OS and architecture, all Go module files, the check/build partition and the commit. Partitioned keys prevent concurrent jobs from saving different compiler subsets under one key. Release builds can seed their cache from backend checks as well as earlier release builds. An older cache only seeds downloads and compilation; every check still runs. New keys are saved only after a successful job.
Go check and build jobs share Go module and compiler-cache directories under `~/.oac/cache/`, keyed by runner OS and architecture, all Go module files, the check/build partition and the commit. Partitioned keys prevent concurrent jobs from saving different compiler subsets under one key. Release builds can seed their cache from backend checks as well as earlier release builds. An older cache only seeds downloads and compilation; every check still runs. Release jobs also cache npm package downloads and the pinned microsandbox archive, whose checksum is verified on every build. Actions cache visibility follows GitHub ref scoping; a tag-specific cache is not shared with other release tags. New keys are saved only after a successful job.

Never move a release tag or overwrite published assets. If the `release` job fails, inspect the Release first: publication may have completed despite a lost response. Leave a complete published Release as it is. For an incomplete draft, delete that draft (the job refuses any existing Release or draft for the tag), then rerun the failed `release` job, which reuses the original Actions artifact. Do not rerun the build or recreate the tag to recover a failed upload.
Never move a release tag or overwrite published assets. If publication fails, inspect the Release first: publication may have completed despite a lost response. Leave a complete published Release as it is. For an incomplete draft, delete that draft only after inspection, download the original `core-release-<revision>` Actions artifact with `gh run download RUN_ID --name core-release-REVISION --dir ASSET_DIRECTORY`, and use a checkout of that exact source revision to run `python3 scripts/publish-core-release.py --assets ASSET_DIRECTORY`. Set `GH_REPO`, `GH_TOKEN`, `RELEASE_REVISION`, `RELEASE_TAG` and `RELEASE_MODE` to the original publication inputs and sign Docker into GHCR for version publication. The script revalidates the assets and refuses existing releases. Do not rerun the combined build job or recreate the tag to recover a failed upload.

### Build a candidate without publishing

Expand Down Expand Up @@ -196,9 +198,9 @@ Measure completed runs with `python3 scripts/ci_metrics.py RUN_ID ...`. It repor

### CI runners and free allowance

Linux jobs use Blacksmith's 2-vCPU Ubuntu 22.04 or 24.04 runners; native Windows uses its 2-vCPU Windows 2025 runner. Blacksmith has no 2-vCPU macOS runner, so native macOS uses the standard GitHub `macos-15` ARM64 runner. Release building and publication also use 2-vCPU Blacksmith runners.
Linux jobs use Blacksmith's 2-vCPU Ubuntu 22.04 or 24.04 runners; native Windows uses its 2-vCPU Windows 2025 runner. Blacksmith has no 2-vCPU macOS runner, so native macOS uses the standard GitHub `macos-15` ARM64 runner. Release building and publication always share one GitHub-hosted `ubuntu-22.04` runner, independent of the runner switch.

Set the repository Actions variable `OAC_USE_GITHUB_RUNNERS` to `true` to run all jobs on standard GitHub-hosted runners instead. Linux keeps its matching Ubuntu version, Windows uses `windows-2025`, and macOS continues using `macos-15`. Remove the variable or set it to `false` to return to Blacksmith's 2-vCPU defaults. For example, maintainers can switch when the organization's free allowance is used up, then restore Blacksmith after the allowance resets:
Set the repository Actions variable `OAC_USE_GITHUB_RUNNERS` to `true` to run all jobs on standard GitHub-hosted runners instead. Linux keeps its matching Ubuntu version, Windows uses `windows-2025`, and macOS continues using `macos-15`. Remove the variable or set it to `false` to return switchable check jobs to Blacksmith's 2-vCPU defaults. For example, maintainers can switch when the organization's free allowance is used up, then restore Blacksmith after the allowance resets:

```sh
gh variable set OAC_USE_GITHUB_RUNNERS --body true --repo MiniMax-AI/OpenAgentCore
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-core-distribution.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ if [[ "$(uname -s)" != Linux || "$(uname -m)" != x86_64 ]]; then
printf 'Build the distribution on Linux x86_64 with a glibc compatible with Debian 12\n' >&2
exit 1
fi
for command in docker go node pnpm python3 curl tar sha256sum; do
for command in docker go node pnpm python3 curl tar sha256sum pigz; do
command -v "$command" >/dev/null
done
build_network="${CORE_DISTRIBUTION_BUILD_NETWORK:-default}"
Expand Down
25 changes: 19 additions & 6 deletions scripts/core-distribution-manifest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Verify matched distribution inputs and package independently fetched artifacts."""

import gzip
from contextlib import contextmanager
import hashlib
import json
import os
Expand Down Expand Up @@ -253,16 +253,29 @@ def native_offline(bundle, stage):
os.link(source, path.parent / (platform + ".tar.gz"))


@contextmanager
def compressed_output(path):
"""Stream deterministic gzip with bounded parallel compression."""
command = ["pigz", "-n", "-6", "-p", str(min(4, os.cpu_count() or 1))]
with pathlib.Path(path).open("wb") as raw:
with subprocess.Popen(command, stdin=subprocess.PIPE, stdout=raw) as compressor:
try:
yield compressor.stdin
finally:
compressor.stdin.close()
if compressor.wait():
raise subprocess.CalledProcessError(compressor.returncode, command)


def package_artifacts(bundle, stage, revision):
"""Move optional payload out of Core; the manifest owns every asset digest."""
assets = stage / "artifacts"
assets.mkdir()
runtime = bundle / "images/runtime.tar"
compressed = bundle / "images/runtime.tar.gz"
unpacked = {"unpacked_sha256": sha256(runtime), "unpacked_size": runtime.stat().st_size}
with runtime.open("rb") as source, compressed.open("wb") as raw:
with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0, compresslevel=6) as output:
shutil.copyfileobj(source, output, 1024 * 1024)
with runtime.open("rb") as source, compressed_output(compressed) as output:
shutil.copyfileobj(source, output, 1024 * 1024)
runtime.unlink()
result = {}
for logical, suffix in ARTIFACTS.items():
Expand Down Expand Up @@ -355,8 +368,8 @@ def archive(bundle, epoch, variant=""):
if variant not in ("", "offline"):
raise ValueError("Unknown distribution archive variant")
output = bundle.with_name(bundle.name + ("-" + variant if variant else "") + ".tar.gz")
with output.open("wb") as raw, gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed:
with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as tar:
with compressed_output(output) as compressed:
with tarfile.open(fileobj=compressed, mode="w|", format=tarfile.PAX_FORMAT) as tar:
for path in sorted(bundle.rglob("*")):
if not path.is_file():
continue
Expand Down
Loading
Loading