Skip to content

Commit 677a31f

Browse files
enhance
1 parent 6f9202f commit 677a31f

File tree

3 files changed

+228
-27
lines changed

3 files changed

+228
-27
lines changed

docs/book/src/reference/commands/alpha_update.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ automates the process of running `kubebuilder alpha update` on a schedule
1515
workflow when new Kubebuilder releases are available.
1616
>
1717
> Moreover, you will be able to get help from AI models to understand what changes are needed to keep your project up to date
18-
and how to solve conflicts if any be faced.
18+
and how to solve conflicts if any are faced.
1919

2020
## When to Use It
2121

pkg/cli/alpha/internal/update/helpers/open_gh_issue.go

Lines changed: 221 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,36 @@ List each conflicted file with a brief suggestion. For GENERATED files:
146146
Always end suggestions with: "run: make fmt vet lint-fix"
147147
</details>`
148148

149+
// Static repository guidance for AI prompts (concise, model-friendly).
150+
const projectOverview = `## Repository Guidance (Project Overview)
151+
Use this as factual context to interpret file changes. Do not invent paths or files.
152+
153+
- Type: Kubernetes Operator (Kubebuilder scaffold)
154+
- Language: Go (version pinned in go.mod)
155+
- Key libs: sigs.k8s.io/controller-runtime, k8s.io/*
156+
- Tools: sigs.k8s.io/controller-tools (controller-gen), kustomize, envtest, golangci-lint, kind (for e2e)
157+
158+
## Project Layout
159+
- /api — CRD types and deepcopy files
160+
- internal/controllers — Reconciliation logic
161+
- internal/webhooks — Admission webhooks (defaulting and validating)
162+
- /cmd — Main entrypoint
163+
- /config — Kustomize manifests and operator configuration:
164+
- certmanager/ — Certificates and issuers
165+
- crd/ — CustomResourceDefinitions
166+
- default/ — Default overlays
167+
- manager/ — Manager deployment manifests
168+
- network-policy/ — Network policies
169+
- prometheus/ — Monitoring resources
170+
- rbac/ — Roles and bindings
171+
- samples/ — Example custom resources
172+
- webhook/ — Webhook configurations
173+
- /test — E2E tests and helpers
174+
- /dist — Distribution artifacts
175+
- Makefile — Build automation
176+
- PROJECT — Project definition file (auto-generated; avoid manual edits)
177+
`
178+
149179
// isGeneratedKB returns true for Kubebuilder-generated artifacts.
150180
func isGeneratedKB(path string) bool {
151181
return strings.Contains(path, "/zz_generated.") ||
@@ -154,20 +184,6 @@ func isGeneratedKB(path string) bool {
154184
path == "dist/install.yaml"
155185
}
156186

157-
// readCopilotInstructions returns the contents of .github/copilot_instructions.md
158-
// from the repo root. If the file doesn't exist or can't be read, it returns "".
159-
// maxBytes trims the content to avoid overly large prompts (use 0 for no trim).
160-
func readCopilotInstructions() string {
161-
const rel = ".github/copilot_instructions.md"
162-
b, err := os.ReadFile(rel)
163-
if err != nil || len(b) == 0 {
164-
return ""
165-
}
166-
// Normalize line endings and trim extra whitespace to keep the prompt tidy.
167-
s := strings.ReplaceAll(string(b), "\r\n", "\n")
168-
return strings.TrimSpace(s)
169-
}
170-
171187
// ListConflictFiles walks the working directory and finds files that contain
172188
// Git conflict markers, splitting results into SOURCE vs GENERATED buckets.
173189
// This version does not rely on `git grep`.
@@ -308,36 +324,87 @@ func ListChangedFiles(base, head string) (src []string, gen []string) {
308324
return src, gen
309325
}
310326

327+
// BuildAIPrompt builds the full positional prompt for `gh models run`.
328+
// Provide BOTH: changed files and conflicted files.
311329
// BuildAIPrompt builds the full positional prompt for `gh models run`.
312330
// Provide BOTH: changed files and conflicted files.
313331
func BuildAIPrompt(
314-
from, to, out, compareURL, releaseURL string,
332+
fromVersion, toVersion, baseBranch, outBranch, compareURL, releaseURL string,
315333
changedSrc, changedGen []string,
316334
conflictSrc, conflictGen []string,
317335
) string {
318336
var ctx strings.Builder
337+
338+
// Header/context
319339
ctx.WriteString("PROJECT: Created with Kubebuilder\n")
320-
fmt.Fprintf(&ctx, "Upgrade: from %s to %s\n", from, to)
321-
fmt.Fprintf(&ctx, "Output branch: %s\n", out)
340+
fmt.Fprintf(&ctx, "Upgrade: from %s to %s\n", fromVersion, toVersion)
341+
fmt.Fprintf(&ctx, "Output branch: %s\n", outBranch)
322342
fmt.Fprintf(&ctx, "Compare PR URL: %s\n", compareURL)
323343
fmt.Fprintf(&ctx, "Release notes: %s\n\n", releaseURL)
324344

325-
// Optional: project guidance for the model (teaches the AI)
326-
if s := strings.TrimSpace(readCopilotInstructions()); s != "" {
327-
ctx.WriteString("## Repository Guidance (.github/copilot_instructions.md)\n")
328-
ctx.WriteString(s)
329-
ctx.WriteString("\n\n")
345+
// Repository guidance (concise, model-friendly)
346+
ctx.WriteString(projectOverview)
347+
ctx.WriteString("\n\n")
348+
349+
// Teach the model the 3-way workflow and show explicit compare range with SHAs
350+
ctx.WriteString("## Update Method (3-way merge)\n")
351+
ctx.WriteString("- Ancestor: clean scaffold at FROM version\n")
352+
ctx.WriteString("- Original: your current project from --from-branch\n")
353+
ctx.WriteString("- Upgrade: clean scaffold at TO version\n")
354+
ctx.WriteString("- Merge: Original -> Upgrade (then squash or keep history)\n\n")
355+
356+
base := strings.TrimSpace(baseBranch)
357+
head := strings.TrimSpace(outBranch)
358+
fmt.Fprintf(&ctx, "## Compare Range\nBASE: %s\nHEAD: %s\n", rev(base), rev(head))
359+
if compareURL != "" {
360+
fmt.Fprintf(&ctx, "Compare PR URL: %s\n\n", compareURL)
361+
} else {
362+
ctx.WriteString("\n")
363+
}
364+
365+
// Whole-repo diff summaries
366+
short := DiffShortStat(base, head)
367+
nameStatus := DiffNameStatus(base, head)
368+
stat := DiffStat(base, head)
369+
370+
ctx.WriteString("## Diff Summary (whole repo)\n")
371+
if short != "" {
372+
fmt.Fprintf(&ctx, "%s\n\n", short)
373+
}
374+
if stat != "" {
375+
ctx.WriteString("### Per-file stat\n")
376+
ctx.WriteString(stat + "\n\n")
377+
}
378+
if nameStatus != "" {
379+
ctx.WriteString("### Name-Status (A/M/D/R)\n")
380+
ctx.WriteString(nameStatus + "\n\n")
330381
}
331382

332-
// List changed files so the AI can build the Reviewed Changes table.
383+
// Dependency bump spotlight
384+
ctx.WriteString("## go.mod Summary (require/replace/module)\n")
385+
ctx.WriteString(summarizeGoModDiff(base, head) + "\n\n")
386+
387+
// Targeted patches for reviewer-relevant SOURCE files (exclude GENERATED)
388+
important := pickImportantFiles(changedSrc, 10) // include /api; keep token budget tight
389+
if len(important) > 0 {
390+
ctx.WriteString("## Selected Patches (SOURCE)\n")
391+
for _, p := range important {
392+
patch := filePatch(base, head, p, 12<<10) // 12KB/file cap
393+
if patch == "" {
394+
continue
395+
}
396+
fmt.Fprintf(&ctx, "\n----- BEGIN PATCH %s -----\n%s\n----- END PATCH %s -----\n", p, patch, p)
397+
}
398+
ctx.WriteString("\n")
399+
}
400+
401+
// File lists for the model to build tables/sections
333402
if len(changedSrc) > 0 {
334403
fmt.Fprintf(&ctx, "\nChanged [SOURCE] files:\n%s\n", bulletList(changedSrc))
335404
}
336405
if len(changedGen) > 0 {
337406
fmt.Fprintf(&ctx, "\nChanged [GENERATED] files:\n%s\n", bulletList(changedGen))
338407
}
339-
340-
// List conflicts for extra context (will be empty if none)
341408
if len(conflictSrc) > 0 {
342409
fmt.Fprintf(&ctx, "\nConflicted [SOURCE] files:\n%s\n", bulletList(conflictSrc))
343410
}
@@ -347,3 +414,132 @@ func BuildAIPrompt(
347414

348415
return ctx.String() + "\n\n" + aiPRPrompt
349416
}
417+
418+
// DiffShortStat returns "N files changed, X insertions(+), Y deletions(-)".
419+
func DiffShortStat(base, head string) string {
420+
cmd := exec.Command("git", "diff", "--shortstat", base+".."+head)
421+
out, _ := cmd.Output()
422+
return strings.TrimSpace(string(out))
423+
}
424+
425+
// DiffNameStatus returns lines like "M\tpath/file.go" (with renames).
426+
func DiffNameStatus(base, head string) string {
427+
cmd := exec.Command("git", "diff", "--name-status", "-M", base+".."+head)
428+
out, _ := cmd.Output()
429+
return strings.TrimSpace(string(out))
430+
}
431+
432+
// DiffStat returns per-file stat (compact summary with +/- bars).
433+
func DiffStat(base, head string) string {
434+
cmd := exec.Command("git", "diff", "--stat", "-M", base+".."+head)
435+
out, _ := cmd.Output()
436+
return strings.TrimSpace(string(out))
437+
}
438+
439+
// filePatch returns a unified diff for one file, truncated to maxBytes.
440+
func filePatch(base, head, path string, maxBytes int) string {
441+
cmd := exec.Command("git", "diff", "--no-color", "--unified=3", base+".."+head, "--", path)
442+
out, _ := cmd.Output()
443+
if len(out) == 0 {
444+
return ""
445+
}
446+
if maxBytes > 0 && len(out) > maxBytes {
447+
out = append(out[:maxBytes], []byte("\n... [truncated] ...")...)
448+
}
449+
return strings.TrimSpace(string(out))
450+
}
451+
452+
// pickImportantFiles chooses reviewer-relevant SOURCE files first.
453+
// Priority order: go.mod, Makefile, /api, internal/controllers, internal/webhooks, /cmd, then the rest.
454+
func pickImportantFiles(src []string, limit int) []string {
455+
if len(src) == 0 || limit <= 0 {
456+
return nil
457+
}
458+
orderFirst := []string{"go.mod", "Makefile"}
459+
var api, ctrls, hooks, cmdPaths, rest []string
460+
seen := map[string]bool{}
461+
462+
for _, p := range src {
463+
switch {
464+
case p == "go.mod" || p == "Makefile":
465+
// handled later to keep strict top order
466+
case strings.HasPrefix(p, "api/"):
467+
api = append(api, p)
468+
seen[p] = true
469+
case strings.HasPrefix(p, "internal/controllers/"):
470+
ctrls = append(ctrls, p)
471+
seen[p] = true
472+
case strings.HasPrefix(p, "internal/webhooks/"):
473+
hooks = append(hooks, p)
474+
seen[p] = true
475+
case strings.HasPrefix(p, "cmd/"):
476+
cmdPaths = append(cmdPaths, p)
477+
seen[p] = true
478+
default:
479+
// defer to the end
480+
}
481+
}
482+
for _, p := range src {
483+
if !seen[p] && p != "go.mod" && p != "Makefile" {
484+
rest = append(rest, p)
485+
}
486+
}
487+
488+
var out []string
489+
// go.mod and Makefile first if present
490+
for _, k := range orderFirst {
491+
for _, p := range src {
492+
if p == k {
493+
out = append(out, p)
494+
break
495+
}
496+
}
497+
}
498+
// then API, controllers, webhooks, cmd/, then the rest
499+
out = append(out, api...)
500+
out = append(out, ctrls...)
501+
out = append(out, hooks...)
502+
out = append(out, cmdPaths...)
503+
out = append(out, rest...)
504+
505+
if len(out) > limit {
506+
out = out[:limit]
507+
}
508+
return out
509+
}
510+
511+
// summarizeGoModDiff extracts only require/replace/module lines for brevity.
512+
func summarizeGoModDiff(base, head string) string {
513+
cmd := exec.Command("git", "diff", "--no-color", "--unified=0", base+".."+head, "--", "go.mod")
514+
out, _ := cmd.Output()
515+
if len(out) == 0 {
516+
return "(no go.mod summary changes detected)"
517+
}
518+
lines := strings.Split(string(out), "\n")
519+
var keep []string
520+
for _, l := range lines {
521+
t := strings.TrimSpace(l)
522+
if strings.HasPrefix(t, "+require ") || strings.HasPrefix(t, "-require ") ||
523+
strings.HasPrefix(t, "+replace ") || strings.HasPrefix(t, "-replace ") ||
524+
strings.HasPrefix(t, "+module ") || strings.HasPrefix(t, "-module ") {
525+
keep = append(keep, t)
526+
}
527+
}
528+
s := strings.Join(keep, "\n")
529+
s = strings.TrimSpace(s)
530+
if s == "" {
531+
return "(no go.mod summary changes detected)"
532+
}
533+
return s
534+
}
535+
536+
// rev prints "<ref> @ <sha7>" if ref resolves, else returns the ref.
537+
func rev(ref string) string {
538+
cmd := exec.Command("git", "rev-parse", "--short=7", ref)
539+
out, _ := cmd.Output()
540+
sha := strings.TrimSpace(string(out))
541+
if sha == "" {
542+
return ref
543+
}
544+
return fmt.Sprintf("%s @ %s", ref, sha)
545+
}

pkg/cli/alpha/internal/update/update.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,12 @@ func (opts *Update) openGitHubIssue(hasConflicts bool) error {
284284
releaseURL := fmt.Sprintf("https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%s",
285285
opts.ToVersion)
286286
fullPrompt := helpers.BuildAIPrompt(
287-
opts.FromVersion, opts.ToVersion, out, createPRURL, releaseURL,
287+
opts.FromVersion, // FROM version
288+
opts.ToVersion, // TO version
289+
opts.FromBranch, // base branch for diff
290+
out, // output branch (HEAD) for diff
291+
createPRURL, // compare URL
292+
releaseURL, // release notes
288293
changedSrc, changedGen, conflictSrc, conflictGen,
289294
)
290295

0 commit comments

Comments
 (0)