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
13 changes: 9 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,22 @@ jobs:
with:
cluster_name: kind

- name: Build controller image
run: make image WHAT=cmd/axon-controller VERSION=e2e
- name: Build images
run: |
make image WHAT=cmd/axon-controller VERSION=e2e
make image WHAT=cmd/axon-spawner VERSION=e2e

- name: Load images into kind
run: kind load docker-image gjkim42/axon-controller:e2e
run: |
kind load docker-image gjkim42/axon-controller:e2e
kind load docker-image gjkim42/axon-spawner:e2e

- name: Deploy controller
run: |
kubectl apply -f install-crd.yaml
kubectl apply -f install.yaml
kubectl set image deployment/axon-controller-manager manager=gjkim42/axon-controller:e2e -n axon-system
kubectl patch deployment axon-controller-manager -n axon-system -p '{"spec":{"template":{"spec":{"containers":[{"name":"manager","imagePullPolicy":"Never"}]}}}}'
kubectl patch deployment axon-controller-manager -n axon-system -p '{"spec":{"template":{"spec":{"containers":[{"name":"manager","imagePullPolicy":"Never","args":["--leader-elect","--spawner-image=gjkim42/axon-spawner:e2e","--spawner-image-pull-policy=Never"]}]}}}}'
kubectl wait --for=condition=available deployment/axon-controller-manager -n axon-system --timeout=120s

- name: Build CLI
Expand All @@ -88,5 +92,6 @@ jobs:
- name: E2E Test
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
AXON_BIN: ${{ github.workspace }}/bin/axon
run: make test-e2e
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
- **Use Makefile targets** instead of discovering build/test commands yourself.
- **Keep changes minimal.** Do not refactor, reorganize, or 'improve' code beyond what was explicitly requested.
- **For CI/release workflows**, always use existing Makefile targets rather than reimplementing build logic in YAML.
- **Better tests** Always try to add or improve tests(including integration, e2e) when modifying code.
- **Better tests.** Always try to add or improve tests(including integration, e2e) when modifying code.
- **Logging conventions.** Start log messages with capital letters and do not end with punctuation.

## Key Makefile Targets
- `make verify` — run all verification checks (lint, fmt, vet, etc.)
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Image configuration
REGISTRY ?= gjkim42
VERSION ?= latest
IMAGE_DIRS ?= cmd/axon-controller claude-code
IMAGE_DIRS ?= cmd/axon-controller cmd/axon-spawner claude-code

# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.31.0
Expand Down
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ flowchart LR
AC --> J3[Job 3] --> P3["Pod 3 (--dangerously-skip-permissions)"] --> F3[Failed]
```

### TaskSpawner — Automatic Task Creation from External Sources

TaskSpawner watches external sources (e.g., GitHub Issues) and automatically creates Tasks for each discovered item.

```mermaid
flowchart LR
TS["TaskSpawner"] -- "polls" --> GH["GitHub Issues"]
GH -- "new issues" --> TS
TS -- "creates" --> T1["Task: fix-bugs-1"]
TS -- "creates" --> T2["Task: fix-bugs-2"]
```

---

1. You **apply a Task** manifest with a prompt, agent type, credential reference, and optionally a git repo to clone.
2. The **Axon controller** creates a Kubernetes **Job**.
3. If a workspace is specified, an **init container clones the repo**. Then the agent Pod starts with `--dangerously-skip-permissions`.
Expand Down Expand Up @@ -163,6 +177,7 @@ Or pass `--secret` to `axon run` with a pre-created secret (api-key is the defau
| CI-Native | Trigger agents from any pipeline via `kubectl`, Helm, Argo, or your own tooling |
| Git Workspace | Clone a repo into the agent's working directory via `spec.workspace` |
| Config File | Set token, model, namespace, and workspace in `~/.axon/config.yaml` — secrets are auto-created |
| TaskSpawner | Automatically create Tasks from GitHub Issues (or other sources) via a long-running spawner |
| CLI | `axon init`, `axon run`, `axon get`, `axon logs`, `axon delete` — manage tasks without writing YAML |
| Full Lifecycle | `Pending` → `Running` → `Succeeded` / `Failed` |
| Owner References | Delete a Task and its Job + Pod are automatically cleaned up |
Expand Down Expand Up @@ -195,6 +210,51 @@ Or pass `--secret` to `axon run` with a pre-created secret (api-key is the defau
| `spec.workspace.repo` | Git repository URL to clone (HTTPS, git://, or SSH) | No |
| `spec.workspace.ref` | Branch, tag, or commit SHA to checkout (defaults to repo's default branch) | No |

### TaskSpawner Spec

| Field | Description | Required |
|-------|-------------|----------|
| `spec.when.githubIssues.owner` | GitHub repository owner | Yes |
| `spec.when.githubIssues.repo` | GitHub repository name | Yes |
| `spec.when.githubIssues.labels` | Filter issues by labels | No |
| `spec.when.githubIssues.state` | Filter by state: `open`, `closed`, `all` (default: `open`) | No |
| `spec.when.githubIssues.tokenSecretRef.name` | Secret containing `GITHUB_TOKEN` | No |
| `spec.taskTemplate.type` | Agent type (`claude-code`) | Yes |
| `spec.taskTemplate.credentials` | Credentials for the agent (same as Task) | Yes |
| `spec.taskTemplate.model` | Model override | No |
| `spec.taskTemplate.workspace` | Git workspace for spawned Tasks | No |
| `spec.taskTemplate.promptTemplate` | Go text/template for prompt (`{{.Title}}`, `{{.Body}}`, `{{.Number}}`, etc.) | No |
| `spec.pollInterval` | How often to poll the source (default: `5m`) | No |

<details>
<summary>Example TaskSpawner YAML</summary>

```yaml
apiVersion: axon.io/v1alpha1
kind: TaskSpawner
metadata:
name: fix-bugs
spec:
when:
githubIssues:
owner: your-org
repo: your-repo
labels: [bug]
state: open
tokenSecretRef:
name: github-token
taskTemplate:
type: claude-code
credentials:
type: oauth
secretRef:
name: claude-credentials
promptTemplate: "Fix: {{.Title}}\n{{.Body}}"
pollInterval: 5m
```

</details>

### Task Status

| Field | Description |
Expand Down Expand Up @@ -255,6 +315,9 @@ axon run -p "Fix bug" --secret other-secret --credential-type api-key
# List tasks
axon get tasks

# List task spawners
axon get taskspawners

# View logs (follow mode)
axon logs my-task -f

Expand Down
148 changes: 148 additions & 0 deletions api/v1alpha1/taskspawner_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TaskSpawnerPhase represents the current phase of a TaskSpawner.
type TaskSpawnerPhase string

const (
// TaskSpawnerPhasePending means the TaskSpawner has been accepted but the spawner is not yet running.
TaskSpawnerPhasePending TaskSpawnerPhase = "Pending"
// TaskSpawnerPhaseRunning means the spawner is actively polling and creating tasks.
TaskSpawnerPhaseRunning TaskSpawnerPhase = "Running"
// TaskSpawnerPhaseFailed means the spawner has failed.
TaskSpawnerPhaseFailed TaskSpawnerPhase = "Failed"
)

// When defines the conditions that trigger task spawning.
// Exactly one field must be set.
type When struct {
// GitHubIssues discovers issues from a GitHub repository.
// +optional
GitHubIssues *GitHubIssues `json:"githubIssues,omitempty"`
}

// GitHubIssues discovers issues from a GitHub repository.
type GitHubIssues struct {
// Owner is the GitHub repository owner.
// +kubebuilder:validation:Required
Owner string `json:"owner"`

// Repo is the GitHub repository name.
// +kubebuilder:validation:Required
Repo string `json:"repo"`

// Labels filters issues by labels.
// +optional
Labels []string `json:"labels,omitempty"`

// State filters issues by state (open, closed, all). Defaults to open.
// +kubebuilder:validation:Enum=open;closed;all
// +kubebuilder:default=open
// +optional
State string `json:"state,omitempty"`

// TokenSecretRef references a Secret containing a GitHub token.
// The secret must have a key named GITHUB_TOKEN.
// +optional
TokenSecretRef *SecretReference `json:"tokenSecretRef,omitempty"`
}

// TaskTemplate defines the template for spawned Tasks.
type TaskTemplate struct {
// Type specifies the agent type (e.g., claude-code).
// +kubebuilder:validation:Required
Type string `json:"type"`

// Credentials specifies how to authenticate with the agent.
// +kubebuilder:validation:Required
Credentials Credentials `json:"credentials"`

// Model optionally overrides the default model.
// +optional
Model string `json:"model,omitempty"`

// Workspace optionally specifies a git repository for the agent to work in.
// +optional
Workspace *Workspace `json:"workspace,omitempty"`

// PromptTemplate is a Go text/template for rendering the task prompt.
// Available variables: {{.Number}}, {{.Title}}, {{.Body}}, {{.URL}}, {{.Comments}}, {{.Labels}}.
// +optional
PromptTemplate string `json:"promptTemplate,omitempty"`
}

// TaskSpawnerSpec defines the desired state of TaskSpawner.
type TaskSpawnerSpec struct {
// When defines the conditions that trigger task spawning.
// +kubebuilder:validation:Required
When When `json:"when"`

// TaskTemplate defines the template for spawned Tasks.
// +kubebuilder:validation:Required
TaskTemplate TaskTemplate `json:"taskTemplate"`

// PollInterval is how often to poll the source for new items (e.g., "5m"). Defaults to "5m".
// +kubebuilder:default="5m"
// +optional
PollInterval string `json:"pollInterval,omitempty"`
}

// TaskSpawnerStatus defines the observed state of TaskSpawner.
type TaskSpawnerStatus struct {
// Phase represents the current phase of the TaskSpawner.
// +optional
Phase TaskSpawnerPhase `json:"phase,omitempty"`

// DeploymentName is the name of the Deployment running the spawner.
// +optional
DeploymentName string `json:"deploymentName,omitempty"`

// TotalDiscovered is the total number of work items discovered.
// +optional
TotalDiscovered int `json:"totalDiscovered,omitempty"`

// TotalTasksCreated is the total number of Tasks created.
// +optional
TotalTasksCreated int `json:"totalTasksCreated,omitempty"`

// LastDiscoveryTime is the last time the source was polled.
// +optional
LastDiscoveryTime *metav1.Time `json:"lastDiscoveryTime,omitempty"`

// Message provides additional information about the current status.
// +optional
Message string `json:"message,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Source",type=string,JSONPath=`.spec.when.githubIssues.repo`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Discovered",type=integer,JSONPath=`.status.totalDiscovered`
// +kubebuilder:printcolumn:name="Tasks",type=integer,JSONPath=`.status.totalTasksCreated`
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`

// TaskSpawner is the Schema for the taskspawners API.
type TaskSpawner struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec TaskSpawnerSpec `json:"spec,omitempty"`
Status TaskSpawnerStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// TaskSpawnerList contains a list of TaskSpawner.
type TaskSpawnerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TaskSpawner `json:"items"`
}

func init() {
SchemeBuilder.Register(&TaskSpawner{}, &TaskSpawnerList{})
}
Loading