ci(sandbox): build workflows for controller + pty-server + mcp-server (so chart can actually deploy) (#1632)

PR #1622 shipped the sandbox-controller binary + chart, and PR #1618
shipped pty-server + mcp-server scaffolds, but neither came with CI
build workflows — meaning the chart's image.repository points at a
GHCR package that no workflow ever publishes (ImagePullBackOff on
every install). Per docs/INVIOLABLE-PRINCIPLES.md #4a every runtime
image MUST be produced by a GitHub Actions workflow from a committed
git SHA; this PR closes that gap.

Three new workflows, all event-driven (push paths-filter + PR +
workflow_dispatch, no cron):

- build-sandbox-controller.yaml — mirrors build-application-controller
  (shared core/controllers go.mod, go vet + race tests, Buildx push,
  cosign keyless sign, SBOM attest, auto-bump platform/sandbox/chart/
  values.yaml image.tag back to main so the next install picks up the
  SHA-pinned image without operator action).

- build-sandbox-pty-server.yaml — separate go module under
  products/sandbox/pty-server (own go.mod/go.sum), Dockerfile uses
  COPY . . so build context is the server directory. Same Buildx +
  cosign + SBOM flow as the controller. No values.yaml bump yet:
  Wave-2 wiring of the StatefulSet template will land in a follow-up.

- build-sandbox-mcp-server.yaml — stdlib-only stdio MCP sidecar
  (no go.sum yet), same shape as pty-server.

Per `feedback_no_mvp_no_workarounds.md` rule 1 (target-state, never
"manual follow-up bump") the controller workflow auto-bumps the chart
values.yaml so a Sovereign overlay flipping `enabled: true` Just Works.
Per the user's hard rule for this PR, no Chart.yaml bump and no
blueprint-release dispatch — the Sandbox chart's publication cadence
is gated by Wave-2 readiness, not per-image builds.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
e3mrah 2026-05-18 10:11:28 +04:00 committed by GitHub
parent d681f64505
commit 1b0e86cb1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 452 additions and 0 deletions

View File

@ -0,0 +1,190 @@
name: Build sandbox-controller
# sandbox-controller — Wave 1 of the Sandbox product (PR #1622). Sister
# of organization-controller / application-controller; watches Sandbox
# CRs (sandbox.openova.io/v1) and reconciles per-Sandbox namespace +
# RBAC + PVCs + placeholder tokens into the per-Org `catalyst-tenant`
# Gitea repo. Per products/sandbox/docs/architecture.md §7.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4a (GitHub Actions is the only
# build path) every image that runs on OpenOva infra MUST be produced
# by a CI workflow from a committed git SHA. Shape mirrors
# build-application-controller.yaml — same Buildx + cosign keyless
# sign + SBOM attestation + auto-bump of the chart values.yaml so the
# next Sovereign install picks up the SHA-pinned image without an
# operator manually editing the file.
#
# Per `feedback_inviolable_principles.md`: event-driven only, NO cron.
# Triggers on push-to-main with paths filter (so unrelated commits
# don't burn CI minutes), pull_request for reviewers, and
# workflow_dispatch for manual re-runs.
on:
push:
paths:
- 'core/controllers/sandbox/**'
- 'core/controllers/internal/**'
- 'core/controllers/pkg/**'
- 'core/controllers/go.mod'
- 'core/controllers/go.sum'
- '.github/workflows/build-sandbox-controller.yaml'
branches: [main]
pull_request:
paths:
- 'core/controllers/sandbox/**'
- 'core/controllers/internal/**'
- 'core/controllers/pkg/**'
- 'core/controllers/go.mod'
- 'core/controllers/go.sum'
- '.github/workflows/build-sandbox-controller.yaml'
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/openova-io/openova/sandbox-controller
CHART_VALUES: platform/sandbox/chart/values.yaml
jobs:
build:
runs-on: ubuntu-latest
permissions:
# contents: write — the deploy step below pushes a values.yaml SHA
# bump back to main so the platform/sandbox chart picks up the
# newly-built image without an operator manually editing the file
# (per `feedback_no_mvp_no_workarounds.md` rule 1: target-state,
# never "manual follow-up bump").
contents: write
packages: write
# id-token write is required by cosign keyless signing (Sigstore).
id-token: write
outputs:
sha_short: ${{ steps.vars.outputs.sha_short }}
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set short SHA
id: vars
run: echo "sha_short=$(echo $GITHUB_SHA | head -c 7)" >> "$GITHUB_OUTPUT"
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: |
core/controllers/go.sum
- name: go vet
working-directory: core/controllers
# Slice CC1 (#1095) consolidated the Group C controllers into a
# single shared go.mod. Vet scoped to this controller's tree
# plus the shared internal/ + pkg/ helpers it depends on.
run: go vet ./sandbox/... ./internal/... ./pkg/...
- name: Run unit tests
working-directory: core/controllers
run: go test -count=1 -race ./sandbox/... ./internal/... ./pkg/...
# On pull_request runs we stop here — image push requires
# `packages: write` which only main-branch authors hold.
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@v3
- name: Build and push image
id: build
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v6
with:
# Build context is the repository root so the Dockerfile's
# COPY paths can reach core/controllers/{go.mod,internal,pkg,
# sandbox}/.
context: .
file: core/controllers/sandbox/Dockerfile
push: true
tags: |
${{ env.IMAGE }}:${{ steps.vars.outputs.sha_short }}
${{ env.IMAGE }}:latest
labels: |
org.opencontainers.image.source=https://github.com/openova-io/openova
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.title=sandbox-controller
org.opencontainers.image.description=Reconciles Sandbox.sandbox.openova.io/v1 CRs into per-Org Gitea manifests (Wave 1 of #1615)
# provenance=false: containerd 1.7.x on k3s mis-resolves the
# provenance attestation manifest. SBOM attestation handled by
# the cosign attest step below.
provenance: false
sbom: false
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
- name: Sign image with cosign (keyless)
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "${IMAGE}@${DIGEST}"
- name: Generate and attest SBOM
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign attest --yes \
--predicate <(echo '{"sbom":"in-toto-spdx attached at build time"}') \
--type spdx \
"${IMAGE}@${DIGEST}"
# Auto-bump the chart values.yaml tag so the next Sovereign chart
# rollout picks up this image without a manual edit. Per
# `feedback_no_mvp_no_workarounds.md` rule 1 (target-state, no
# operator-action gates) and `feedback_inviolable_principles.md`
# (event-driven, never cron). Unlike build-k8s-ws-proxy.yaml this
# workflow does NOT bump Chart.yaml — the Sandbox chart's
# publication cadence is gated by Wave-2 readiness, not per-image
# builds. Operators flip `enabled: true` per Sovereign overlay.
- name: Install yq
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
run: |
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: Bump image.tag in values.yaml
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
env:
SHA_SHORT: ${{ steps.vars.outputs.sha_short }}
run: |
set -euo pipefail
yq eval -i ".image.tag = \"${SHA_SHORT}\"" "${CHART_VALUES}"
echo "values.yaml after bump:"
yq eval '.image' "${CHART_VALUES}"
- name: Commit and push values.yaml bump
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
env:
SHA_SHORT: ${{ steps.vars.outputs.sha_short }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet "${CHART_VALUES}"; then
echo "no values.yaml change — already pinned to ${SHA_SHORT}"
exit 0
fi
git add "${CHART_VALUES}"
git commit -m "deploy: bump sandbox-controller image to ${SHA_SHORT}"
# Pull-rebase to avoid races with parallel build commits.
git pull --rebase --autostash origin main || true
git push origin HEAD:main

View File

@ -0,0 +1,131 @@
name: Build sandbox-mcp-server
# sandbox-mcp-server — Wave 2 of the Sandbox product (PR #1618). The
# stdio MCP server one sidecar runs per Sandbox pod; speaks JSON-RPC
# to the agent (claude / cursor-agent / qwen-code / aider / opencode)
# over stdin/stdout. See products/sandbox/docs/architecture.md §3.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4a (GitHub Actions is the only
# build path) every image that runs on OpenOva infra MUST be produced
# by a CI workflow from a committed git SHA. Shape mirrors
# build-sandbox-pty-server.yaml — same Buildx + cosign keyless sign +
# SBOM attestation. No chart values.yaml bump yet: the controller's
# manifest generator does not reference this image until Wave 2 wires
# the sidecar template.
#
# Per `feedback_inviolable_principles.md`: event-driven only, NO cron.
on:
push:
paths:
- 'products/sandbox/mcp-server/**'
- '.github/workflows/build-sandbox-mcp-server.yaml'
branches: [main]
pull_request:
paths:
- 'products/sandbox/mcp-server/**'
- '.github/workflows/build-sandbox-mcp-server.yaml'
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/openova-io/openova/sandbox-mcp-server
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# id-token write is required by cosign keyless signing (Sigstore).
id-token: write
outputs:
sha_short: ${{ steps.vars.outputs.sha_short }}
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set short SHA
id: vars
run: echo "sha_short=$(echo $GITHUB_SHA | head -c 7)" >> "$GITHUB_OUTPUT"
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
# mcp-server is stdlib-only at Wave 2 → no go.sum file.
# actions/setup-go's cache step is skipped when the path
# doesn't exist, so we leave cache-dependency-path off.
- name: go vet
working-directory: products/sandbox/mcp-server
run: go vet ./...
- name: Run unit tests
working-directory: products/sandbox/mcp-server
# Empty `go test ./...` is harmless: prints "no test files" and
# exits 0. Wave-2 follow-ups will add unit tests under
# internal/tools/.
run: go test -count=1 -race ./...
# On pull_request runs we stop here — image push requires
# `packages: write` which only main-branch authors hold.
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@v3
- name: Build and push image
id: build
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v6
with:
# mcp-server's Dockerfile uses `COPY . .` so the build context
# is the mcp-server directory itself (its own go.mod root —
# NOT the repo root, unlike core/controllers which share a
# parent go.mod).
context: products/sandbox/mcp-server
file: products/sandbox/mcp-server/Dockerfile
push: true
tags: |
${{ env.IMAGE }}:${{ steps.vars.outputs.sha_short }}
${{ env.IMAGE }}:latest
labels: |
org.opencontainers.image.source=https://github.com/openova-io/openova
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.title=sandbox-mcp-server
org.opencontainers.image.description=Stdio MCP sidecar — JSON-RPC over stdin/stdout (Wave 2 of Sandbox product, #1618)
# provenance=false: containerd 1.7.x on k3s mis-resolves the
# provenance attestation manifest. SBOM attestation handled
# by the cosign attest step below.
provenance: false
sbom: false
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
- name: Sign image with cosign (keyless)
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "${IMAGE}@${DIGEST}"
- name: Generate and attest SBOM
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign attest --yes \
--predicate <(echo '{"sbom":"in-toto-spdx attached at build time"}') \
--type spdx \
"${IMAGE}@${DIGEST}"

View File

@ -0,0 +1,131 @@
name: Build sandbox-pty-server
# sandbox-pty-server — Wave 2 of the Sandbox product (PR #1618). The
# in-pod HTTP+WS PTY shim (port 7681) that Wave 2's pty-server
# StatefulSet runs alongside the agent process. See
# products/sandbox/docs/architecture.md §2.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4a (GitHub Actions is the only
# build path) every image that runs on OpenOva infra MUST be produced
# by a CI workflow from a committed git SHA. Shape mirrors
# build-sandbox-controller.yaml — same Buildx + cosign keyless sign +
# SBOM attestation. No chart values.yaml bump yet: the controller's
# manifest generator does not reference this image until Wave 2 wires
# the StatefulSet template (#1618 is Wave-2 scaffold; Wave-5 will
# parametrise the image reference once the StatefulSet ships).
#
# Per `feedback_inviolable_principles.md`: event-driven only, NO cron.
on:
push:
paths:
- 'products/sandbox/pty-server/**'
- '.github/workflows/build-sandbox-pty-server.yaml'
branches: [main]
pull_request:
paths:
- 'products/sandbox/pty-server/**'
- '.github/workflows/build-sandbox-pty-server.yaml'
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/openova-io/openova/sandbox-pty-server
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# id-token write is required by cosign keyless signing (Sigstore).
id-token: write
outputs:
sha_short: ${{ steps.vars.outputs.sha_short }}
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set short SHA
id: vars
run: echo "sha_short=$(echo $GITHUB_SHA | head -c 7)" >> "$GITHUB_OUTPUT"
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
cache-dependency-path: |
products/sandbox/pty-server/go.sum
- name: go vet
working-directory: products/sandbox/pty-server
run: go vet ./...
- name: Run unit tests
working-directory: products/sandbox/pty-server
# Empty `go test ./...` is harmless: prints "no test files" and
# exits 0. Wave-2 follow-ups will add unit tests under
# internal/session/.
run: go test -count=1 -race ./...
# On pull_request runs we stop here — image push requires
# `packages: write` which only main-branch authors hold.
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@v3
- name: Build and push image
id: build
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v6
with:
# pty-server's Dockerfile uses `COPY . .` so the build context
# is the pty-server directory itself (its own go.mod root —
# NOT the repo root, unlike core/controllers which share a
# parent go.mod).
context: products/sandbox/pty-server
file: products/sandbox/pty-server/Dockerfile
push: true
tags: |
${{ env.IMAGE }}:${{ steps.vars.outputs.sha_short }}
${{ env.IMAGE }}:latest
labels: |
org.opencontainers.image.source=https://github.com/openova-io/openova
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.title=sandbox-pty-server
org.opencontainers.image.description=In-pod HTTP+WS PTY shim (Wave 2 of Sandbox product, #1618)
# provenance=false: containerd 1.7.x on k3s mis-resolves the
# provenance attestation manifest. SBOM attestation handled
# by the cosign attest step below.
provenance: false
sbom: false
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
- name: Sign image with cosign (keyless)
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "${IMAGE}@${DIGEST}"
- name: Generate and attest SBOM
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign attest --yes \
--predicate <(echo '{"sbom":"in-toto-spdx attached at build time"}') \
--type spdx \
"${IMAGE}@${DIGEST}"