Skip to content

Commit a99ad5c

Browse files
stubbiclaude
andauthored
feat: hermes-operator v1.0.0 — plans 1–7 implementation (#1)
## Summary Implements **all seven plans** to bring `hermes-operator` to v1.0.0 quality. 175 commits delivering CRDs, reconcilers, webhooks, day-2 ops, release pipeline, conformance suite, and v1 docs/examples. ### Plans landed | Plan | Scope | Status | |---|---|---| | 1 | Kubebuilder v4 scaffold + minimal happy path | ✅ | | 2 | Hardened defaults, observability, leader election, full feature surface | ✅ | | 3 | Gateways, Honcho profile store, web terminal, Tailscale | ✅ | | 4 | `HermesSelfConfig` SSA controller + validator + GitOps coexistence | ✅ | | 5 | Day-2 ops: backup/restore, auto-update with rollback, OpenClaw migration | ✅ | | 6 | Distribution: release-please, GoReleaser, Cosign + SBOM, OLM bundle, conformance suite | ✅ (heavy conformance variants 18/19/20/22 deferred) | | 7 | v1 polish: api-versioning policy, deprecation policy, condition catalogue, ROADMAP, README, CHANGELOG, 10 worked examples | ✅ (Grafana 16/17 + release-merge 19-24 still open) | ### Highlights **API surface (`hermes.agent/v1`):** `HermesInstance` (namespaced), `HermesSelfConfig` (namespaced), `HermesClusterDefaults` (cluster-scoped singleton). Full validating/defaulting webhooks via cert-manager. **Reconcilers:** Main `HermesInstanceReconciler` + four sub-reconcilers (backup, restore, auto-update, migration) + dedicated `HermesSelfConfigReconciler`. All finalizer mutations use `r.Patch(ctx, inst, client.MergeFrom(original))` — never `r.Update` — enforced by `hack/reconcile-guard.sh` grep check (lesson #437). **SSA coexistence:** `HermesSelfConfig` writes via field manager `hermes.agent/selfconfig`; FluxCD writes via `kustomize-controller`; explicit envtest proves no flap. **Day-2 ops:** Scheduled + on-delete + pre-update backups (restic to S3/MinIO/R2); declarative `restoreFrom` with init-container; OCI-registry-driven `autoUpdate` with channel resolution, pre-update backup, probe-driven rollback, and `lastFailedTag` suppression; one-shot OpenClaw migration (sibling PVC or S3 snapshot). **Distribution pipeline:** `release-please-action` v4 → GoReleaser v2 multi-arch dockers → Cosign keyless OIDC signing of all three image tags (`vX.Y.Z`, `X.Y`, `latest`) → Syft SPDX-JSON SBOM attest → Helm OCI push to `ghcr.io/stubbi/charts` → OLM bundle (CSV + CRDs) auto-submitted to `k8s-operatorhub/community-operators` and `redhat-openshift-ecosystem/community-operators-prod`. Weekly `verify-signing.yaml` drift detector opens an `infra-broken` issue if cosign verification ever fails. **Conformance:** Ginkgo suite at `test/conformance/` with 10 testdata fixtures, negative tests for every webhook deny path, idempotency canary (10-reconcile no-op proving lesson #437), and nightly `.github/workflows/conformance.yaml` cron. envtest matrix runs k8s 1.28–1.32 on every PR. **Docs:** `docs/api-versioning.md`, `docs/deprecations.md`, `docs/conditions.md` (canonical catalogue), `docs/release-process.md`, `docs/conformance.md`, `docs/supported-versions.md`, `docs/backup-restore.md`, `docs/autoupdate.md`, `docs/migration.md`, `docs/security/signing.md`, `docs/api-reference.md`, `docs/selfconfig.md`. Public `ROADMAP.md`, `CHANGELOG.md` with v1.0.0 entry, README rewritten as v1 landing page. **Examples (10 worked recipes under `examples/`):** `minimal`, `full-featured`, `multi-platform`, `honcho`, `auto-update`, `backup-s3` (MinIO on kind), `migration-from-openclaw` (both source modes), `gitops-fluxcd`, `cluster-defaults`, plus `examples/README.md` index. ### What's deferred - **Plan 6 Tasks 18/19/20/22** — upgrade-matrix / GitOps kind / failure-injection / benchmark conformance jobs are scaffolded as workflow stubs but the heavy test bodies are deferred. The non-stub conformance jobs (negative, idempotency) gate PRs. - **Plan 6 Task 24** — live end-to-end smoke of release pipeline requires user-provided `RELEASE_PLEASE_TOKEN` PAT and cosign OIDC permissions. - **Plan 7 Tasks 16/17** — Grafana dashboard JSON + README still to land. - **Plan 7 Tasks 19–24** — release-merge / OperatorHub PR tracking / launch announce / flip repo public are user-driven post-merge actions. ### Required repo secrets before first release - `RELEASE_PLEASE_TOKEN` — fine-grained PAT with `contents: write` + `pull-requests: write`; needed for tag creation + OperatorHub fork-and-PR. Without it `release-please.yaml` and `operatorhub-submit.yaml` will fail. - Cosign keyless signing uses GitHub Actions OIDC — no key material to store, but the `release.yaml` workflow needs `id-token: write` permission (already set). ## Test plan - [ ] Lint workflow green - [ ] Test workflow green across k8s 1.28–1.32 envtest matrix - [ ] Reconcile Guard workflow green (no `r.Update` near finalizer mutations) - [ ] Helm RBAC Sync + Bundle RBAC Sync workflows green - [ ] Build workflow green (multi-arch) - [ ] E2E workflow green (kind + cert-manager + MinIO + helm install) - [ ] Conformance jobs (negative, idempotency) green; stub jobs may be skipped - [ ] Sample manifests under `config/samples/` apply cleanly - [ ] `examples/` recipes apply on a kind cluster (manual) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 173e60b commit a99ad5c

275 files changed

Lines changed: 55013 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
2+
# Ignore build and test binaries.
3+
bin/

.github/ISSUE_TEMPLATE/bug.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Bug report
2+
description: Report a defect in hermes-operator
3+
labels: ["bug", "needs-triage"]
4+
body:
5+
- type: textarea
6+
id: what-happened
7+
attributes:
8+
label: What happened?
9+
description: A clear description of the bug, including any error messages.
10+
validations:
11+
required: true
12+
- type: textarea
13+
id: expected
14+
attributes:
15+
label: What did you expect?
16+
validations:
17+
required: true
18+
- type: textarea
19+
id: reproduce
20+
attributes:
21+
label: How to reproduce
22+
description: A minimal HermesInstance / HermesSelfConfig manifest + the sequence of commands that triggers the bug.
23+
render: yaml
24+
validations:
25+
required: true
26+
- type: input
27+
id: operator-version
28+
attributes:
29+
label: hermes-operator version
30+
placeholder: "v0.1.0"
31+
validations:
32+
required: true
33+
- type: input
34+
id: kubernetes-version
35+
attributes:
36+
label: Kubernetes version
37+
placeholder: "v1.30.4"
38+
validations:
39+
required: true
40+
- type: dropdown
41+
id: install-method
42+
attributes:
43+
label: Install method
44+
options:
45+
- Helm chart
46+
- OLM bundle
47+
- dist/install.yaml (kubectl apply)
48+
- Other (please describe in "What happened?")
49+
validations:
50+
required: true
51+
- type: textarea
52+
id: logs
53+
attributes:
54+
label: Relevant operator logs
55+
description: |
56+
Output of `kubectl logs -n hermes-system -l control-plane=controller-manager --tail=200`
57+
render: shell
58+
- type: checkboxes
59+
id: terms
60+
attributes:
61+
label: Code of Conduct
62+
options:
63+
- label: I agree to follow this project's [Code of Conduct](https://github.com/stubbi/hermes-operator/blob/main/CODE_OF_CONDUCT.md).
64+
required: true

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: Questions
4+
url: https://github.com/stubbi/hermes-operator/discussions/categories/q-a
5+
about: Ask usage questions in Discussions (Q&A category).
6+
- name: Ideas + proposals
7+
url: https://github.com/stubbi/hermes-operator/discussions/categories/ideas
8+
about: Discuss ideas before opening a formal feature request.
9+
- name: Security vulnerabilities
10+
url: https://github.com/stubbi/hermes-operator/security/advisories/new
11+
about: Report privately via GitHub Security Advisories — do not open a public issue.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Feature request
2+
description: Suggest a new capability or improvement
3+
labels: ["enhancement", "needs-triage"]
4+
body:
5+
- type: textarea
6+
id: problem
7+
attributes:
8+
label: What problem are you trying to solve?
9+
description: Describe the user-facing problem, not the implementation. Skip if this is a pure DX/quality-of-life improvement.
10+
validations:
11+
required: true
12+
- type: textarea
13+
id: proposal
14+
attributes:
15+
label: Proposed solution
16+
description: How would this look from the user's perspective? Sketch CRD fields, CLI flags, condition reasons, etc.
17+
validations:
18+
required: true
19+
- type: textarea
20+
id: alternatives
21+
attributes:
22+
label: Alternatives considered
23+
- type: checkboxes
24+
id: terms
25+
attributes:
26+
label: Code of Conduct
27+
options:
28+
- label: I agree to follow this project's [Code of Conduct](https://github.com/stubbi/hermes-operator/blob/main/CODE_OF_CONDUCT.md).
29+
required: true

.github/dependabot.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: gomod
4+
directory: "/"
5+
schedule:
6+
interval: weekly
7+
day: monday
8+
open-pull-requests-limit: 10
9+
groups:
10+
kubernetes:
11+
patterns:
12+
- "k8s.io/*"
13+
- "sigs.k8s.io/*"
14+
otel:
15+
patterns:
16+
- "go.opentelemetry.io/*"
17+
labels:
18+
- dependencies
19+
- go
20+
21+
- package-ecosystem: github-actions
22+
directory: "/"
23+
schedule:
24+
interval: weekly
25+
day: monday
26+
open-pull-requests-limit: 5
27+
labels:
28+
- dependencies
29+
- github-actions
30+
31+
- package-ecosystem: docker
32+
directory: "/"
33+
schedule:
34+
interval: weekly
35+
day: monday
36+
labels:
37+
- dependencies
38+
- docker

.github/pull_request_template.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
## Summary
2+
3+
<!-- 1-3 sentences. What does this change and why? -->
4+
5+
## Test plan
6+
7+
- [ ] Lint passes (`make lint`)
8+
- [ ] Tests pass (`make test`)
9+
- [ ] Reconcile-guard passes (`bash hack/reconcile-guard.sh`)
10+
- [ ] Helm RBAC sync passes (`bash hack/check-helm-rbac.sh`)
11+
- [ ] For CRD/API changes: `make manifests` + `make generate` regenerated
12+
- [ ] For RBAC changes: `make sync-bundle-rbac` if the bundle is affected
13+
- [ ] For behavior changes: added/updated an envtest or e2e test
14+
15+
## Related issues
16+
17+
<!-- Closes #123 / refs #456 -->
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# .github/workflows/agent-image-smoke.yaml
2+
#
3+
# Per-PR smoke test: build the agent image on amd64, verify --help exits 0 and
4+
# entrypoint.sh refuses to start without a config. Does NOT push.
5+
name: agent-image-smoke
6+
7+
on:
8+
pull_request:
9+
paths:
10+
- 'images/hermes-agent/**'
11+
- '.github/workflows/agent-image-smoke.yaml'
12+
- 'Makefile'
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
smoke:
19+
runs-on: ubuntu-22.04
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Guard — skip when uv.lock is absent
24+
id: guard
25+
run: |
26+
if [ ! -f images/hermes-agent/uv.lock ]; then
27+
echo "uv.lock not yet committed; skipping smoke."
28+
echo "skip=true" >> "$GITHUB_OUTPUT"
29+
fi
30+
31+
- name: Set up Buildx
32+
if: steps.guard.outputs.skip != 'true'
33+
uses: docker/setup-buildx-action@v3
34+
35+
- name: Read pinned HERMES_VERSION from uv.lock
36+
if: steps.guard.outputs.skip != 'true'
37+
id: ver
38+
run: |
39+
set -eux
40+
ver=$(awk '/^name = "hermes-agent"$/{getline; if ($0 ~ /^version =/) {gsub(/[" ]/, "", $3); print $3; exit}}' images/hermes-agent/uv.lock)
41+
# uv.lock records the bare package version; tag form has a leading v.
42+
echo "tag=v${ver}" >> "$GITHUB_OUTPUT"
43+
44+
- name: Build image locally
45+
if: steps.guard.outputs.skip != 'true'
46+
run: |
47+
docker buildx build \
48+
--platform linux/amd64 \
49+
--build-arg HERMES_VERSION=${{ steps.ver.outputs.tag }} \
50+
--load \
51+
-t hermes-agent:smoke \
52+
images/hermes-agent
53+
54+
- name: Smoke — --help exits 0
55+
if: steps.guard.outputs.skip != 'true'
56+
run: docker run --rm hermes-agent:smoke hermes-agent --help >/dev/null
57+
58+
- name: Smoke — entrypoint refuses missing config with EX_CONFIG (78)
59+
if: steps.guard.outputs.skip != 'true'
60+
run: |
61+
set +e
62+
docker run --rm --entrypoint /usr/local/bin/hermes-entrypoint hermes-agent:smoke
63+
rc=$?
64+
if [ "${rc}" != "78" ]; then
65+
echo "Expected exit 78 (EX_CONFIG), got ${rc}" >&2
66+
exit 1
67+
fi
68+
69+
- name: Smoke — non-root by default
70+
if: steps.guard.outputs.skip != 'true'
71+
run: |
72+
uid=$(docker run --rm --entrypoint id hermes-agent:smoke -u)
73+
[ "${uid}" = "1000" ] || { echo "Expected UID 1000, got ${uid}"; exit 1; }

.github/workflows/agent-image.yaml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# .github/workflows/agent-image.yaml
2+
#
3+
# Builds and publishes ghcr.io/stubbi/hermes-agent for every hermes-agent
4+
# release in the supported matrix. Triggered:
5+
# - Manually via workflow_dispatch (engineer picks the HERMES_VERSION).
6+
# - On every push of a tag matching `agent/vX.Y.Z`.
7+
#
8+
# Cosign-signed (keyless OIDC) and SBOM via Syft.
9+
name: agent-image
10+
11+
on:
12+
workflow_dispatch:
13+
inputs:
14+
hermes_version:
15+
description: "Upstream hermes-agent release tag (e.g. v0.13.0)"
16+
required: true
17+
type: string
18+
push:
19+
tags:
20+
- 'agent/v*'
21+
22+
permissions:
23+
contents: read
24+
packages: write
25+
id-token: write
26+
27+
jobs:
28+
build:
29+
runs-on: ubuntu-22.04
30+
steps:
31+
- uses: actions/checkout@v4
32+
33+
- name: Resolve HERMES_VERSION
34+
id: ver
35+
run: |
36+
set -eux
37+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
38+
v="${{ inputs.hermes_version }}"
39+
else
40+
v="${GITHUB_REF#refs/tags/agent/}"
41+
fi
42+
# Strip any leading "v" for the bare package version, keep the tag form too.
43+
bare="${v#v}"
44+
echo "tag=${v}" >> "$GITHUB_OUTPUT"
45+
echo "bare=${bare}" >> "$GITHUB_OUTPUT"
46+
47+
- name: Set up QEMU
48+
uses: docker/setup-qemu-action@v3
49+
50+
- name: Set up Buildx
51+
uses: docker/setup-buildx-action@v3
52+
53+
- name: Log in to GHCR
54+
uses: docker/login-action@v3
55+
with:
56+
registry: ghcr.io
57+
username: ${{ github.actor }}
58+
password: ${{ secrets.GITHUB_TOKEN }}
59+
60+
- name: Verify uv.lock matches the requested HERMES_VERSION
61+
run: |
62+
set -eux
63+
# The lockfile is the source of truth; refuse to ship if the committed lock
64+
# disagrees with the requested version.
65+
ver=$(awk '/^name = "hermes-agent"$/{getline; if ($0 ~ /^version =/) {gsub(/[" ]/, "", $3); print $3; exit}}' images/hermes-agent/uv.lock)
66+
if [ "${ver}" != "${{ steps.ver.outputs.bare }}" ]; then
67+
echo "uv.lock pins hermes-agent==${ver} but workflow requested ${{ steps.ver.outputs.bare }}" >&2
68+
echo "Run: make agent-image-relock HERMES_VERSION=${{ steps.ver.outputs.tag }} and commit." >&2
69+
exit 1
70+
fi
71+
72+
- name: Build and push
73+
id: build
74+
uses: docker/build-push-action@v5
75+
with:
76+
context: images/hermes-agent
77+
file: images/hermes-agent/Dockerfile
78+
platforms: linux/amd64,linux/arm64
79+
push: true
80+
tags: |
81+
ghcr.io/stubbi/hermes-agent:${{ steps.ver.outputs.tag }}
82+
ghcr.io/stubbi/hermes-agent:latest
83+
build-args: |
84+
HERMES_VERSION=${{ steps.ver.outputs.tag }}
85+
provenance: true
86+
sbom: true
87+
88+
- name: Install Cosign
89+
uses: sigstore/cosign-installer@v3
90+
91+
- name: Sign image (keyless OIDC)
92+
env:
93+
DIGEST: ${{ steps.build.outputs.digest }}
94+
run: |
95+
cosign sign --yes "ghcr.io/stubbi/hermes-agent@${DIGEST}"
96+
97+
- name: Install Syft
98+
uses: anchore/sbom-action/download-syft@v0
99+
100+
- name: Generate SBOM
101+
run: |
102+
syft "ghcr.io/stubbi/hermes-agent:${{ steps.ver.outputs.tag }}" \
103+
-o spdx-json=sbom.spdx.json
104+
105+
- name: Attach SBOM as Cosign attestation
106+
env:
107+
DIGEST: ${{ steps.build.outputs.digest }}
108+
run: |
109+
cosign attest --yes \
110+
--predicate sbom.spdx.json \
111+
--type spdxjson \
112+
"ghcr.io/stubbi/hermes-agent@${DIGEST}"

.github/workflows/build.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Build
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
7+
jobs:
8+
docker:
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 20
11+
permissions:
12+
contents: read
13+
packages: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: docker/setup-buildx-action@v3
17+
# Multi-arch builds for releases happen in release.yaml via GoReleaser
18+
# (uses native runners + Go cross-compile, not QEMU). This job is a fast
19+
# PR smoke test — single-platform, host-arch.
20+
- uses: docker/login-action@v3
21+
if: github.event_name == 'push'
22+
with:
23+
registry: ghcr.io
24+
username: ${{ github.actor }}
25+
password: ${{ secrets.GITHUB_TOKEN }}
26+
- uses: docker/build-push-action@v6
27+
with:
28+
context: .
29+
platforms: linux/amd64
30+
push: ${{ github.event_name == 'push' }}
31+
tags: ghcr.io/stubbi/hermes-operator:dev,ghcr.io/stubbi/hermes-operator:${{ github.sha }}
32+
cache-from: type=gha
33+
cache-to: type=gha,mode=max

0 commit comments

Comments
 (0)