Rewrite Agent CI around a durable state machine #4

Merged
StanPonomarev merged 4 commits from chore/rewrite into main 2026-07-25 12:36:32 +02:00
5 changed files with 89 additions and 29 deletions
Showing only changes of commit 4de8c2624a - Show all commits
+1 -2
View File
@@ -1,10 +1,10 @@
GITEA_NETWORK=gitea GITEA_NETWORK=gitea
AGENTCI_IMAGE=git.krtss.de/stanponomarev/agentci:latest
AGENTCI_GITEA_URL=http://gitea:3000 AGENTCI_GITEA_URL=http://gitea:3000
AGENTCI_BOT_USERNAME=agentci AGENTCI_BOT_USERNAME=agentci
AGENTCI_BOT_NAME=Agent CI AGENTCI_BOT_NAME=Agent CI
AGENTCI_BOT_EMAIL=agentci@localhost AGENTCI_BOT_EMAIL=agentci@localhost
OPENCODE_SERVER_USERNAME=opencode OPENCODE_SERVER_USERNAME=opencode
AGENTCI_OPENCODE_VERSION=^1
AGENTCI_PLAN_MODEL=openai/gpt-5.6-sol AGENTCI_PLAN_MODEL=openai/gpt-5.6-sol
AGENTCI_PLAN_VARIANT= AGENTCI_PLAN_VARIANT=
AGENTCI_IMPLEMENT_MODEL=openai/gpt-5.6-sol AGENTCI_IMPLEMENT_MODEL=openai/gpt-5.6-sol
@@ -24,4 +24,3 @@ AGENTCI_INSTALL_SCRIPTS=
AGENTCI_INSTALL_SCRIPT_TIMEOUT_SECONDS=900 AGENTCI_INSTALL_SCRIPT_TIMEOUT_SECONDS=900
AGENTCI_PYTHON_VERSION=3.13 AGENTCI_PYTHON_VERSION=3.13
AGENTCI_DOTNET_CHANNEL=10.0 AGENTCI_DOTNET_CHANNEL=10.0
CODEGRAPH_VERSION=1.3.1
+41
View File
@@ -0,0 +1,41 @@
name: Publish container image
on:
push:
branches:
- master
env:
REGISTRY: git.krtss.de
IMAGE_NAME: git.krtss.de/stanponomarev/agentci
jobs:
publish:
name: Build and push
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to Gitea registry
uses: https://github.com/docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push image
uses: https://github.com/docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
push: true
pull: true
tags: |
${{ env.IMAGE_NAME }}:latest
${{ env.IMAGE_NAME }}:${{ gitea.sha }}
labels: |
org.opencontainers.image.revision=${{ gitea.sha }}
org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }}
+24 -7
View File
@@ -46,10 +46,11 @@ recreated.
OpenCode models. OpenCode models.
3. Create `secrets/gitea_token`, `secrets/webhook_secret`, and 3. Create `secrets/gitea_token`, `secrets/webhook_secret`, and
`secrets/opencode_server_password`. Use high-entropy values for both secret/password files. `secrets/opencode_server_password`. Use high-entropy values for both secret/password files.
4. Build the image: 4. Log in to the Gitea container registry with a personal access token, then pull the image:
```sh ```sh
docker compose build docker login git.krtss.de
docker compose pull
``` ```
5. Authenticate the configured OpenCode providers before starting the persistent server: 5. Authenticate the configured OpenCode providers before starting the persistent server:
@@ -75,6 +76,22 @@ deployment, run the one-off `auth login` command above and then `docker compose
healthy and every configured model exists, supports tool calls, accepts its configured variant, and healthy and every configured model exists, supports tool calls, accepts its configured variant, and
has a connected provider. The worker leaves jobs queued while the runtime is unavailable. has a connected provider. The worker leaves jobs queued while the runtime is unavailable.
### Image publishing
Every push to `master` runs `.gitea/workflows/publish-image.yaml` and publishes the image as both
`git.krtss.de/stanponomarev/agentci:latest` and
`git.krtss.de/stanponomarev/agentci:<full-commit-sha>`. Configure these repository Actions secrets
before the first run:
| Secret | Value |
| --- | --- |
| `REGISTRY_USERNAME` | Username that owns the package or can write packages for the owner. |
| `REGISTRY_TOKEN` | Personal access token with package write permission. |
The automatic Gitea Actions token cannot publish packages. Compose uses `latest` by default; set
`AGENTCI_IMAGE=git.krtss.de/stanponomarev/agentci:<full-commit-sha>` in `.env` to deploy an immutable
revision.
## OpenCode ## OpenCode
Models use OpenCode's `provider/model` format. Planning, implementation, and research can use Models use OpenCode's `provider/model` format. Planning, implementation, and research can use
@@ -84,10 +101,11 @@ configures the research subagent and defaults to `high`. `AGENTCI_EXPLORE_MODEL`
`AGENTCI_EXPLORE_VARIANT` configure OpenCode's explore agent and default to `AGENTCI_EXPLORE_VARIANT` configure OpenCode's explore agent and default to
`openai/gpt-5.6-luna` with `low`. `openai/gpt-5.6-luna` with `low`.
`AGENTCI_OPENCODE_VERSION` controls the npm version or range installed into the image and defaults The `AGENTCI_OPENCODE_VERSION` Docker build argument controls the npm version or range installed into
to `^1`. The build verifies that the resolved version is still OpenCode 1.x and prints it. Docker the image and defaults to `^1`. The build verifies that the resolved version is still OpenCode 1.x
may reuse the cached installation layer until the configured version or build inputs change. and prints it. Docker may reuse the cached installation layer until the configured version or build
Runtime auto-update is disabled so an image cannot cross into OpenCode 2.x after it is built. inputs change. Runtime auto-update is disabled so an image cannot cross into OpenCode 2.x after it
is built.
The trusted configuration is `opencode/opencode.json`. OpenCode's default global and built-in-agent The trusted configuration is `opencode/opencode.json`. OpenCode's default global and built-in-agent
permission policies remain in effect; Agent CI does not replace them with an allow-all policy. permission policies remain in effect; Agent CI does not replace them with an allow-all policy.
@@ -172,7 +190,6 @@ uv run pyright
uv run pytest uv run pytest
uv run pytest --cov=agentci --cov-branch uv run pytest --cov=agentci --cov-branch
docker compose config docker compose config
docker compose build
``` ```
The coverage command is an opt-in diagnostic report; the regular test run remains the default and The coverage command is an opt-in diagnostic report; the regular test run remains the default and
+5 -15
View File
@@ -1,12 +1,6 @@
services: services:
agentci: agentci:
image: agentci:local image: ${AGENTCI_IMAGE:-git.krtss.de/stanponomarev/agentci:latest}
build:
context: .
args:
AGENTCI_OPENCODE_VERSION: ${AGENTCI_OPENCODE_VERSION:-^1}
CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1}
TEA_VERSION: ${TEA_VERSION:-0.14.2}
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
opencode: opencode:
@@ -39,7 +33,7 @@ services:
- webhook_secret - webhook_secret
- opencode_server_password - opencode_server_password
volumes: volumes:
- agentci_data:/var/lib/agentci - ./data/agentci:/var/lib/agentci
- ./install-scripts:/etc/agentci/install-scripts:ro - ./install-scripts:/etc/agentci/install-scripts:ro
tmpfs: tmpfs:
- /run/agentci:mode=1777 - /run/agentci:mode=1777
@@ -50,7 +44,7 @@ services:
- agentci_control - agentci_control
opencode: opencode:
image: agentci:local image: ${AGENTCI_IMAGE:-git.krtss.de/stanponomarev/agentci:latest}
command: ["opencode", "serve", "--hostname", "0.0.0.0", "--port", "4096"] command: ["opencode", "serve", "--hostname", "0.0.0.0", "--port", "4096"]
restart: unless-stopped restart: unless-stopped
environment: environment:
@@ -81,8 +75,8 @@ services:
- gitea_token - gitea_token
- opencode_server_password - opencode_server_password
volumes: volumes:
- agentci_data:/var/lib/agentci - ./data/agentci:/var/lib/agentci
- opencode_home:/var/lib/opencode - ./data/opencode:/var/lib/opencode
tmpfs: tmpfs:
- /run/agentci:mode=1777 - /run/agentci:mode=1777
expose: expose:
@@ -105,10 +99,6 @@ secrets:
opencode_server_password: opencode_server_password:
file: ./secrets/opencode_server_password file: ./secrets/opencode_server_password
volumes:
agentci_data:
opencode_home:
networks: networks:
gitea: gitea:
external: true external: true
+18 -5
View File
@@ -126,11 +126,10 @@ def test_compose_services_have_expected_runtime_contract() -> None:
"4096", "4096",
] ]
build = _section(agentci, "build") image = "${AGENTCI_IMAGE:-git.krtss.de/stanponomarev/agentci:latest}"
assert _mapping(build, "args", indent=6)["AGENTCI_OPENCODE_VERSION"] == ( assert _scalar(agentci, "image") == image
"${AGENTCI_OPENCODE_VERSION:-^1}" assert _scalar(opencode, "image") == image
) assert all(line.strip() != "build:" for line in [*agentci, *opencode])
assert all("no_cache" not in line for line in build)
dependency = _section(_section(agentci, "depends_on"), "opencode", indent=6) dependency = _section(_section(agentci, "depends_on"), "opencode", indent=6)
assert _mapping([" dependency:", *dependency], "dependency", indent=6) == { assert _mapping([" dependency:", *dependency], "dependency", indent=6) == {
@@ -157,6 +156,20 @@ def test_compose_has_no_sandbox_security_exceptions() -> None:
assert forbidden not in compose assert forbidden not in compose
def test_gitea_workflow_publishes_master_images() -> None:
lines = (ROOT / ".gitea" / "workflows" / "publish-image.yaml").read_text().splitlines()
push = _section(_section(lines, "on", indent=0), "push", indent=2)
assert _sequence(push, "branches", indent=4) == ["master"]
assert _mapping(lines, "env", indent=0)["IMAGE_NAME"] == (
"git.krtss.de/stanponomarev/agentci"
)
assert " ${{ env.IMAGE_NAME }}:latest" in lines
assert " ${{ env.IMAGE_NAME }}:${{ gitea.sha }}" in lines
assert " username: ${{ secrets.REGISTRY_USERNAME }}" in lines
assert " password: ${{ secrets.REGISTRY_TOKEN }}" in lines
def test_container_pins_opencode_major_version_contract() -> None: def test_container_pins_opencode_major_version_contract() -> None:
lines = [line.strip() for line in (ROOT / "Dockerfile").read_text().splitlines()] lines = [line.strip() for line in (ROOT / "Dockerfile").read_text().splitlines()]
build_arguments = {line.removeprefix("ARG ") for line in lines if line.startswith("ARG ")} build_arguments = {line.removeprefix("ARG ") for line in lines if line.startswith("ARG ")}