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
12 changes: 8 additions & 4 deletions .woodpecker/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ when:
branch: main

steps:
deploy-o6n:
deploy:
# Built by this repository's CI and pushed to the build registry.
image: git.mesh.kinaz.me/kina/coding-agent-deploy-tools:kubectl-1.36.0-helm-3.17.3-python-3.12-slim
environment:
KUBECONFIG_CONTENT:
from_secret: kubeconfig
IMAGE_REPOSITORY: git.mesh.kinaz.me/kina/coding-agent
IMAGE_REPOSITORY:
from_secret: image_repository
IMAGE_TAG: ${CI_COMMIT_SHA}
K8S_NAMESPACE: coding-agent-deepseek
K8S_NAMESPACE:
from_secret: k8s_namespace
K8S_DEPLOYMENT: coding-agent-coding-agent
K8S_CONTAINER: coding-agent
ROLLOUT_TIMEOUT: 420s
ENABLE_POD_HEALTH_SMOKE: "1"
HELM_RELEASE: coding-agent
HELM_CHART_DIR: coding-agent/helm
HELM_VALUES: coding-agent/helm/values-o6n-deepseek.yaml
VALUES_CONTENT:
from_secret: deploy_values
HELM_DEPLOY_MODE: dry-run
HELM_DRIVER: configmap
commands:
Expand Down
29 changes: 16 additions & 13 deletions coding-agent/docs/deployment/phase2-k8s.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,29 +197,32 @@ helm --kube-context <kube-context> upgrade --install coding-agent ./helm \
Use `--take-ownership` only when same-name resources have been confirmed to be
the existing coding-agent deployment that should become Helm-managed.

## Existing o6n DeepSeek Values
## Example Values

`helm/values-o6n-deepseek.yaml` captures the non-secret runtime configuration
for the existing o6n DeepSeek instance:
`helm/values-example.yaml` captures the non-secret runtime configuration shape
with placeholders only:

- Forgejo registry image repository: `git.mesh.kinaz.me/kina/coding-agent`
- registry image repository: `<registry>/<namespace>/coding-agent`
- namespace/release-compatible Service name through the chart fullname
- NodePort `30082`
- provider/model: `deepseek` / `deepseek-chat`
- runtime Secret references: `coding-agent-deepseek` and `coding-agent-langfuse`
- example NodePort `30080`
- provider/model placeholders for the target runtime
- runtime Secret references: `<provider-secret>` and `<observability-secret>`
- API bearer token Secret reference: `coding-agent-coding-agent-api-key` key
`api-key`
- Langfuse OTLP endpoint reference: `LANGFUSE_BASE_URL` from
`coding-agent-langfuse`
- a narrow NetworkPolicy egress exception for the mesh Langfuse endpoint
- observability endpoint reference from `<observability-secret>`
- a narrow NetworkPolicy egress exception placeholder for the observability
endpoint

The file must not contain token values. If these Secrets are managed by
SealedSecrets in the SRE repo, keep using SealedSecrets for the Secret manifests
and keep this chart values file limited to names and keys.

The Woodpecker o6n deploy script uses Helm server-side dry-run by default and
passes `--take-ownership` so the first apply can adopt the existing deployment
objects instead of failing on missing Helm ownership metadata.
Real values live in the private SRE inventory and Woodpecker repository secrets:
`deploy_values`, `kubeconfig`, `k8s_namespace`, and `image_repository`.

The Woodpecker deploy script uses Helm server-side dry-run by default and passes
`--take-ownership` so the first apply can adopt the existing deployment objects
instead of failing on missing Helm ownership metadata.

## Health Checks

Expand Down
54 changes: 31 additions & 23 deletions coding-agent/docs/deployment/woodpecker.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,40 @@ This repository uses Woodpecker for self-hosted CI/CD on the private mesh.

The deployment pipeline intentionally keeps runtime provider credentials in
Kubernetes Secrets. Woodpecker builds and deploys images, but it does not bake
DeepSeek, Langfuse, bearer tokens, or kube credentials into the image.
provider, observability, bearer token, or kube credentials into the image.

Real values live in the private SRE inventory and Woodpecker repository secrets:
`deploy_values`, `kubeconfig`, `k8s_namespace`, and `image_repository`.

## Pipelines

- `.woodpecker/ci.yml` runs lint, focused smoke tests, and builds the HTTP
server image.
- `.woodpecker/deploy.yml` is manual-only on `main` and runs the Helm deploy
script against the o6n DeepSeek values. It defaults to `HELM_DEPLOY_MODE=dry-run`
so the first pass uses Helm server-side dry-run to validate RBAC, ownership
takeover, and admission without mutating the cluster.
script against values supplied by the `deploy_values` Woodpecker secret. It
defaults to `HELM_DEPLOY_MODE=dry-run` so the first pass uses Helm server-side
dry-run to validate RBAC, ownership takeover, and admission without mutating
the cluster.

The manual deploy step uses the internal deploy-tools image built by
The manual deploy step uses the build-registry deploy-tools image built by
`.woodpecker/ci.yml`:

```text
git.mesh.kinaz.me/kina/coding-agent-deploy-tools:kubectl-1.36.0-helm-3.17.3-python-3.12-slim
<build-registry>/<namespace>/coding-agent-deploy-tools:kubectl-1.36.0-helm-3.17.3-python-3.12-slim
```

The default image target is:

```text
git.mesh.kinaz.me/kina/coding-agent:${CI_COMMIT_SHA}
<registry>/<namespace>/coding-agent:${CI_COMMIT_SHA}
```

The deploy workflow updates:

```text
namespace: coding-agent-deepseek
namespace: <k8s-namespace>
release: coding-agent
values: coding-agent/helm/values-o6n-deepseek.yaml
values: VALUES_CONTENT from deploy_values
deployment: coding-agent-coding-agent, when HELM_DEPLOY_MODE=apply
```

Expand All @@ -45,13 +49,16 @@ Configure these in Woodpecker repository secrets:
registry_username
registry_password
kubeconfig
deploy_values
k8s_namespace
image_repository
```

`registry_username` and `registry_password` must be able to push
`git.mesh.kinaz.me/kina/coding-agent`.
`<registry>/<namespace>/coding-agent`.

`kubeconfig` should be scoped to the target cluster and preferably to the
`coding-agent-deepseek` namespace.
`<k8s-namespace>` namespace.

Dry-run mode uses `helm upgrade --install --dry-run=server`, not local-only
template rendering. It therefore needs the same Kubernetes API visibility and
Expand All @@ -60,7 +67,7 @@ deploy script sets `HELM_DRIVER=configmap` by default so Helm stores release
metadata in ConfigMaps instead of Secrets.

Apply mode needs permission to manage the chart-owned resources in the
`coding-agent-deepseek` namespace:
`<k8s-namespace>` namespace:

- Deployment, Service, ConfigMap, PVC, ServiceAccount, and NetworkPolicy
- ConfigMaps used by Helm release metadata
Expand All @@ -76,28 +83,28 @@ switching from dry-run to apply.

## Existing Kubernetes Secrets

Keep runtime secrets in Kubernetes, not Woodpecker. The o6n values file contains
only Secret names and key names; Secret values should be supplied by ordinary
Kubernetes Secrets or SealedSecrets in the SRE repo.
Keep runtime secrets in Kubernetes, not this public repository. The deployment
values contain only Secret names and key names; Secret values should be supplied
by ordinary Kubernetes Secrets or SealedSecrets in the private SRE repo.

```bash
kubectl -n coding-agent-deepseek get secret coding-agent-deepseek
kubectl -n coding-agent-deepseek get secret coding-agent-langfuse
kubectl -n <k8s-namespace> get secret <provider-secret>
kubectl -n <k8s-namespace> get secret <observability-secret>
```

The deployment pipeline does not create or replace these secrets.

The o6n chart values expect:
The chart values expect:

```text
coding-agent-coding-agent-api-key: api-key
coding-agent-deepseek: DEEPSEEK_API_KEY
coding-agent-langfuse: LANGFUSE_BASE_URL, LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY
<provider-secret>: <provider-api-key-env>
<observability-secret>: <observability-endpoint-env>, <observability-public-key-env>, <observability-secret-key-env>
```

`LANGFUSE_BASE_URL` must be the Langfuse OTLP base endpoint accepted by the
Coding Agent exporter, for example a URL ending in `/api/public/otel`; it is not
the UI root URL unless the exporter endpoint is served there.
`<observability-endpoint-env>` must be the OTLP base endpoint accepted by the
Coding Agent exporter; it is not the UI root URL unless the exporter endpoint is
served there.

Verify key names from the cluster before switching apply mode on. Do not print
or commit Secret values.
Expand All @@ -121,6 +128,7 @@ ENABLE_POD_HEALTH_SMOKE
HELM_RELEASE
HELM_CHART_DIR
HELM_VALUES
VALUES_CONTENT
HELM_DEPLOY_MODE
HELM_DRIVER
```
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
image:
repository: git.mesh.kinaz.me/kina/coding-agent
repository: <registry>/<namespace>/coding-agent
tag: main
pullPolicy: IfNotPresent

Expand All @@ -13,20 +13,22 @@ server:

cors:
origins:
- http://100.123.220.136:30082
- https://<agent-host>

service:
type: NodePort
port: 8080
nodePort: 30082
# Example only. Pick an available NodePort in the target cluster.
nodePort: 30080

agent:
secretRef:
- coding-agent-deepseek
- coding-agent-langfuse
- <provider-secret>
- <observability-secret>
config:
provider: deepseek
model: deepseek-chat
# Example OpenAI-compatible provider settings. Replace for the target runtime.
provider: openai
model: gpt-4.1
approvalPolicy: auto
maxTurns: 30
subagentTimeout: 30.0
Expand All @@ -39,15 +41,15 @@ agent:
observability:
enabled: true
backend: langfuse
endpointEnv: LANGFUSE_BASE_URL
publicKeyEnv: LANGFUSE_PUBLIC_KEY
secretKeyEnv: LANGFUSE_SECRET_KEY
endpointEnv: <observability-endpoint-env>
publicKeyEnv: <observability-public-key-env>
secretKeyEnv: <observability-secret-key-env>

networkPolicy:
extraEgress:
- to:
- ipBlock:
cidr: 100.123.220.136/32
cidr: <observability-host-ip>/32
ports:
- protocol: TCP
port: 443
Expand Down
57 changes: 51 additions & 6 deletions coding-agent/tests/deploy/test_deploy_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ def test_deploy_script_defaults_to_helm_dry_run_without_kubectl_mutation(
assert result.returncode == 0, result.stderr
calls = (tmp_path / "calls.log").read_text(encoding="utf-8")
assert "helm upgrade --install coding-agent coding-agent/helm" in calls
assert "--namespace coding-agent-deepseek" in calls
assert "--values coding-agent/helm/values-o6n-deepseek.yaml" in calls
assert "--set image.repository=git.mesh.kinaz.me/kina/coding-agent" in calls
assert "--namespace coding-agent" in calls
assert "--values coding-agent/helm/values-example.yaml" in calls
assert "--set image.repository=<registry>/<namespace>/coding-agent" in calls
assert "--set image.tag=test-sha" in calls
assert "--take-ownership" in calls
assert "--dry-run=server" in calls
Expand All @@ -74,20 +74,65 @@ def test_deploy_script_defaults_to_helm_dry_run_without_kubectl_mutation(
def test_deploy_script_apply_mode_waits_for_rollout_and_smokes_service(
tmp_path: Path,
) -> None:
result = _run_script(tmp_path, HELM_DEPLOY_MODE="apply")
result = _run_script(
tmp_path,
HELM_DEPLOY_MODE="apply",
IMAGE_REPOSITORY="example.invalid/coding-agent",
VALUES_CONTENT="image:\n repository: example.invalid/coding-agent\n",
)

assert result.returncode == 0, result.stderr
calls = (tmp_path / "calls.log").read_text(encoding="utf-8")
assert "helm upgrade --install coding-agent coding-agent/helm" in calls
assert "--take-ownership" in calls
assert "--dry-run" not in calls
assert "kubectl -n coding-agent-deepseek rollout status" in calls
assert "kubectl -n coding-agent rollout status" in calls
assert (
"curl -fsS http://coding-agent-coding-agent.coding-agent-deepseek.svc.cluster.local:8080/healthz"
"curl -fsS http://coding-agent-coding-agent.coding-agent.svc.cluster.local:8080/healthz"
in calls
)


def test_deploy_script_apply_mode_rejects_placeholder_image_repository(
tmp_path: Path,
) -> None:
# Guard: apply must not run against the genericized placeholder defaults.
result = _run_script(tmp_path, HELM_DEPLOY_MODE="apply")

assert result.returncode == 2
assert "apply requires a real IMAGE_REPOSITORY" in result.stderr
assert not (tmp_path / "calls.log").exists()


def test_deploy_script_apply_mode_rejects_example_values_without_content(
tmp_path: Path,
) -> None:
result = _run_script(
tmp_path,
HELM_DEPLOY_MODE="apply",
IMAGE_REPOSITORY="example.invalid/coding-agent",
)

assert result.returncode == 2
assert "apply requires real values" in result.stderr
assert not (tmp_path / "calls.log").exists()


def test_deploy_script_uses_values_content_temp_file_when_present(
tmp_path: Path,
) -> None:
values_content = "image:\n repository: example.invalid/coding-agent\n"
result = _run_script(tmp_path, VALUES_CONTENT=values_content)

assert result.returncode == 0, result.stderr
calls = (tmp_path / "calls.log").read_text(encoding="utf-8")
helm_call = next(line for line in calls.splitlines() if "helm upgrade" in line)
parts = helm_call.split()
values_path = Path(parts[parts.index("--values") + 1])
assert values_path.read_text(encoding="utf-8") == values_content
assert "coding-agent/helm/values-example.yaml" not in helm_call


def test_woodpecker_builds_deploy_tools_image_used_by_manual_deploy() -> None:
ci = (ROOT / ".woodpecker" / "ci.yml").read_text(encoding="utf-8")
deploy = (ROOT / ".woodpecker" / "deploy.yml").read_text(encoding="utf-8")
Expand Down
Loading
Loading