diff --git a/.env.example b/.env.example index 88c6b67..b99be15 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,10 @@ GITEA_NETWORK=gitea +AGENTCI_IMAGE=git.krtss.de/stanponomarev/agentci:latest AGENTCI_GITEA_URL=http://gitea:3000 AGENTCI_BOT_USERNAME=agentci AGENTCI_BOT_NAME=Agent CI AGENTCI_BOT_EMAIL=agentci@localhost OPENCODE_SERVER_USERNAME=opencode -AGENTCI_OPENCODE_VERSION=^1 AGENTCI_PLAN_MODEL=openai/gpt-5.6-sol AGENTCI_PLAN_VARIANT= AGENTCI_IMPLEMENT_MODEL=openai/gpt-5.6-sol @@ -24,4 +24,3 @@ AGENTCI_INSTALL_SCRIPTS= AGENTCI_INSTALL_SCRIPT_TIMEOUT_SECONDS=900 AGENTCI_PYTHON_VERSION=3.13 AGENTCI_DOTNET_CHANNEL=10.0 -CODEGRAPH_VERSION=1.3.1 diff --git a/.gitea/workflows/publish-image.yaml b/.gitea/workflows/publish-image.yaml new file mode 100644 index 0000000..98acd9e --- /dev/null +++ b/.gitea/workflows/publish-image.yaml @@ -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 }} diff --git a/README.md b/README.md index d4517de..eae0930 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,11 @@ recreated. OpenCode models. 3. Create `secrets/gitea_token`, `secrets/webhook_secret`, and `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 - docker compose build + docker login git.krtss.de + docker compose pull ``` 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 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:`. 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:` in `.env` to deploy an immutable +revision. + ## OpenCode 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 `openai/gpt-5.6-luna` with `low`. -`AGENTCI_OPENCODE_VERSION` controls the npm version or range installed into the image and defaults -to `^1`. The build verifies that the resolved version is still OpenCode 1.x and prints it. Docker -may reuse the cached installation layer until the configured version or build inputs change. -Runtime auto-update is disabled so an image cannot cross into OpenCode 2.x after it is built. +The `AGENTCI_OPENCODE_VERSION` Docker build argument controls the npm version or range installed into +the image and defaults to `^1`. The build verifies that the resolved version is still OpenCode 1.x +and prints it. Docker may reuse the cached installation layer until the configured version or build +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 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 --cov=agentci --cov-branch docker compose config -docker compose build ``` The coverage command is an opt-in diagnostic report; the regular test run remains the default and diff --git a/compose.yaml b/compose.yaml index 2762d9c..f1a451e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,12 +1,6 @@ services: agentci: - image: agentci:local - build: - context: . - args: - AGENTCI_OPENCODE_VERSION: ${AGENTCI_OPENCODE_VERSION:-^1} - CODEGRAPH_VERSION: ${CODEGRAPH_VERSION:-1.3.1} - TEA_VERSION: ${TEA_VERSION:-0.14.2} + image: ${AGENTCI_IMAGE:-git.krtss.de/stanponomarev/agentci:latest} restart: unless-stopped depends_on: opencode: @@ -39,7 +33,7 @@ services: - webhook_secret - opencode_server_password volumes: - - agentci_data:/var/lib/agentci + - ./data/agentci:/var/lib/agentci - ./install-scripts:/etc/agentci/install-scripts:ro tmpfs: - /run/agentci:mode=1777 @@ -50,7 +44,7 @@ services: - agentci_control opencode: - image: agentci:local + image: ${AGENTCI_IMAGE:-git.krtss.de/stanponomarev/agentci:latest} command: ["opencode", "serve", "--hostname", "0.0.0.0", "--port", "4096"] restart: unless-stopped environment: @@ -81,8 +75,8 @@ services: - gitea_token - opencode_server_password volumes: - - agentci_data:/var/lib/agentci - - opencode_home:/var/lib/opencode + - ./data/agentci:/var/lib/agentci + - ./data/opencode:/var/lib/opencode tmpfs: - /run/agentci:mode=1777 expose: @@ -105,10 +99,6 @@ secrets: opencode_server_password: file: ./secrets/opencode_server_password -volumes: - agentci_data: - opencode_home: - networks: gitea: external: true diff --git a/tests/test_opencode_deployment.py b/tests/test_opencode_deployment.py index 90db7be..8aedb1b 100644 --- a/tests/test_opencode_deployment.py +++ b/tests/test_opencode_deployment.py @@ -126,11 +126,10 @@ def test_compose_services_have_expected_runtime_contract() -> None: "4096", ] - build = _section(agentci, "build") - assert _mapping(build, "args", indent=6)["AGENTCI_OPENCODE_VERSION"] == ( - "${AGENTCI_OPENCODE_VERSION:-^1}" - ) - assert all("no_cache" not in line for line in build) + image = "${AGENTCI_IMAGE:-git.krtss.de/stanponomarev/agentci:latest}" + assert _scalar(agentci, "image") == image + assert _scalar(opencode, "image") == image + assert all(line.strip() != "build:" for line in [*agentci, *opencode]) dependency = _section(_section(agentci, "depends_on"), "opencode", 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 +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: lines = [line.strip() for line in (ROOT / "Dockerfile").read_text().splitlines()] build_arguments = {line.removeprefix("ARG ") for line in lines if line.startswith("ARG ")}