Skip to content

Introduce an initial set of DevEx features#10

Open
adamwg wants to merge 3 commits into
crossplane:mainfrom
adamwg:awg/devex
Open

Introduce an initial set of DevEx features#10
adamwg wants to merge 3 commits into
crossplane:mainfrom
adamwg:awg/devex

Conversation

@adamwg
Copy link
Copy Markdown
Member

@adamwg adamwg commented May 11, 2026

Description of your changes

This PR introduces the initial set of DevEx tools based around the concept of control plane Projects. Specifically, it introduces tools for:

  • Scaffolding a new, empty project.
  • Generating XRDs from example XRs or simpleschema documents.
  • Scaffolding compositions.
  • Scaffolding functions in go, go-templating, kcl, and python and adding them to composition pipelines.
  • Managing both runtime and build-time dependencies for a project, including generating language bindings (schemas) for added dependencies.
  • Building a project into a set of xpkgs.
  • Installing a project into a local control plane (created using kind) for testing.
  • Support for projects and embedded functions in crossplane composition render and crossplane operation render.

Fixes crossplane/crossplane#6840

I have:

Comment thread internal/docker/docker.go
const maxFileSize = 1024 * 1024 * 1024

for {
header, err := tarReader.Next()
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to reject relative paths that leave the path prefix.

@adamwg adamwg force-pushed the awg/devex branch 2 times, most recently from 3d3d93c to 23c2b56 Compare May 12, 2026 19:12
@adamwg adamwg marked this pull request as ready for review May 12, 2026 19:12
@adamwg adamwg requested review from a team, jcogilvie and tampakrap as code owners May 12, 2026 19:12
@adamwg adamwg requested review from phisco and removed request for a team May 12, 2026 19:12
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

Review Change Stack

Warning

Rate limit exceeded

@adamwg has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 36 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f7ebbdfc-ffa9-48df-9556-cbac4dcbae8f

📥 Commits

Reviewing files that changed from the base of the PR and between 66a8bbd and 6b7fc37.

⛔ Files ignored due to path filters (29)
  • cmd/crossplane/function/templates/go-templating/00-prelude.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go-templating/01-compose.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go.tar is excluded by !**/*.tar and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.lock is excluded by !**/*.lock and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.tmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/kcl/main.k is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__init__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__version__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/fn.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/main.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/pyproject.toml.tmpl is excluded by none and included by none
  • go.mod is excluded by none and included by none
  • go.sum is excluded by !**/*.sum and included by none
  • gomod2nix.toml is excluded by none and included by none
  • internal/crd/testdata/claimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/unclaimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/project/functions/testdata/go-templating-function/resource1.yaml.gotmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/go-templating-function/resource2.yaml.tmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod.lock is excluded by !**/*.lock, !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/main.k is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/account_scaffold_composition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/account_scaffold_definition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/api__v1_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/api_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/azure_linux_function_app.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/configuration_crossplane.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/runner/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
📒 Files selected for processing (88)
  • cmd/crossplane/composition/composition.go
  • cmd/crossplane/composition/generate.go
  • cmd/crossplane/composition/generate_test.go
  • cmd/crossplane/dependency/add.go
  • cmd/crossplane/dependency/auth.go
  • cmd/crossplane/dependency/cache.go
  • cmd/crossplane/dependency/dependency.go
  • cmd/crossplane/function/function.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/function/pipeline.go
  • cmd/crossplane/function/templates/python/README.md
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/init.go
  • cmd/crossplane/project/project.go
  • cmd/crossplane/project/push.go
  • cmd/crossplane/project/run.go
  • cmd/crossplane/project/stop.go
  • cmd/crossplane/render/op/cmd.go
  • cmd/crossplane/render/op/cmd_test.go
  • cmd/crossplane/render/xr/cmd.go
  • cmd/crossplane/render/xr/cmd_test.go
  • cmd/crossplane/xrd/generate.go
  • cmd/crossplane/xrd/generate_test.go
  • cmd/crossplane/xrd/xrd.go
  • internal/async/event.go
  • internal/crd/convert.go
  • internal/crd/convert_test.go
  • internal/crd/generator.go
  • internal/crd/generator_test.go
  • internal/dependency/cache_dir.go
  • internal/dependency/manager.go
  • internal/dependency/manager_test.go
  • internal/docker/docker.go
  • internal/filesystem/filesystem.go
  • internal/filesystem/filesystem_test.go
  • internal/git/git.go
  • internal/git/git_test.go
  • internal/kcl/import.go
  • internal/kcl/import_test.go
  • internal/project/build.go
  • internal/project/build_test.go
  • internal/project/certs/cert_generator.go
  • internal/project/certs/tls.go
  • internal/project/controlplane/controlplane.go
  • internal/project/functions/build.go
  • internal/project/functions/build_test.go
  • internal/project/functions/go.go
  • internal/project/functions/go_templating.go
  • internal/project/functions/kcl.go
  • internal/project/functions/python.go
  • internal/project/helm/helm.go
  • internal/project/helm/restclientgetter.go
  • internal/project/image.go
  • internal/project/install.go
  • internal/project/install_test.go
  • internal/project/projectfile/projectfile.go
  • internal/project/projectfile/projectfile_test.go
  • internal/project/push.go
  • internal/project/push_test.go
  • internal/project/render.go
  • internal/project/sort.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/go_test.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/json.go
  • internal/schemas/generator/json_test.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/kcl_test.go
  • internal/schemas/generator/python.go
  • internal/schemas/generator/python_test.go
  • internal/schemas/manager/lock.go
  • internal/schemas/manager/manager.go
  • internal/schemas/manager/manager_test.go
  • internal/schemas/manager/source.go
  • internal/schemas/manager/source_test.go
  • internal/schemas/runner/run.go
  • internal/schemas/runner/run_test.go
  • internal/terminal/spinner.go
  • internal/xpkg/client.go
  • internal/xpkg/doc.go
  • internal/xpkg/fetcher.go
  • internal/xpkg/metadata.go
  • internal/xpkg/resolver.go
  • internal/xpkg/resolver_test.go
  • internal/xrd/infer.go
  • internal/xrd/infer_test.go
📝 Walkthrough

Walkthrough

Adds Project API and types, parsing/validation, dependency and schema managers, multi-language schema generators, function builders, project build/push/run flows, local Kind dev control plane, render/composition/XRD commands, CLI wiring, utilities, and tests.

Changes

Project developer tooling and CLI

Layer / File(s) Summary
Combined PR checkpoint (all files)
apis/*, internal/*, cmd/*, generate.go
Entire PR: Project API types/defaults/validation, projectfile parsing/persistence, dependency manager and cache/CLI, schema generators (Go/JSON/KCL/Python), function builders and generation CLI/templates, project builder/packaging/pusher and CLI (init/build/push/run/stop), local dev control plane and certs/Helm helpers, render/composition/XRD tooling and tests, plus filesystem/git/docker/CRD utilities and extensive tests.

Estimated code review effort
🎯 5 (Critical) | ⏱️ ~180 minutes

Possibly related issues

Thank you — would you like this large checkpoint split into focused review passes (API & projectfile, dependency manager, schema generators, builders & packaging, control-plane)?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (21)
internal/project/certs/tls.go-153-160 (1)

153-160: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Quick sanity check on the completeness condition — should this be && instead of ||?

Comparing with the analogous check in loadOrGenerateCA at Line 103 (if len(kd) != 0 && len(cd) != 0), the server-cert check here uses ||. As written, the function will treat the secret as already complete whenever any one of tls.crt, tls.key, or ca.crt is non-empty — which seems at odds with the log message just below ("Server certificates are empty or not complete, generating a new pair...") and would cause us to skip regeneration when the secret is only partially populated (e.g., someone hand-set ca.crt).

Was the || intentional here, or should all three fields be required for "complete"? If the latter:

🐛 Proposed fix
-		if len(sec.Data[corev1.TLSCertKey]) != 0 || len(sec.Data[corev1.TLSPrivateKeyKey]) != 0 || len(sec.Data[SecretKeyCACert]) != 0 {
+		if len(sec.Data[corev1.TLSCertKey]) != 0 && len(sec.Data[corev1.TLSPrivateKeyKey]) != 0 && len(sec.Data[SecretKeyCACert]) != 0 {
 			e.log.Info("TLS secret contains server certificate.", "secret", nn.Name)
 			return nil
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/certs/tls.go` around lines 153 - 160, The completeness check
for the TLS secret currently returns early if any one of
sec.Data[corev1.TLSCertKey], sec.Data[corev1.TLSPrivateKeyKey], or
sec.Data[SecretKeyCACert] is non-empty; change this to require all three to be
present (use logical AND semantics) so the function only treats the secret as
complete when tls.crt AND tls.key AND ca.crt are non-empty (mirror the behavior
in loadOrGenerateCA) — update the condition that sets create=false and the early
return accordingly in the code around the TLS secret check.
internal/docker/docker.go-508-525 (1)

508-525: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Wrap the new filesystem/read errors with user context.

Thanks for adding these helpers. A few paths here still return raw errors (MkdirAll, Create, io.Copy, io.ReadAll), so users won't see which container path or extracted entry failed. Could we wrap those with the source path / entry name the same way the Docker calls are wrapped?

Suggested fix
 		case tar.TypeDir:
 			if err := fs.MkdirAll(cleanedPath, 0o755); err != nil {
-				return err
+				return errors.Wrapf(err, "cannot create directory %q while copying %q from container %q", cleanedPath, path, cid)
 			}
 		case tar.TypeReg:
 			outFile, err := fs.Create(cleanedPath)
 			if err != nil {
-				return err
+				return errors.Wrapf(err, "cannot create file %q while copying %q from container %q", cleanedPath, path, cid)
 			}
@@
-			if _, err := io.Copy(outFile, limitedReader); err != nil {
+			if _, err := io.Copy(outFile, limitedReader); err != nil {
 				if cerr := outFile.Close(); cerr != nil {
 					err = errors.Wrap(cerr, "error while closing file")
 				}
-				return err
+				return errors.Wrapf(err, "cannot write extracted file %q from container path %q", cleanedPath, path)
 			}
@@
-	return io.ReadAll(reader)
+	b, err := io.ReadAll(reader)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to read tarball copied from container path %q", path)
+	}
+	return b, nil

As per coding guidelines, "Use crossplane-runtime/pkg/errors for wrapping" and "Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible."

Also applies to: 541-547

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/docker/docker.go` around lines 508 - 525, Wrap all filesystem and
reader errors with user-facing context using crossplane-runtime/pkg/errors the
same way Docker call errors are wrapped: when calling fs.MkdirAll(cleanedPath),
fs.Create(cleanedPath), io.Copy(outFile, limitedReader), and any io.ReadAll
usage (lines around the io.ReadAll block), wrap returned errors with
errors.Wrapf or errors.Wrap including the tar header name (header.Name) and the
container/source path being extracted, and preserve the existing close-error
wrapping for outFile.Close; reference the symbols fs.MkdirAll, fs.Create,
tarReader, limitedReader, io.Copy, io.ReadAll, outFile.Close, header.Name and
maxFileSize so the new error messages show which archive entry/path failed and
include actionable context.
internal/docker/docker.go-492-523 (1)

492-523: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on oversized tar entries to prevent silent corruption.

The current code uses io.LimitReader to cap bytes read from the tar stream, but this causes files larger than 1GiB to be silently truncated. Since io.Copy stops at EOF (not an error), the function returns nil even though the extracted file is incomplete and corrupted. This could lead to downstream packaging using broken artifacts without knowing.

Adding an upfront check on header.Size and rejecting oversized entries is the right approach:

Suggested fix
 	// Limit files to 1GiB to avoid excessive memory usage.
 	const maxFileSize = 1024 * 1024 * 1024
@@
 		case tar.TypeReg:
+			if header.Size > maxFileSize {
+				return errors.Errorf("cannot copy %q from container path %q: file exceeds 1GiB limit", header.Name, path)
+			}
 			outFile, err := fs.Create(cleanedPath)
 			if err != nil {
 				return err
 			}
-
-			limitedReader := io.LimitReader(tarReader, maxFileSize)
-			if _, err := io.Copy(outFile, limitedReader); err != nil {
+			if _, err := io.Copy(outFile, tarReader); err != nil {
 				if cerr := outFile.Close(); cerr != nil {
 					err = errors.Wrap(cerr, "error while closing file")
 				}
 				return err
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/docker/docker.go` around lines 492 - 523, The tar extraction
silently truncates large files; update the loop handling tar.TypeReg in the
function reading from tarReader to first check header.Size against maxFileSize
and return a clear error if header.Size > maxFileSize (include header.Name and
header.Size in the message), before creating outFile or using io.LimitReader;
keep the existing use of maxFileSize, header, cleanedPath, outFile and tarReader
but fail fast on oversized entries to avoid producing corrupted files.
internal/docker/docker.go-507-525 (1)

507-525: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve file and directory modes when extracting from container tarballs.

Files copied from containers currently lose their permission bits. When CopyFromContainer extracts a tar archive, directories are hardcoded to 0o755 and regular files get default mode via fs.Create(), which discards the header.Mode from the tar entry. This breaks executable binaries or scripts that need specific permissions to function correctly.

The tar header provides the original file mode—use os.FileMode(header.Mode).Perm() to extract the permission bits and apply them when creating files and directories with fs.OpenFile() and fs.MkdirAll().

Suggested fix
+	"os"
@@
 		switch header.Typeflag {
 		case tar.TypeDir:
-			if err := fs.MkdirAll(cleanedPath, 0o755); err != nil {
+			if err := fs.MkdirAll(cleanedPath, os.FileMode(header.Mode).Perm()); err != nil {
 				return err
 			}
 		case tar.TypeReg:
-			outFile, err := fs.Create(cleanedPath)
+			if err := fs.MkdirAll(filepath.Dir(cleanedPath), 0o755); err != nil {
+				return err
+			}
+			outFile, err := fs.OpenFile(cleanedPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode).Perm())
 			if err != nil {
 				return err
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/docker/docker.go` around lines 507 - 525, The extraction code
currently hardcodes directory mode (0o755) and uses fs.Create for files, losing
permission bits from the tar header; update the tar.TypeDir and tar.TypeReg
branches to preserve header.Mode by converting header.Mode to
os.FileMode(header.Mode).Perm() and passing that mode into fs.MkdirAll and
fs.OpenFile (use os.O_CREATE|os.O_WRONLY|os.O_TRUNC) instead of fs.Create; after
writing, ensure the file is closed and if the filesystem abstraction doesn't
accept modes on create, call fs.Chmod(cleanedPath,
os.FileMode(header.Mode).Perm()) to set the correct permissions (referencing
cleanedPath, header.Mode, tar.TypeDir, tar.TypeReg, maxFileSize, fs.MkdirAll,
fs.OpenFile, fs.Create).
cmd/crossplane/project/stop.go-47-50 (1)

47-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap errors with command context and preserve the parse cause.

Thanks for adding this command — one blocker: Line 49 and Line 66 return raw errors, and Line 57 masks all parse failures as “not a project directory”. That makes troubleshooting harder for users (e.g., malformed YAML vs missing file).

Suggested fix
 	projFilePath, err := filepath.Abs(c.ProjectFile)
 	if err != nil {
-		return err
+		return errors.Wrapf(err, "cannot resolve project definition path %q", c.ProjectFile)
 	}
@@
 	prj, err := projectfile.Parse(projFS, projFileName)
 	if err != nil {
-		return errors.New("this is not a project directory; use --control-plane-name to specify the control plane name")
+		return errors.Wrapf(err, "cannot read project definition %q; run this command from a project directory or use --control-plane-name", projFilePath)
 	}
@@
 	if err := sp.WrapWithSuccessSpinner("Tearing down control plane", func() error {
 		return controlplane.TeardownLocalDevControlPlane(ctx, name, c.RegistryDir)
 	}); err != nil {
-		return err
+		return errors.Wrap(err, "cannot tear down local dev control plane")
 	}
As per coding guidelines, "`**/*.go`: Use crossplane-runtime/pkg/errors for wrapping... Ensure all error messages are meaningful to end users, include context, and suggest next steps."

Also applies to: 55-58, 63-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/stop.go` around lines 47 - 50, Wrap and annotate raw
errors from filepath.Abs(c.ProjectFile) and the project parsing logic using
crossplane-runtime/pkg/errors so the original causes are preserved and users get
actionable context; e.g., replace returns like "return err" with
errors.Wrapf(err, "failed to resolve absolute path for project file %q",
c.ProjectFile) for the filepath.Abs call, and for the project parse/validate
code detect os.IsNotExist to return a clear "project file not found" message but
otherwise return errors.Wrapf(parseErr, "failed to parse project file %q",
c.ProjectFile) so malformed YAML and other parse failures are preserved and
surfaced rather than being masked as "not a project directory".
internal/project/functions/go.go-55-128 (1)

55-128: ⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Global logger modification could affect concurrent builds.

Line 58 modifies the global log package's output with log.SetOutput(io.Discard). If multiple goroutines are building Go functions concurrently, this could cause race conditions or unexpected log suppression in other parts of the system that use the standard logger.

Consider whether:

  1. This builder is guaranteed to run in isolation (no concurrent builds)
  2. If concurrent builds are possible, whether ko provides a way to configure logging per-builder instance, or if you should protect this with a mutex
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/functions/go.go` around lines 55 - 128, The code currently
silences the global logger with log.SetOutput(io.Discard) inside goBuilder.Build
which can race with other concurrent builds; instead avoid mutating the global
logger. Fix by either (A) using a per-builder logger option if available when
calling build.NewGo (check for any build.WithLogger / WithLog option) or (B) if
no per-builder option exists, wrap the global suppression with a mutex and
restore the previous writer: capture old := log.Writer(), lock a package-level
mutex, call log.SetOutput(io.Discard) before invoking builder (build.NewGo /
builder.Build), defer restoring with log.SetOutput(old) and unlock in the defer,
or simply remove the log.SetOutput call if builds run in shared context. Ensure
changes target the log.SetOutput usage in goBuilder.Build and the
build.NewGo/builder.Build call sites.
internal/project/functions/python.go-104-107 (1)

104-107: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Build the Python venv per target architecture.

Thanks for adding the Python builder. The current flow builds /fn once, before the architecture loop, then reuses that same tarball for every output image. That means an arm64 image can still contain the host/build-container’s amd64 venv, entrypoints, and any native wheels, so multi-arch outputs will fail at runtime. Could we make the build step architecture-aware and run it inside each per-arch branch instead of reusing one venvTar?

Also applies to: 123-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/functions/python.go` around lines 104 - 107, The venv is
currently built once (venvTar := b.buildVenv(ctx, c)) before the architecture
loop and reused for all arches; change the flow so buildVenv is invoked inside
the per-architecture branch instead of once globally: move the call to
b.buildVenv(ctx, c) into the loop that handles each target architecture (the
same place where you prepare per-arch image outputs) so each architecture gets
its own venvTar built in that architecture context; update any variables
(venvTar, err) scoping accordingly and ensure the per-arch error handling
mirrors the original pattern.
cmd/crossplane/dependency/add.go-116-131 (1)

116-131: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject --git-path unless --git-ref is set.

Right now --git-path alone is silently ignored because the code falls through to xpkg parsing. This should return a clear validation error so users don’t add the wrong dependency type by accident.

As per coding guidelines, "**/cmd/**: Review CLI commands for proper flag handling, help text, and error messages."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/dependency/add.go` around lines 116 - 131, Add explicit
validation to reject a provided GitPath when GitRef is not set: before falling
through to xpkg parsing (near the block that checks c.GitRef and constructs
v1alpha1.Dependency with v1alpha1.GitDependency), add a check like if c.GitPath
!= "" && c.GitRef == "" then return an error (e.g. "git-path requires git-ref to
be set") so --git-path alone no longer gets silently ignored; update any error
messages to clearly instruct the user to set --git-ref when using --git-path.
internal/project/render.go-49-55 (1)

49-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter to function dependencies before creating pkgv1.Function objects.

Current logic includes every non-API-only xpkg dependency. If a project has provider/configuration xpkgs, this generates invalid Function manifests. Please gate on dependency metadata (kind/apiVersion) so only function packages are converted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/render.go` around lines 49 - 55, The loop currently converts
every non-API-only xpkg into pkgv1.Functions; change the guard in the
proj.Spec.Dependencies iteration (the block checking dep.Type ==
DependencyTypeXpkg and dep.Xpkg != nil && !dep.Xpkg.APIOnly) to additionally
verify the package metadata identifies a function package (e.g., check
dep.Xpkg.Kind and dep.Xpkg.APIVersion or equivalent fields) and only proceed to
create pkgv1.Function objects when those metadata values match the expected
function kind/apiVersion; update the conditional that leads to Function
construction so provider/configuration xpkgs are skipped.
apis/dev/v1alpha1/validate.go-121-123 (1)

121-123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate that type matches the populated source field.

sourceCount == 1 is necessary but not sufficient. A dependency can currently declare type: xpkg while only git is set, and still pass validation. Please enforce exact type-to-source consistency to prevent invalid API objects.

As per coding guidelines, "**/apis/**: Focus on API design following Kubernetes API conventions... Ensure error messages in validation are user-friendly. Pay attention to API consistency."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apis/dev/v1alpha1/validate.go` around lines 121 - 123, sourceCount only
ensures one source is set but doesn't verify the declared Type matches that
populated source; after the existing sourceCount check, switch on the
dependency's Type (field Type) and validate the corresponding source field
(Xpkg, Git, Http, K8s) is the one populated (e.g., if Type == "xpkg" then Xpkg
must be non-nil/non-empty and Git/Http/K8s must be empty), appending
user-friendly errors to errs via errors.New when mismatches occur; also handle
unknown Type values with a clear error message so validation fails for
inconsistent or invalid type-to-source combinations.
cmd/crossplane/project/run.go-302-306 (1)

302-306: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t swallow all kubeconfig load errors.

Line 302 currently treats any LoadFromFile error as “file missing” and starts fresh, which can overwrite a real kubeconfig after permission/parse failures. Please only fall back on os.IsNotExist(err) and return other errors with context.

Suggested fix
+import "os"
...
  existing, err := clientcmd.LoadFromFile(defaultPath)
  if err != nil {
-   // If the file doesn't exist, start fresh.
-   existing = clientcmdapi.NewConfig()
+   if os.IsNotExist(err) {
+     existing = clientcmdapi.NewConfig()
+   } else {
+     return errors.Wrapf(err, "cannot read kubeconfig at %q", defaultPath)
+   }
  }

As per coding guidelines, "**/cmd/**: Review CLI commands for proper flag handling, help text, and error messages... CLI error messages must be especially user-friendly - avoid internal error details, provide actionable guidance."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/run.go` around lines 302 - 306, The code currently
treats any error from clientcmd.LoadFromFile(defaultPath) as "file missing" and
silently creates a new config; instead check the error with os.IsNotExist(err)
and only then set existing = clientcmdapi.NewConfig(); for all other errors
return a wrapped/contextual error (e.g., "failed to load kubeconfig at
defaultPath: <err>") so permission or parse failures are propagated. Locate the
call to clientcmd.LoadFromFile and update the error branch to use
os.IsNotExist(err) and return the error otherwise, leaving the existing variable
and clientcmdapi.NewConfig() logic intact.
cmd/crossplane/dependency/add.go-156-165 (1)

156-165: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use name.NewTag() to properly parse xpkg references with registry ports.

The current strings.Cut(c.Package, ":") approach breaks valid references like example.com:5000/repo:v1.2.3 by splitting on the first colon instead of the last one. Since go-containerregistry/pkg/name is already imported and used throughout the codebase, consider trying name.NewTag() before falling back to strings.Cut(). This follows the pattern already established in internal/xpkg/resolver.go.

As a secondary concern, --git-path is silently ignored when --git-ref is not provided. Per the CLI guidelines, adding a validation error would improve user experience: "git-path is only valid with git-ref".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/dependency/add.go` around lines 156 - 165, Replace the fragile
strings.Cut(c.Package, ":") parsing with a try-first name.NewTag(c.Package)
parse (as used in internal/xpkg/resolver.go) to correctly handle registry
host:port forms; if name.NewTag succeeds, extract the repository part into pkg
and the tag into version, otherwise fall back to the existing strings.Cut
behavior. Also add CLI validation in the add command to error when git-path is
provided without git-ref (e.g., in the command handling that reads c.GitPath /
c.GitRef) and return a clear validation error "git-path is only valid with
git-ref".
internal/project/push.go-55-58 (1)

55-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against zero concurrency to avoid deadlock

Could we clamp concurrency to at least 1? Right now, if n == 0, Line 120 creates a zero-capacity semaphore and every worker blocks on Line 123.

Suggested fix
func PushWithMaxConcurrency(n uint) PusherOption {
  return func(p *realPusher) {
-   p.maxConcurrency = n
+   if n == 0 {
+     p.maxConcurrency = 1
+     return
+   }
+   p.maxConcurrency = n
  }
}

Also applies to: 120-126

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/push.go` around lines 55 - 58, Clamp concurrency to at least
1: in PushWithMaxConcurrency set p.maxConcurrency = 1 when n == 0 (e.g.,
p.maxConcurrency = uint(math.Max(1, int(n))) or a simple if n == 0 {
p.maxConcurrency = 1 } else { p.maxConcurrency = n }). Also ensure the semaphore
creation that reads p.maxConcurrency (the code that builds the
semaphore/capacity used by the workers in realPusher) uses max(1,
p.maxConcurrency) so you never create a zero-capacity semaphore; update both
PushWithMaxConcurrency and the semaphore-instantiation site accordingly.
internal/dependency/manager.go-129-137 (1)

129-137: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Prevent nil-pointer panics by enforcing manager invariants

Could we make NewManager guarantee non-nil proj.Spec.Paths, resolver, and client (or fail fast with a clear error)? As written, Line 132 / Line 166 / Line 186 can panic if any of these are unset.

As per coding guidelines, "**/*.go: ... Ensure all error messages are meaningful to end users ... include context about what the user was trying to do, and suggest next steps when possible."

Also applies to: 165-189

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/dependency/manager.go` around lines 129 - 137, NewManager must
validate required inputs and fail fast with clear errors: change NewManager
signature to return (*Manager, error), then at the start validate proj != nil
and that proj.Spec.Paths is non-nil (and that proj.Spec.Paths.Schemas is set)
before creating schemaFS; validate that provided ManagerOption values produce
non-nil resolver and client (or set safe defaults) before using them; if any
invariant is missing, return a descriptive error (e.g., "missing project paths:
set Project.Spec.Paths.Schemas" or "missing resolver/client: provide via
ManagerOption") rather than letting code dereference nils; update managerOptions
handling to surface these errors when constructing Manager (symbols: NewManager,
managerOptions, proj.Spec.Paths, schemaFS, resolver, client).
internal/schemas/generator/kcl.go-230-243 (1)

230-243: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close opened files during KCL transform walk

Thanks for this transform flow. Could we close both srcFile and destFile after copy? Right now Lines 230 and 235 open handles that are never closed, which can exhaust descriptors on larger inputs.

Suggested fix
srcFile, err := fs.Open(path)
if err != nil {
  return errors.Wrapf(err, "failed to open source file %s", path)
}
+defer func() { _ = srcFile.Close() }()

destFile, err := fs.Create(newFilePath)
if err != nil {
  return errors.Wrapf(err, "failed to create destination file %s", newFilePath)
}
+defer func() { _ = destFile.Close() }()

_, err = io.Copy(destFile, srcFile)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/kcl.go` around lines 230 - 243, The srcFile and
destFile opened in the KCL transform (calls to fs.Open and fs.Create in
generator/kcl.go around the io.Copy) are not closed and can leak file
descriptors; after successfully opening each file, ensure you close them (use
defer srcFile.Close() and defer destFile.Close() immediately after their
nil-error checks) and optionally check/return any Close errors (wrap with
errors.Wrapf) after io.Copy to surface close failures.
internal/schemas/generator/go.go-999-1000 (1)

999-1000: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use crossplane-runtime error wrapping consistently

Lines 999-1000 and 1023-1025 use fmt.Errorf while the rest of the file uses errors.Wrap/errors.Wrapf. Can we align these with the rest of the codebase for consistency? The error messages could also be more descriptive—consider matching the pattern used elsewhere in the file (e.g., "failed to parse Go code" on line 1035) to provide clearer context to users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/go.go` around lines 999 - 1000, Replace the
fmt.Errorf usages in go.go (the return "", fmt.Errorf("parsing failed: %w", err)
call and the other fmt.Errorf blocks around lines ~1023-1025) with
crossplane-runtime style errors.Wrap/errors.Wrapf to match the rest of the file;
use a clearer message consistent with existing patterns (e.g., errors.Wrap(err,
"failed to parse Go code") or errors.Wrapf(err, "failed to parse Go code: %s",
someContext)) so the wrapped error preserves the original error while providing
the same descriptive context used elsewhere.
internal/schemas/generator/python.go-290-305 (1)

290-305: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Create destination directory before copying v1.py

Thanks for the transform logic here. Could we add MkdirAll(destDir, ...) before Line 303? Without it, writing targetDir/.../v1.py can fail when the directory hasn’t been created yet.

Suggested fix
destDir := filepath.Join(targetDir, "io", "k8s", "apimachinery", "pkg", "apis", "meta")
destPath := filepath.Join(destDir, "v1.py")
+if err := fs.MkdirAll(destDir, os.ModePerm); err != nil {
+  return errors.Wrapf(err, "failed to create directory %s", destDir)
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/python.go` around lines 290 - 305, The code copies
v1.py into destPath but doesn't ensure the destination directory exists; before
writing (prior to the afero.WriteFile call) call MkdirAll on destDir using the
provided afero FS and appropriate permissions (e.g., 0755) to create the
directory hierarchy; locate the block handling destDir/destPath in the function
containing the ReadFile/Stat/WriteFile sequence and insert an afero.MkdirAll(fs,
destDir, 0755) (or equivalent) and handle/return any error from that call.
internal/project/build.go-504-515 (1)

504-515: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scan every YAML document before dropping the file.

yaml.Unmarshal into TypeMeta only inspects the first document in a multi-doc manifest. That means a file whose later document is an XRD, Composition, or Operation gets skipped entirely, which will silently omit valid resources from the built package.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/build.go` around lines 504 - 515, The current logic uses
yaml.Unmarshal into metav1.TypeMeta (u) which only checks the first YAML
document, causing multi-doc manifests to be skipped; change the check to iterate
over all YAML documents in the file (e.g., use
yaml.NewDecoder(bytes.NewReader(bs)) or a YAML decoder that supports multiple
docs) and for each decoded metav1.TypeMeta call u.GroupVersionKind().String()
and test slices.Contains(gvks, ...) so the file is only dropped if none of the
documents match; update the code around bs, yaml.Unmarshal, metav1.TypeMeta (u),
and the slices.Contains(gvks, u.GroupVersionKind().String()) check accordingly.
internal/project/controlplane/controlplane.go-504-512 (1)

504-512: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Please reapply and wait for registry trust configuration before returning.

On the fast path, this returns as soon as an existing registry container is restarted, so a newly created/reused kind cluster never gets /etc/containerd/certs.d populated. On the slow path, configureContainerdLocalRegistry returns right after creating a fixed-name Job, so the control plane can be reported ready before trust is installed, and later runs won't rerun once that Job already exists. Could we make this step idempotent and wait for Job completion before reporting readiness?

Also applies to: 554-556, 637-641

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/controlplane/controlplane.go` around lines 504 - 512, The
fast-path early return after restarting an existing registry container (when
docker.StartContainerByID returns successfully) must not return immediately —
call configureContainerdLocalRegistry with the same context/inputs and wait for
the registry-trust Job to finish before returning; make
configureContainerdLocalRegistry idempotent (create-or-get) and add a
deterministic wait/timeout path that polls the Job status (or uses a new
waitForJobCompletion helper) so repeated runs do nothing if trust is already
installed. Apply the same change to the slow-path return site (where the
fixed-name Job is created) so that instead of returning right after Job creation
the code waits for Job completion (using the idempotent create-or-wait behavior)
before reporting readiness; reference functions/variables:
docker.StartContainerByID, configureContainerdLocalRegistry, certDir,
certSecret, and the fixed-name Job handling to locate the changes.
internal/project/build.go-89-92 (1)

89-92: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp maxConcurrency away from zero.

If a caller sets this to 0, the semaphore in buildFunctions becomes unbuffered and every worker blocks forever on the first send at Line 333. Could we treat 0 as 1 or reject it explicitly here?

💡 Minimal fix
 func BuildWithMaxConcurrency(n uint) BuilderOption {
 	return func(b *realBuilder) {
+		if n == 0 {
+			n = 1
+		}
 		b.maxConcurrency = n
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/build.go` around lines 89 - 92, BuildWithMaxConcurrency
currently assigns b.maxConcurrency directly, allowing 0 which makes the
semaphore in buildFunctions unbuffered and deadlock; change
BuildWithMaxConcurrency to clamp zero to one by setting b.maxConcurrency = 1
when n == 0 (i.e., replace the direct assignment with a conditional that sets 1
for n==0, otherwise n) so realBuilder.maxConcurrency is never zero and the
semaphore remains buffered. Ensure you update any related comments/tests that
assume zero is allowed.
internal/project/controlplane/controlplane.go-520-530 (1)

520-530: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Please preserve the underlying filesystem error here.

These errors.New(...) returns drop whether setup failed because of permissions, a bad mount, disk space, etc. Wrapping the original err with user-facing context would make local registry failures much easier to debug.

💡 Example
 	if err := os.MkdirAll(certDir, 0o755); err != nil { //nolint:gosec // Container needs to read the dir.
-		return "", errors.New("failed to create cert directory")
+		return "", errors.Wrap(err, "failed to prepare TLS files for the local registry")
 	}
 	if err := os.WriteFile(filepath.Join(certDir, "ca.crt"), certSecret.Data[certs.SecretKeyCACert], 0o644); err != nil { //nolint:gosec // Container needs to read the file.
-		return "", errors.New("failed to write ca cert")
+		return "", errors.Wrap(err, "failed to write the local registry CA certificate")
 	}
 	if err := os.WriteFile(filepath.Join(certDir, "tls.crt"), certSecret.Data[corev1.TLSCertKey], 0o644); err != nil { //nolint:gosec // Container needs to read the file.
-		return "", errors.New("failed to write tls cert")
+		return "", errors.Wrap(err, "failed to write the local registry TLS certificate")
 	}
 	if err := os.WriteFile(filepath.Join(certDir, "tls.key"), certSecret.Data[corev1.TLSPrivateKeyKey], 0o644); err != nil { //nolint:gosec // Container needs to read the file.
-		return "", errors.New("failed to write tls key")
+		return "", errors.Wrap(err, "failed to write the local registry TLS private key")
 	}

As per coding guidelines, "Use crossplane-runtime/pkg/errors for wrapping" and "Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/controlplane/controlplane.go` around lines 520 - 530, The
current writes in controlplane.go drop the underlying filesystem error; update
the error returns in the os.MkdirAll and os.WriteFile calls that use certDir and
certSecret (keys certs.SecretKeyCACert, corev1.TLSCertKey,
corev1.TLSPrivateKeyKey) to wrap the original err using
crossplane-runtime/pkg/errors (e.g., errors.Wrapf or errors.Wrap) and include
contextual details like the target path (filepath.Join(certDir, "...")) and a
brief user-facing hint (e.g., check permissions/mount/disk space) so the
returned error preserves the original error and gives meaningful next steps.
🟡 Minor comments (9)
internal/project/certs/cert_generator.go-61-63 (1)

61-63: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Doc comment doesn't quite match what Generate actually does — could we tighten it up?

The comment says this "creates TLS Secret with 10 years expiration date that is valid for the given domains", but the function:

  • returns raw PEM bytes (no Kubernetes Secret involved),
  • doesn't set the expiration (NotAfter is whatever the caller put on cert),
  • doesn't set DNS names (also driven by the caller's cert).

It looks like the doc was likely copy/pasted from a higher-level caller. Mind updating it to describe what this method really does so future readers don't get misled? Thanks!

📝 Suggested doc tweak
-// Generate creates TLS Secret with 10 years expiration date that is valid
-// for the given domains.
+// Generate produces a PEM-encoded RSA private key and an X.509 certificate
+// built from the given template. If signer is nil, the certificate is
+// self-signed using the generated key.
 func (*CertGenerator) Generate(cert *x509.Certificate, signer *CertificateSigner) (key []byte, crt []byte, err error) {

As per coding guidelines: "Ensure all error messages are meaningful to end users… include context about what the user was trying to do" — the same spirit applies to doc comments documenting exported APIs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/certs/cert_generator.go` around lines 61 - 63, Update the
doc comment for CertGenerator.Generate to accurately describe its behavior:
state that Generate takes an x509.Certificate and a *CertificateSigner and
returns PEM-encoded private key and certificate bytes (key, crt) signed by the
provided signer, and an error if signing fails; explicitly note it does not
create a Kubernetes Secret nor modify cert fields such as NotAfter or DNSNames —
those are taken from the cert argument provided by the caller. Mention the
returned byte slices are PEM-encoded and that expiration/DNS names are
controlled by the input cert.
internal/kcl/import.go-73-73 (1)

73-73: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider adding a nil guard for defensive robustness.

Line 73 would panic if existingAliases is nil. While all current call sites safely initialize the map, adding a guard makes this helper more resilient for future callers. The fix is straightforward and follows defensive programming practices.

Suggested approach
 func FormatKclImportPath(path string, existingAliases map[string]bool) (string, string) {
+	if existingAliases == nil {
+		existingAliases = map[string]bool{}
+	}
 	path = filepath.ToSlash(path)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/kcl/import.go` at line 73, The assignment existingAliases[alias] =
true can panic if existingAliases is nil; add a nil guard in the helper so that
before writing you ensure the map is non-nil (e.g., if existingAliases == nil {
existingAliases = make(map[string]bool) } or initialize it on the
receiver/return path) so writes are safe; update the helper in
internal/kcl/import.go that manipulates existingAliases to perform this
check/initialization prior to the assignment to avoid panics for future callers.
cmd/crossplane/project/build.go-58-61 (1)

58-61: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap error with user-friendly context.

The error from filepath.Abs is returned without context, making it unclear to users what went wrong.

Suggested fix
 	projFilePath, err := filepath.Abs(c.ProjectFile)
 	if err != nil {
-		return err
+		return errors.Wrapf(err, "cannot resolve project file path %q", c.ProjectFile)
 	}

As per coding guidelines: "CLI error messages must be especially user-friendly - avoid internal error details, provide actionable guidance."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/build.go` around lines 58 - 61, The call to
filepath.Abs(c.ProjectFile) returns an error directly; update the surrounding
code (where projFilePath is assigned) to wrap that error with a clear,
user-friendly message that names the problematic input (c.ProjectFile) and
suggests next steps (e.g., "check the project file path"). Use the project's
error-wrapping convention (e.g., fmt.Errorf or errors.Wrap) to return a concise,
non-technical message instead of the raw filepath.Abs error so callers of the
function see actionable guidance.
cmd/crossplane/composition/generate.go-66-69 (1)

66-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap error with user-friendly context.

The error from filepath.Abs is returned without context. Users won't understand what went wrong or which file path caused the issue.

Suggested fix
 	projFilePath, err := filepath.Abs(c.ProjectFile)
 	if err != nil {
-		return err
+		return errors.Wrapf(err, "cannot resolve project file path %q", c.ProjectFile)
 	}

As per coding guidelines: "Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/composition/generate.go` around lines 66 - 69, Wrap the error
returned by filepath.Abs when computing projFilePath with a user-friendly
message that includes the offending path (c.ProjectFile) and the operation being
attempted (resolving an absolute path), e.g., replace the direct return of err
after projFilePath, err := filepath.Abs(c.ProjectFile) with returning a
formatted error using fmt.Errorf or errors.Wrapf that says something like
"failed to resolve absolute path for project file '%s': %w" including
c.ProjectFile and the original err so users see both context and the underlying
cause.
cmd/crossplane/project/build.go-98-105 (1)

98-105: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap error with user-friendly context.

The error from clixpkg.NewClient is returned without context. This is the same issue as in cmd/crossplane/composition/generate.go.

Suggested fix
 	client, err := clixpkg.NewClient(
 		clixpkg.NewRemoteFetcher(),
 		clixpkg.WithCacheDir(afero.NewOsFs(), cacheDir),
 		clixpkg.WithImageConfigs(c.proj.Spec.ImageConfigs),
 	)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "cannot initialize package client")
 	}

As per coding guidelines: "CLI error messages must be especially user-friendly."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/build.go` around lines 98 - 105, The call to
clixpkg.NewClient returns raw errors without user-friendly context; update the
error handling in the block that constructs the client (the clixpkg.NewClient
call using clixpkg.NewRemoteFetcher(), cacheDir, and c.proj.Spec.ImageConfigs)
to wrap the returned err with a clear, actionable message (for example using
fmt.Errorf("creating client for fetching images: %w", err) or errors.Wrapf) so
the CLI surfaces "creating client..." context to the user before returning the
wrapped error.
internal/project/functions/go_templating.go-93-151 (1)

93-151: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Errgroup context is not used in goroutines.

Line 111 creates an errgroup with context using errgroup.WithContext(ctx), but the returned context is discarded (assigned to _). The goroutines should use the errgroup's context so they can be canceled if one fails:

Suggested fix
 	images := make([]v1.Image, len(c.Architectures))
-	eg, _ := errgroup.WithContext(ctx)
+	eg, egCtx := errgroup.WithContext(ctx)
 	for i, arch := range c.Architectures {
 		eg.Go(func() error {
-			baseImg, err := baseImageForArch(baseRef, arch, b.transport)
+			baseImg, err := baseImageForArch(egCtx, baseRef, arch, b.transport)

Note: This assumes baseImageForArch accepts a context parameter. If it doesn't, this is less critical, but the context should still be passed to any functions that accept it for proper cancellation propagation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/functions/go_templating.go` around lines 93 - 151, The
errgroup context returned by errgroup.WithContext(ctx) is currently discarded;
change the call to capture the derived context (e.g., eg, egCtx :=
errgroup.WithContext(ctx)) and use egCtx inside each goroutine so cancellation
propagates (pass egCtx into calls that accept context such as baseImageForArch
and any context-aware filesystem helpers). Update the goroutine closure in Build
to use egCtx and ensure any functions that can take a context (baseImageForArch,
filesystem.FSToTar if available) receive it; keep using the same eg variable for
launching goroutines and call eg.Wait() as before.
cmd/crossplane/composition/generate.go-85-92 (1)

85-92: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap error with user-friendly context.

The error from clixpkg.NewClient is returned without wrapping. Users need context about what operation failed (setting up package cache or authentication).

Suggested fix
 	client, err := clixpkg.NewClient(
 		clixpkg.NewRemoteFetcher(),
 		clixpkg.WithCacheDir(afero.NewOsFs(), cacheDir),
 		clixpkg.WithImageConfigs(proj.Spec.ImageConfigs),
 	)
 	if err != nil {
-		return err
+		return errors.Wrap(err, "cannot initialize package client")
 	}

As per coding guidelines: "Ensure all error messages are meaningful to end users, not just developers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/composition/generate.go` around lines 85 - 92, The call to
clixpkg.NewClient returns an unwrapped error; update the error handling around
clixpkg.NewClient (the NewClient call that takes clixpkg.NewRemoteFetcher(),
clixpkg.WithCacheDir(afero.NewOsFs(), cacheDir),
clixpkg.WithImageConfigs(proj.Spec.ImageConfigs)) to wrap the returned err with
a user-friendly message (e.g., "failed to initialize package client: could not
set up package cache or authenticate") before returning so callers get
actionable context.
cmd/crossplane/render/xr/cmd.go-464-466 (1)

464-466: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve non-ENOENT failures when probing ProjectFile.

Could we handle os.IsNotExist(err) separately here as well? Right now any os.Stat failure becomes “functions argument is required when not in a project”, which is misleading if the project file exists but is unreadable or otherwise invalid to stat. Wrapping the real error with the file path would make this much easier to diagnose for CLI users.

As per coding guidelines **/cmd/**: CLI error messages must be especially user-friendly - avoid internal error details, provide actionable guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/render/xr/cmd.go` around lines 464 - 466, The current
os.Stat(projFilePath) failure always returns the generic "functions argument is
required when not in a project" error; change the logic in the block that calls
os.Stat(projFilePath) to check os.IsNotExist(err) first and only return the
generic guidance when the file truly does not exist, and for any other error
return a wrapped/error-with-context result (e.g. fmt.Errorf("stat %s: %w",
projFilePath, err)) so callers see the real problem; reference projFilePath,
os.Stat and os.IsNotExist in your fix and ensure the returned message remains
user-friendly for the CLI.
cmd/crossplane/render/op/cmd.go-403-405 (1)

403-405: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve non-ENOENT failures when probing ProjectFile.

Thanks for wiring the project-aware fallback. One thing to tighten up here: os.Stat can fail for reasons other than “not found” (for example permissions or an unreadable parent), but this branch currently rewrites all of them to “functions argument is required”. That hides the real problem and points users at the wrong fix. Could we special-case os.IsNotExist(err) and wrap any other error with the project file path, ideally mentioning --functions / --project-file in the not-found case?

As per coding guidelines **/cmd/**: CLI error messages must be especially user-friendly - avoid internal error details, provide actionable guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/render/op/cmd.go` around lines 403 - 405, The
os.Stat(projFilePath) error handling currently replaces all errors with a
generic "functions argument is required" message; instead, in the block that
checks os.Stat(projFilePath) (where projFilePath is probed), special-case
missing-file with os.IsNotExist(err) and return a user-friendly message that
suggests using --functions or --project-file, and for any other os.Stat error
preserve and wrap the original error (e.g., using fmt.Errorf("stat %s: %w",
projFilePath, err)) so permission/IO errors are not hidden; update the error
returns in that section accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 419e2fce-109c-481d-9316-97ba0c2d3063

📥 Commits

Reviewing files that changed from the base of the PR and between 6e3542b and 23c2b56.

⛔ Files ignored due to path filters (31)
  • apis/dev/v1alpha1/zz_generated.deepcopy.go is excluded by !**/zz_generated*.go and included by **/*.go
  • cmd/crossplane/function/templates/go-templating/00-prelude.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go-templating/01-compose.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go.tar is excluded by !**/*.tar and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.lock is excluded by !**/*.lock and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.tmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/kcl/main.k is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__init__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__version__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/fn.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/main.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/pyproject.toml.tmpl is excluded by none and included by none
  • go.mod is excluded by none and included by none
  • go.sum is excluded by !**/*.sum and included by none
  • gomod2nix.toml is excluded by none and included by none
  • hack/boilerplate.go.txt is excluded by none and included by none
  • internal/crd/testdata/claimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/unclaimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/project/functions/testdata/go-templating-function/resource1.yaml.gotmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/go-templating-function/resource2.yaml.tmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod.lock is excluded by !**/*.lock, !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/main.k is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/account_scaffold_composition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/account_scaffold_definition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/api__v1_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/api_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/azure_linux_function_app.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/configuration_crossplane.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/runner/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
📒 Files selected for processing (94)
  • apis/dev/v1alpha1/defaults.go
  • apis/dev/v1alpha1/doc.go
  • apis/dev/v1alpha1/project_types.go
  • apis/dev/v1alpha1/validate.go
  • apis/dev/v1alpha1/validate_test.go
  • cmd/crossplane/composition/composition.go
  • cmd/crossplane/composition/generate.go
  • cmd/crossplane/composition/generate_test.go
  • cmd/crossplane/dependency/add.go
  • cmd/crossplane/dependency/auth.go
  • cmd/crossplane/dependency/cache.go
  • cmd/crossplane/dependency/dependency.go
  • cmd/crossplane/function/function.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/function/pipeline.go
  • cmd/crossplane/function/templates/python/README.md
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/init.go
  • cmd/crossplane/project/project.go
  • cmd/crossplane/project/push.go
  • cmd/crossplane/project/run.go
  • cmd/crossplane/project/stop.go
  • cmd/crossplane/render/op/cmd.go
  • cmd/crossplane/render/op/cmd_test.go
  • cmd/crossplane/render/xr/cmd.go
  • cmd/crossplane/render/xr/cmd_test.go
  • cmd/crossplane/xrd/generate.go
  • cmd/crossplane/xrd/generate_test.go
  • cmd/crossplane/xrd/xrd.go
  • generate.go
  • internal/async/event.go
  • internal/crd/convert.go
  • internal/crd/convert_test.go
  • internal/crd/generator.go
  • internal/crd/generator_test.go
  • internal/dependency/cache_dir.go
  • internal/dependency/manager.go
  • internal/dependency/manager_test.go
  • internal/docker/docker.go
  • internal/filesystem/filesystem.go
  • internal/filesystem/filesystem_test.go
  • internal/git/git.go
  • internal/git/git_test.go
  • internal/kcl/import.go
  • internal/kcl/import_test.go
  • internal/project/build.go
  • internal/project/build_test.go
  • internal/project/certs/cert_generator.go
  • internal/project/certs/tls.go
  • internal/project/controlplane/controlplane.go
  • internal/project/functions/build.go
  • internal/project/functions/build_test.go
  • internal/project/functions/go.go
  • internal/project/functions/go_templating.go
  • internal/project/functions/kcl.go
  • internal/project/functions/python.go
  • internal/project/helm/helm.go
  • internal/project/helm/restclientgetter.go
  • internal/project/image.go
  • internal/project/install.go
  • internal/project/install_test.go
  • internal/project/projectfile/projectfile.go
  • internal/project/projectfile/projectfile_test.go
  • internal/project/push.go
  • internal/project/push_test.go
  • internal/project/render.go
  • internal/project/sort.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/go_test.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/json.go
  • internal/schemas/generator/json_test.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/kcl_test.go
  • internal/schemas/generator/python.go
  • internal/schemas/generator/python_test.go
  • internal/schemas/manager/lock.go
  • internal/schemas/manager/manager.go
  • internal/schemas/manager/manager_test.go
  • internal/schemas/manager/source.go
  • internal/schemas/manager/source_test.go
  • internal/schemas/runner/run.go
  • internal/schemas/runner/run_test.go
  • internal/terminal/spinner.go
  • internal/xpkg/client.go
  • internal/xpkg/doc.go
  • internal/xpkg/fetcher.go
  • internal/xpkg/metadata.go
  • internal/xpkg/resolver.go
  • internal/xpkg/resolver_test.go
  • internal/xrd/infer.go
  • internal/xrd/infer_test.go

Comment thread internal/project/build.go
Comment thread internal/project/functions/go_templating.go
Comment on lines +46 to +54
func (c *restClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := c.ToRESTConfig()
if err != nil {
return nil, err
}
config.Burst = 300
config.QPS = 50
discoveryClient, _ := discovery.NewDiscoveryClientForConfig(config)
return memory.NewMemCacheClient(discoveryClient), nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ignored discovery-client constructor errors in this area.
rg -n 'NewDiscoveryClientForConfig' -C3 internal/project/helm/restclientgetter.go

Repository: crossplane/cli

Length of output: 246


🏁 Script executed:

cat -n internal/project/helm/restclientgetter.go | head -60

Repository: crossplane/cli

Length of output: 2213


🏁 Script executed:

rg 'import' internal/project/helm/restclientgetter.go -A 20

Repository: crossplane/cli

Length of output: 510


🏁 Script executed:

rg 'CopyConfig' -t go --max-count 5

Repository: crossplane/cli

Length of output: 295


Handle the error from discovery client creation and avoid mutating the shared config.

Line 53 ignores the error returned by NewDiscoveryClientForConfig(), which means a nil client could be passed to memory.NewMemCacheClient() and cause failures downstream. Additionally, lines 51–52 mutate the config object in place, which affects any subsequent calls to ToDiscoveryClient() since ToRESTConfig() returns the struct's shared config field.

The fix is straightforward: copy the config before mutating it, and propagate the error:

Proposed patch
 import (
 	"k8s.io/apimachinery/pkg/api/meta"
@@
 	"k8s.io/client-go/tools/clientcmd"
+
+	"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
 )
@@
 func (c *restClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
 	config, err := c.ToRESTConfig()
 	if err != nil {
 		return nil, err
 	}
-	config.Burst = 300
-	config.QPS = 50
-	discoveryClient, _ := discovery.NewDiscoveryClientForConfig(config)
+	cfg := rest.CopyConfig(config)
+	cfg.Burst = 300
+	cfg.QPS = 50
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg)
+	if err != nil {
+		return nil, errors.Wrap(err, "cannot create Kubernetes discovery client")
+	}
 	return memory.NewMemCacheClient(discoveryClient), nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c *restClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := c.ToRESTConfig()
if err != nil {
return nil, err
}
config.Burst = 300
config.QPS = 50
discoveryClient, _ := discovery.NewDiscoveryClientForConfig(config)
return memory.NewMemCacheClient(discoveryClient), nil
func (c *restClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := c.ToRESTConfig()
if err != nil {
return nil, err
}
cfg := rest.CopyConfig(config)
cfg.Burst = 300
cfg.QPS = 50
discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg)
if err != nil {
return nil, errors.Wrap(err, "cannot create Kubernetes discovery client")
}
return memory.NewMemCacheClient(discoveryClient), nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/helm/restclientgetter.go` around lines 46 - 54, In
ToDiscoveryClient (restClientGetter) avoid mutating the shared config returned
by ToRESTConfig: create a shallow copy of the REST config before setting
QPS/Burst, call discovery.NewDiscoveryClientForConfig with the copied config and
check its error return instead of discarding it, and if err != nil return that
error; only then wrap the created discovery client with memory.NewMemCacheClient
and return it. Ensure you reference ToRESTConfig,
discovery.NewDiscoveryClientForConfig and memory.NewMemCacheClient in the
change.

from crossplane.function import logging, response
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with this version we doing this without "schemas" ? but i can see schemas generator - so how does this differ from the up-cli ? do we still need . in front =

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three changes from the up version:

  1. Schemas are always generated client-side rather than packaged as xpkg layers. We intend for users to check the schemas/ dir into git with their project, and the lockfile ensures schemas aren't regenerated unless deps change.
  2. We use full Python projects rather than the simplified Python experience we had in up. This means the pyproject.toml can reference the schemas as "crossplane-models @ file:./../../schemas/python" and they get pulled in from the schema dir as part of the build process, eliminating the need for the symlink and leading ..
  3. To make the imports nicer with the above, we put all the generated Python schemas under models/, so that they're imported like models.io.k8s.api.apps.

I'll share an example project shortly with a Python function to show how everything looks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

means all good with schemas taking some times ? do we have some docs regarding this ? what we expect here for something like ec2 provider?, compute provider etc ? what happen if we bring beack monolith ? do we have options for MRAPs in project ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, for now we'll accept that adding a dep takes a while. For reference, on my machine upbound/provider-aws-ec2 takes about 2 minutes and upbound/provider-gcp-compute takes about 3 minutes.

I do think we should optimize this in the future. A few possible (not mutually exclusive) options:

  • Package pre-generated schemas with packages, like we did in up (though maybe as related OCI artifacts rather than layers, so k8s doesn't have to pull them) and avoid generating client-side when these are present.
  • Define a way to find pre-generated schemas in standard language-specific repositories (e.g., a mapping between xpkgs and pip modules for python).
  • Optimize client-side generation by using MRAPs (or a more generic equivalent) to generate schemas for a subset of resources.
  • Add a langauges: field to the Project spec and avoid generating schemas for languages the user doesn't care about. I believe Python schemas are the slowest by far, so this would make things a lot faster for non-Python users.

Comment thread internal/terminal/spinner.go
Comment thread cmd/crossplane/render/op/cmd.go Outdated
Timeout time.Duration `default:"1m" help:"How long to run before timing out."`
CacheDir string `env:"CROSSPLANE_XPKG_CACHE" help:"Directory for cached xpkg package contents." name:"cache-dir"`
MaxConcurrency uint `default:"8" help:"Maximum concurrency for building embedded functions."`
ProjectFile string `default:"crossplane-project.yaml" help:"Path to the project file." short:"f" type:"path"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean everybody needs to have a "project" that render is working for op ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, render will work as it does today if the users provides a functions file and/or is not in a project directory (functions file is required when not in a project directory).

I'll update the ProjectFile arg here to be optional:"" so this is clearer.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the help to make it clearer that the project file is optional, and the functions file can be omitted when using render in a project.

p.Operations = "operations"
}
if p.Schemas == "" {
p.Schemas = "schemas"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for what? we not allowing apiDependencies and dependsOn as input for schemas ? that we can render ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the directory into which schemas will be generated (and where the function builders will look for schemas) - like the .up directory in the Upbound DevEx. Figured we may as well make it configurable rather than hard-coding to schemas/.

// Crossplane 2.x release.
Crossplane *pkgmetav1.CrossplaneConstraints `json:"crossplane,omitempty"`
// Dependencies are built-time and runtime dependencies of the project.
Dependencies []Dependency `json:"dependencies,omitempty"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is old dependsOn ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, consolidated dependsOn and apiDependencies into a single set based on feedback in the design doc.


// ProjectPackageMetadata holds metadata about the project, which will become
// package metadata when a project is built into a Crossplane package.
type ProjectPackageMetadata struct {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this up2date with "marketplace" guys - or we decouple this here ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In up we've added AdditionalMetadata, which can hold arbitrary k/v pairs that are specifically used by the Upbound marketplace (and must be prefixed with meta.upbound.io/). I've left those out here for now since they're pretty Upbound-specific and the k/v pairs get copied directly into the resulting configuration package's metadata.annotations so it's possible to fill them in just by writing them directly in the project's metadata.annotations. We can always add them in the future if desired.

// built package. Only xpkg dependencies can be runtime dependencies.
// Default is false, meaning xpkg dependencies are runtime by default.
// +optional
APIOnly bool `json:"apiOnly,omitempty"`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have an case that an xpkgDependency is not a dependency? - like Configurations skipping dependency ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With apiOnly: true we'll generate schemas for the xpkg, but not add it to the dependsOn of the configuration. The only realistic use-case I can think of for this right now is if a user wants to build with provider schemas but install providers manually rather than using the package manager.

@haarchri
Copy link
Copy Markdown
Member

@adamwg can you provide some demo/template repositories based on this PR ? this makes it much easier to review

@adamwg
Copy link
Copy Markdown
Member Author

adamwg commented May 12, 2026

@adamwg can you provide some demo/template repositories based on this PR ? this makes it much easier to review

Yep, good idea. Building some examples here: https://github.com/adamwg/crossplane-devex-examples (only Python so far, will work on others).

I will also update https://github.com/adamwg/kubecon-eu-2026-devex-demo to reflect the current state of the tooling so it can be used as an end-to-end testing script.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (7)
cmd/crossplane/project/push.go (1)

85-88: ⚡ Quick win

Consider setting MinVersion for defense in depth.

When TLS verification is enabled (the default case), the transport should enforce a minimum TLS version for better security posture. While InsecureSkipVerify is correctly flagged and documented, the TLS config should still use secure defaults when verification is enabled.

🔒 Proposed fix to set TLS minimum version
 	t := http.DefaultTransport.(*http.Transport).Clone() //nolint:forcetypeassert // http.DefaultTransport is always *http.Transport
 	if c.InsecureSkipTLSVerify {
 		t.TLSClientConfig = &tls.Config{
+			MinVersion:         tls.VersionTLS12,
 			InsecureSkipVerify: true, //nolint:gosec // we need to support insecure connections if requested
 		}
+	} else {
+		// Enforce minimum TLS version for secure connections
+		t.TLSClientConfig = &tls.Config{
+			MinVersion: tls.VersionTLS12,
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/push.go` around lines 85 - 88, The TLS config for the
HTTP transport currently only sets InsecureSkipVerify when
c.InsecureSkipTLSVerify is true; add a secure default by setting MinVersion on
t.TLSClientConfig (the tls.Config used when c.InsecureSkipTLSVerify is false or
when creating the config) to tls.VersionTLS12 (or a higher minimum) to enforce a
minimum TLS version for defense-in-depth; update the logic around
t.TLSClientConfig and tls.Config so MinVersion is always set when verification
is enabled while preserving the existing no-verify branch that sets
InsecureSkipVerify.
internal/schemas/generator/go.go (5)

638-659: ⚡ Quick win

goRemoveRequired doesn't recurse into AdditionalProperties — possible miss for map-typed fields.

Other helpers in this file (e.g. goReferenceK8sTypesPropertiesWithMetaPath at lines 746-749) explicitly recurse into prop.AdditionalProperties.Schema and its properties. Here, goRemovePropertiesRequired only walks Properties and Items. If any CRD has an object with additionalProperties: { type: object, required: [...] } (e.g., map[string]SomeObject), those nested required lists won't be cleared, and oapi-codegen may still emit non-optional fields.

Could you confirm whether that path can occur in practice for the schemas you're generating? If yes, mirroring the AdditionalProperties recursion here would keep the "everything is optional" invariant true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/go.go` around lines 638 - 659, The helper
goRemovePropertiesRequired currently clears Required for Properties and Items
but misses AdditionalProperties, so map/object-typed fields keep nested required
lists; update both goRemoveRequired and goRemovePropertiesRequired to check for
schema.AdditionalProperties (and prop.AdditionalProperties), and when non-nil
set AdditionalProperties.Schema.Required = nil and call
goRemovePropertiesRequired on AdditionalProperties.Schema.Properties (with nil
checks), mirroring the existing Items handling to ensure map values' nested
required arrays are also removed.

1003-1021: ⚡ Quick win

Map iteration order may make import output non-deterministic.

Just flagging for your awareness: k8sImports is a map[string]string, and we iterate it directly to call astutil.AddNamedImport. Because Go randomizes map iteration order, two runs over the same input could produce imports added in different orders. go/printer doesn't re-sort imports, so the resulting generated files could differ between runs (annoying for diffs / reproducible builds / test stability).

Would it be worth iterating over a sorted slice of aliases instead? Happy to be told this doesn't matter in practice if downstream tooling re-formats.

🔧 Suggested tweak
-	for alias, path := range k8sImports {
-		if !needed[alias] {
-			continue
-		}
-		// AddNamedImport will do nothing if the import (with that alias) is already present
-		astutil.AddNamedImport(fset, f, alias, path)
-	}
+	aliases := make([]string, 0, len(k8sImports))
+	for alias := range k8sImports {
+		aliases = append(aliases, alias)
+	}
+	slices.Sort(aliases)
+	for _, alias := range aliases {
+		if !needed[alias] {
+			continue
+		}
+		// AddNamedImport will do nothing if the import (with that alias) is already present.
+		astutil.AddNamedImport(fset, f, alias, k8sImports[alias])
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/go.go` around lines 1003 - 1021, The loop over
k8sImports can yield non-deterministic import ordering because maps iterate
randomly; change the logic so after building the needed map you collect the
aliases from k8sImports into a slice, sort that slice (e.g., with sort.Strings),
then iterate the sorted aliases and call astutil.AddNamedImport(fset, f, alias,
path) only for aliases present in needed; update references to k8sImports,
needed, astutil.AddNamedImport, fset and f accordingly so imports are added in a
deterministic, sorted order.

478-486: 💤 Low value

Consider defer f.Close() and surfacing the close error.

writeGoCode swallows the result of f.Close() with _ = f.Close(). For an in-memory afero.MemMapFs this is essentially harmless, but if the schema FS abstraction is ever swapped for a real filesystem (or a writer that flushes on close), a write error could be lost silently. A small defer-and-report pattern would keep this robust to future changes.

🔧 Suggested tweak
 	f, err := schemaFS.Create(goPath)
 	if err != nil {
 		return errors.Wrap(err, "failed to create go schema file")
 	}
+	defer func() { _ = f.Close() }()
 	if _, err := f.WriteString(code); err != nil {
 		return errors.Wrap(err, "failed to write go code to file")
 	}
-	_ = f.Close()
-
-	return nil
+	return errors.Wrap(f.Close(), "failed to close go schema file")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/go.go` around lines 478 - 486, In writeGoCode,
don't ignore the file close error: replace the current explicit _ = f.Close()
with a defer that captures Close's error and surfaces it (e.g., defer func(){ if
cerr := f.Close(); err == nil && cerr != nil { err = errors.Wrap(cerr, "failed
to close go schema file") } }()), ensuring you preserve the existing write error
path (symbols to locate: schemaFS.Create, f.WriteString, f.Close, writeGoCode).
This ensures any flush/close failure from the underlying FS is returned to the
caller.

1322-1349: 💤 Low value

maps.Copy from schemas is then overwritten by openAPISpec.Components.Schemas — first copy looks redundant.

Reading this end-to-end: line 1343 copies the GVK-filtered schemas into groupSpec.Components.Schemas, then line 1348 copies all of openAPISpec.Components.Schemas over it. Since the GVK-grouped entries are a subset of the full schema set (same keys, same pointer values), the first copy is overwritten verbatim and is effectively a no-op.

Is the intent perhaps that schemas should win over openAPISpec.Components.Schemas (i.e., reverse the order), or can we drop the first copy entirely? A short comment about why supporting types are pulled in would also help future readers.

Also, since parts := strings.Split(gvkKey, "/") is then indexed with parts[0]/parts[1] unconditionally, a tiny length check would protect against a future caller that produces an unexpected key shape — extractGVKKey enforces it today, but the assumption is implicit.

🔧 Suggested simplification
 	groupSpec := &spec3.OpenAPI{
 		Version: "3.0.0",
 		Components: &spec3.Components{
 			Schemas: make(map[string]*spec.Schema),
 		},
 	}
 
-	// Add the main schemas for this GVK group
-	maps.Copy(groupSpec.Components.Schemas, schemas)
-
-	// Add all other schemas from the same spec that might be referenced
-	// but don't have GVK extensions (like TokenRequestSpec, etc.)
-	// Add schemas that don't have GVK extensions (supporting types)
+	// Pull in every schema from the originating spec so supporting types
+	// (e.g. TokenRequestSpec) referenced from the GVK-tagged schemas in
+	// `schemas` are also available during generation.
 	maps.Copy(groupSpec.Components.Schemas, openAPISpec.Components.Schemas)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/go.go` around lines 1322 - 1349, The code in
generateGVKGroupCode currently copies schemas into groupSpec.Components.Schemas
then immediately overwrites them by copying openAPISpec.Components.Schemas
(using maps.Copy), making the first copy redundant; change the copy order so
that maps.Copy(groupSpec.Components.Schemas, openAPISpec.Components.Schemas)
runs first and then maps.Copy(..., schemas) so entries from the GVK-filtered
schemas override the general spec, or remove the first copy entirely if you
prefer general-only merge; also add a defensive length check on parts produced
from strings.Split(gvkKey, "/") (or use strings.Cut/extractGVKKey) to avoid
panics when gvkKey is malformed, and add a short comment explaining that we
merge supporting types from the full OpenAPI spec and allow GVK-specific schemas
to take precedence.

543-554: 💤 Low value

Sharing generateGoMutex between codegen.Generate and goFixName — intentional?

Tiny clarifying question: generateGoMutex is documented (line 375-377) as guarding the non-concurrency-safe codegen.Generate, but it's also taken inside goFixName around codegen.SchemaNameToTypeName / codegen.ToCamelCaseWithInitialisms. Could you confirm whether those helpers also need serialization? If they do, the mutex comment might be worth updating to reflect the broader contract; if they don't, splitting these into two separate mutexes would avoid goFixName callers blocking (and being blocked by) full generation runs in parallel test scenarios.

Not blocking — just want to make sure the coupling here is by design.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/go.go` around lines 543 - 554, goFixName currently
locks generateGoMutex (which is documented as protecting the
non-concurrency-safe codegen.Generate), causing unintended coupling between
callers of goFixName and long-running generate runs; either document that
generateGoMutex also protects codegen.SchemaNameToTypeName and
codegen.ToCamelCaseWithInitialisms, or split the lock so goFixName uses its own
mutex (e.g., goFixNameMutex) and leave generateGoMutex for codegen.Generate
only. Locate generateGoMutex, goFixName, and the helpers
codegen.SchemaNameToTypeName/codegen.ToCamelCaseWithInitialisms and implement
one of the two options: update the mutex comment to reflect the broader
contract, or replace the lock in goFixName with a dedicated mutex to avoid
blocking generate runs. Ensure tests still pass and add a brief comment
describing the new locking rule.
cmd/crossplane/render/xr/cmd.go (1)

62-64: ⚡ Quick win

Document the project fallback in --help.

Could we make the optional third positional explicit here? Right now Functions is optional in code, but the arg help and the long examples still read like functions.yaml is always required. Adding a short “If omitted, functions are loaded from --project-file” note here plus one 2-argument example would make the new flow much easier to discover.

As per coding guidelines **/cmd/**: Review CLI commands for proper flag handling, help text, and error messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/render/xr/cmd.go` around lines 62 - 64, Update the Functions
positional argument help to explicitly state it is optional and describe the
fallback: change the Functions field's help string (symbol: Functions) to append
something like "If omitted, functions are loaded from --project-file." Keep the
`optional:""` tag as-is. Also add a new short example in the command
help/examples showing the two-argument invocation (CompositeResource +
Composition) without Functions so users see the fallback flow; update any local
examples/usage text in the same file (where examples for this command are
defined) to include the 2-arg example.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/crossplane/composition/generate.go`:
- Line 53: Update the help text for the Plural field to accurately describe what
the flag controls: clarify that it sets a custom plural/name/path for generated
resources (affecting derived plural/name/path behavior) rather than modifying
the CompositeTypeRef.Kind; locate the struct field named Plural (the line with
`Plural string`) in generate.go and replace the help string accordingly so the
flag description reflects derived plural/name/path behavior.
- Around line 223-228: Validate that the plural name is non-empty before using
it to derive names/paths: check xrd.Spec.Names.Plural (and consider the override
c.Plural) and if the resulting plural is empty return a clear error (or fail
fast) with a message like "plural name is required: set spec.names.plural or
--plural" instead of proceeding to compute group/kind/plural and generating
invalid paths/metadata; update the logic around the plural assignment (where
group = xrd.Spec.Group, kind = xrd.Spec.Names.Kind, plural =
xrd.Spec.Names.Plural and the c.Plural override) to perform this validation and
error reporting.
- Around line 65-76: The AfterApply() method currently returns raw errors from
filepath.Abs, projectfile.Parse, and later file writes; update AfterApply() (and
the WriteFile error return in the generate flow) to wrap these errors using
errors.Wrap or errors.Wrapf from crossplane-runtime to provide user-facing
context — e.g., wrap the filepath.Abs error with "resolve project file path",
wrap projectfile.Parse error with "parse project file <ProjectFile>" (use
filepath.Base(c.ProjectFile) in the message), and wrap the WriteFile error with
"write generated composition to <output path>"; locate these fixes in the
generateCmd.AfterApply method and the file-write call in the same generate
command implementation and replace direct returns of err with
errors.Wrap/errors.Wrapf including the descriptive messages.

In `@cmd/crossplane/dependency/dependency.go`:
- Around line 21-25: The new Cmd struct (fields Add, UpdateCache, CleanCache)
must be registered only when the experimental feature flag is enabled; update
the command registration so the dependency command group is created/returned
conditionally (e.g., move creation behind a NewDependencyCmd or GetDependencyCmd
factory that checks a feature gate function IsFeatureEnabled("dependencyCmd") or
consults the existing feature flag mechanism) and do not expose Cmd
unconditionally; if the gate is already enforced higher up, add a reference
comment to that check from the Cmd declaration and/or call to NewDependencyCmd
so reviewers can see the protection.

In `@cmd/crossplane/project/push.go`:
- Around line 99-105: The current error returned when
name.NewRepository(c.Repository) fails is too technical; update the error
returned from the repository-parsing block (where c.Repository,
name.NewRepository and c.proj.Spec.Repository are used) to present a
user-friendly message that states the repository value is invalid, shows the
expected format (e.g., "hostname/owner/repo[:tag]" or "registry/repo[:tag]"),
and suggests how to correct it (for example: "use a full registry path like
'ghcr.io/org/repo:tag' or omit the tag to use latest"); wrap the original error
for debugging (keep errors.Wrap(err, ...)) but make the visible message concise
and actionable for CLI users.
- Around line 148-177: The loadPackages function returns technical errors;
update the error messages at the three failure points (tarball.LoadManifest
call, name.NewTag parsing, and tarball.Image loading) to be user-friendly and
actionable: when tarball.LoadManifest fails, wrap the error with something like
"unable to open package file '<c.PackageFile>'; please check the file exists and
is a valid package" and include the original error; when name.NewTag fails,
return "invalid image tag '<desc.RepoTags[0]>' found in package
'<c.PackageFile>'; ensure tags use the form 'repository:tag'"; when
tarball.Image fails, return "could not extract image '<tag>' from package
'<c.PackageFile>'; verify the package contains that image and is not corrupted"
— keep the original error attached for debugging and update the messages around
functions name.NewTag, tarball.LoadManifest, and tarball.Image accordingly.
- Around line 61-64: The call to filepath.Abs in push.go that computes
projFilePath does not wrap the error with user-friendly context; update the
error handling around filepath.Abs(c.ProjectFile) (referencing projFilePath,
filepath.Abs and c.ProjectFile) to return a descriptive message such as "failed
to resolve project file path '<path>'" that includes the original path and
suggests a next step (e.g., verify the path or run from repo root); implement
this by wrapping the original err with fmt.Errorf or errors.Wrapf so the
underlying error is preserved while presenting clear, actionable context to the
user.

In `@cmd/crossplane/render/xr/cmd.go`:
- Around line 464-466: The os.Stat(projFilePath) error handling conflates "file
doesn't exist" with other failures; update the check around
os.Stat(projFilePath) to distinguish os.IsNotExist(err) (return a user-friendly
message saying the project file at projFilePath was not found and suggest using
FUNCTIONS env or --project-file) from permission or other errors (return a
message that the project file at projFilePath is inaccessible and advise
checking permissions or the path), and ensure you still avoid leaking internal
error details while including the checked path (projFilePath) and actionable
guidance; adjust the returned error strings where the current code returns
errors.New("functions argument is required when not in a project") accordingly.

In `@internal/kcl/import.go`:
- Around line 64-71: The alias-collision loop that reassigns alias by joining
progressively larger suffixes (using variables alias, parts and the
existingAliases map) can still return a non-unique alias if every candidate is
already present; change the logic to, after the existing loop, check if alias is
still present in existingAliases and then append a deterministic numeric suffix
(e.g., "_1", incrementing to "_2", etc.) until you find one not in
existingAliases, then use that; apply the same fix to the second similar block
that uses the same pattern so both places guarantee a unique alias.
- Line 73: The code writes to the map variable existingAliases which can be nil
and will panic; modify the function that contains the assignment to defensively
initialize existingAliases before any write (check if existingAliases == nil and
set it to a new map[string]bool) so callers don't need to guarantee it; update
any related variable scope (e.g., the function in import.go where
existingAliases[alias] = true occurs) to perform this nil-check/initialization
once before the loop or assignment.

In `@internal/project/functions/go.go`:
- Around line 76-81: Wrap the raw errors returned from name.ParseReference and
remote.Index with crossplane-runtime/pkg/errors to match the rest of the
function: when calling name.ParseReference(ref, name.StrictValidation) wrap the
error (e.g., using errors.Wrapf) to include context like "parsing base image %s"
referencing baseImage/ name.ParseReference, and when calling remote.Index(ref,
remote.WithTransport(b.transport),
remote.WithAuthFromKeychain(authn.DefaultKeychain)) wrap that error to include
context like "fetching remote index for %s" referencing remote.Index, ref,
b.transport and authn.DefaultKeychain so callers get consistent, user-facing
messages.
- Around line 55-58: The Build method in goBuilder currently silences the
process-wide stdlib logger via log.SetOutput(io.Discard) which permanently drops
logs; change this to save the previous writer and restore it when Build returns:
capture old := log.Writer() (or the prior io.Writer), call
log.SetOutput(io.Discard) at the start of goBuilder.Build, and defer restoring
the previous writer with log.SetOutput(old) to limit the side-effect to the
Build duration; optionally add a TODO in goBuilder.Build noting the concurrency
caveat and that a non-global logger from ko would be preferable in future.
- Around line 65-84: The callback passed to build.NewGo returns raw errors from
name.ParseReference and remote.Index; wrap those errors with user-facing context
using errors.Wrap or fmt.Errorf with context to explain the operation and likely
cause. Update the error returns in the anonymous function (the base-images
callback used by build.NewGo / build.WithBaseImages) so that the ParseReference
error is returned as wrapped (e.g., errors.Wrap(err, "failed to parse base image
reference") ) and the remote.Index error is returned wrapped (e.g.,
errors.Wrap(err, "failed to fetch base image index from registry") ), keeping
the same return values (ref, img, err) shape. Ensure the comment about
intentionally ignoring the importpath (_) remains or add a brief comment, and
use the same error-wrapping style already used for RewritePath to keep
consistency.

In `@internal/schemas/generator/go.go`:
- Around line 516-525: When goFixName(name) returns an empty string in
goRenameTypes (and likewise in goRenameEnums), the code deletes the entry from
s.Components.Schemas but continues to call goRenameSchemaType/goRenameEnumValues
and recurse into properties with an empty goName; add a continue immediately
after delete(s.Components.Schemas, name) so the loop skips further processing
for that entry. Locate the map iteration in goRenameTypes and the analogous loop
in goRenameEnums, identify the use of goFixName(name), the delete call, and then
insert the continue to prevent calling goRenameSchemaType,
goRenamePropertyTypes, goRenameEnumValues or any further recursion when goName
== "".
- Around line 197-246: The switch in goExtractK8sSchemas only sets
group/kind/version for k8sPkgMetaV1 and k8sPkgAutoscalingV1, leaving other pkg
values empty which causes goSchemaPath (used by writeGoCode) to produce
models/.go and overwrite files; fix by populating the missing cases
(k8sPkgRuntime, k8sPkgCoreV1, k8sPkgIntStr, k8sPkgResource) with the same
group/kind/version mapping used by getK8sPackageInfo (or simply call
getK8sPackageInfo(pkg) to retrieve the canonical values) while preserving the
existing refMutator logic, and add a default branch that returns an explicit
error if an unknown pkg is encountered to avoid silent collisions.
- Around line 985-1028: The function fixMissingImports currently uses fmt.Errorf
for errors; replace those with errors.Wrapf from crossplane-runtime/pkg/errors
and update the messages to be user-friendly (e.g., "failed to parse generated Go
code: %v" and "failed to print generated Go AST: %v") so callers get meaningful
context; update the two error returns inside fixMissingImports to use
errors.Wrapf(err, "<message>") and remove the now-unused fmt import.

---

Nitpick comments:
In `@cmd/crossplane/project/push.go`:
- Around line 85-88: The TLS config for the HTTP transport currently only sets
InsecureSkipVerify when c.InsecureSkipTLSVerify is true; add a secure default by
setting MinVersion on t.TLSClientConfig (the tls.Config used when
c.InsecureSkipTLSVerify is false or when creating the config) to
tls.VersionTLS12 (or a higher minimum) to enforce a minimum TLS version for
defense-in-depth; update the logic around t.TLSClientConfig and tls.Config so
MinVersion is always set when verification is enabled while preserving the
existing no-verify branch that sets InsecureSkipVerify.

In `@cmd/crossplane/render/xr/cmd.go`:
- Around line 62-64: Update the Functions positional argument help to explicitly
state it is optional and describe the fallback: change the Functions field's
help string (symbol: Functions) to append something like "If omitted, functions
are loaded from --project-file." Keep the `optional:""` tag as-is. Also add a
new short example in the command help/examples showing the two-argument
invocation (CompositeResource + Composition) without Functions so users see the
fallback flow; update any local examples/usage text in the same file (where
examples for this command are defined) to include the 2-arg example.

In `@internal/schemas/generator/go.go`:
- Around line 638-659: The helper goRemovePropertiesRequired currently clears
Required for Properties and Items but misses AdditionalProperties, so
map/object-typed fields keep nested required lists; update both goRemoveRequired
and goRemovePropertiesRequired to check for schema.AdditionalProperties (and
prop.AdditionalProperties), and when non-nil set
AdditionalProperties.Schema.Required = nil and call goRemovePropertiesRequired
on AdditionalProperties.Schema.Properties (with nil checks), mirroring the
existing Items handling to ensure map values' nested required arrays are also
removed.
- Around line 1003-1021: The loop over k8sImports can yield non-deterministic
import ordering because maps iterate randomly; change the logic so after
building the needed map you collect the aliases from k8sImports into a slice,
sort that slice (e.g., with sort.Strings), then iterate the sorted aliases and
call astutil.AddNamedImport(fset, f, alias, path) only for aliases present in
needed; update references to k8sImports, needed, astutil.AddNamedImport, fset
and f accordingly so imports are added in a deterministic, sorted order.
- Around line 478-486: In writeGoCode, don't ignore the file close error:
replace the current explicit _ = f.Close() with a defer that captures Close's
error and surfaces it (e.g., defer func(){ if cerr := f.Close(); err == nil &&
cerr != nil { err = errors.Wrap(cerr, "failed to close go schema file") } }()),
ensuring you preserve the existing write error path (symbols to locate:
schemaFS.Create, f.WriteString, f.Close, writeGoCode). This ensures any
flush/close failure from the underlying FS is returned to the caller.
- Around line 1322-1349: The code in generateGVKGroupCode currently copies
schemas into groupSpec.Components.Schemas then immediately overwrites them by
copying openAPISpec.Components.Schemas (using maps.Copy), making the first copy
redundant; change the copy order so that maps.Copy(groupSpec.Components.Schemas,
openAPISpec.Components.Schemas) runs first and then maps.Copy(..., schemas) so
entries from the GVK-filtered schemas override the general spec, or remove the
first copy entirely if you prefer general-only merge; also add a defensive
length check on parts produced from strings.Split(gvkKey, "/") (or use
strings.Cut/extractGVKKey) to avoid panics when gvkKey is malformed, and add a
short comment explaining that we merge supporting types from the full OpenAPI
spec and allow GVK-specific schemas to take precedence.
- Around line 543-554: goFixName currently locks generateGoMutex (which is
documented as protecting the non-concurrency-safe codegen.Generate), causing
unintended coupling between callers of goFixName and long-running generate runs;
either document that generateGoMutex also protects codegen.SchemaNameToTypeName
and codegen.ToCamelCaseWithInitialisms, or split the lock so goFixName uses its
own mutex (e.g., goFixNameMutex) and leave generateGoMutex for codegen.Generate
only. Locate generateGoMutex, goFixName, and the helpers
codegen.SchemaNameToTypeName/codegen.ToCamelCaseWithInitialisms and implement
one of the two options: update the mutex comment to reflect the broader
contract, or replace the lock in goFixName with a dedicated mutex to avoid
blocking generate runs. Ensure tests still pass and add a brief comment
describing the new locking rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 916f0883-cbe9-45d8-8a55-38a092185600

📥 Commits

Reviewing files that changed from the base of the PR and between 23c2b56 and f4f09ed.

⛔ Files ignored due to path filters (29)
  • cmd/crossplane/function/templates/go-templating/00-prelude.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go-templating/01-compose.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go.tar is excluded by !**/*.tar and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.lock is excluded by !**/*.lock and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.tmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/kcl/main.k is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__init__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__version__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/fn.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/main.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/pyproject.toml.tmpl is excluded by none and included by none
  • go.mod is excluded by none and included by none
  • go.sum is excluded by !**/*.sum and included by none
  • gomod2nix.toml is excluded by none and included by none
  • internal/crd/testdata/claimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/unclaimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/project/functions/testdata/go-templating-function/resource1.yaml.gotmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/go-templating-function/resource2.yaml.tmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod.lock is excluded by !**/*.lock, !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/main.k is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/account_scaffold_composition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/account_scaffold_definition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/api__v1_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/api_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/azure_linux_function_app.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/configuration_crossplane.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/runner/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
📒 Files selected for processing (88)
  • cmd/crossplane/composition/composition.go
  • cmd/crossplane/composition/generate.go
  • cmd/crossplane/composition/generate_test.go
  • cmd/crossplane/dependency/add.go
  • cmd/crossplane/dependency/auth.go
  • cmd/crossplane/dependency/cache.go
  • cmd/crossplane/dependency/dependency.go
  • cmd/crossplane/function/function.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/function/pipeline.go
  • cmd/crossplane/function/templates/python/README.md
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/init.go
  • cmd/crossplane/project/project.go
  • cmd/crossplane/project/push.go
  • cmd/crossplane/project/run.go
  • cmd/crossplane/project/stop.go
  • cmd/crossplane/render/op/cmd.go
  • cmd/crossplane/render/op/cmd_test.go
  • cmd/crossplane/render/xr/cmd.go
  • cmd/crossplane/render/xr/cmd_test.go
  • cmd/crossplane/xrd/generate.go
  • cmd/crossplane/xrd/generate_test.go
  • cmd/crossplane/xrd/xrd.go
  • internal/async/event.go
  • internal/crd/convert.go
  • internal/crd/convert_test.go
  • internal/crd/generator.go
  • internal/crd/generator_test.go
  • internal/dependency/cache_dir.go
  • internal/dependency/manager.go
  • internal/dependency/manager_test.go
  • internal/docker/docker.go
  • internal/filesystem/filesystem.go
  • internal/filesystem/filesystem_test.go
  • internal/git/git.go
  • internal/git/git_test.go
  • internal/kcl/import.go
  • internal/kcl/import_test.go
  • internal/project/build.go
  • internal/project/build_test.go
  • internal/project/certs/cert_generator.go
  • internal/project/certs/tls.go
  • internal/project/controlplane/controlplane.go
  • internal/project/functions/build.go
  • internal/project/functions/build_test.go
  • internal/project/functions/go.go
  • internal/project/functions/go_templating.go
  • internal/project/functions/kcl.go
  • internal/project/functions/python.go
  • internal/project/helm/helm.go
  • internal/project/helm/restclientgetter.go
  • internal/project/image.go
  • internal/project/install.go
  • internal/project/install_test.go
  • internal/project/projectfile/projectfile.go
  • internal/project/projectfile/projectfile_test.go
  • internal/project/push.go
  • internal/project/push_test.go
  • internal/project/render.go
  • internal/project/sort.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/go_test.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/json.go
  • internal/schemas/generator/json_test.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/kcl_test.go
  • internal/schemas/generator/python.go
  • internal/schemas/generator/python_test.go
  • internal/schemas/manager/lock.go
  • internal/schemas/manager/manager.go
  • internal/schemas/manager/manager_test.go
  • internal/schemas/manager/source.go
  • internal/schemas/manager/source_test.go
  • internal/schemas/runner/run.go
  • internal/schemas/runner/run_test.go
  • internal/terminal/spinner.go
  • internal/xpkg/client.go
  • internal/xpkg/doc.go
  • internal/xpkg/fetcher.go
  • internal/xpkg/metadata.go
  • internal/xpkg/resolver.go
  • internal/xpkg/resolver_test.go
  • internal/xrd/infer.go
  • internal/xrd/infer_test.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/crossplane/function/templates/python/README.md
🚧 Files skipped from review as they are similar to previous changes (63)
  • cmd/crossplane/xrd/xrd.go
  • internal/dependency/cache_dir.go
  • cmd/crossplane/composition/composition.go
  • internal/kcl/import_test.go
  • cmd/crossplane/project/project.go
  • internal/schemas/generator/interface.go
  • cmd/crossplane/render/op/cmd_test.go
  • cmd/crossplane/render/xr/cmd_test.go
  • internal/crd/generator_test.go
  • internal/project/projectfile/projectfile_test.go
  • internal/async/event.go
  • cmd/crossplane/dependency/auth.go
  • internal/schemas/generator/go_test.go
  • cmd/crossplane/dependency/cache.go
  • cmd/crossplane/dependency/add.go
  • internal/dependency/manager_test.go
  • internal/project/build_test.go
  • internal/project/sort.go
  • internal/crd/generator.go
  • internal/crd/convert_test.go
  • cmd/crossplane/function/function.go
  • internal/project/helm/restclientgetter.go
  • cmd/crossplane/composition/generate_test.go
  • cmd/crossplane/project/stop.go
  • internal/project/push_test.go
  • internal/filesystem/filesystem.go
  • cmd/crossplane/project/init.go
  • internal/docker/docker.go
  • cmd/crossplane/main.go
  • internal/project/helm/helm.go
  • cmd/crossplane/xrd/generate.go
  • cmd/crossplane/function/pipeline.go
  • internal/schemas/generator/json_test.go
  • internal/project/functions/kcl.go
  • internal/project/functions/python.go
  • cmd/crossplane/xrd/generate_test.go
  • internal/schemas/generator/kcl_test.go
  • internal/project/push.go
  • internal/project/certs/tls.go
  • internal/project/image.go
  • cmd/crossplane/render/op/cmd.go
  • internal/project/functions/go_templating.go
  • internal/project/certs/cert_generator.go
  • internal/project/render.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • internal/project/functions/build_test.go
  • internal/project/functions/build.go
  • internal/project/projectfile/projectfile.go
  • internal/schemas/generator/json.go
  • cmd/crossplane/project/build.go
  • internal/project/build.go
  • internal/project/install_test.go
  • internal/git/git.go
  • internal/dependency/manager.go
  • internal/git/git_test.go
  • internal/project/install.go
  • cmd/crossplane/project/run.go
  • internal/crd/convert.go
  • internal/filesystem/filesystem_test.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/python.go
  • internal/project/controlplane/controlplane.go

Comment thread cmd/crossplane/composition/generate.go
Comment thread cmd/crossplane/composition/generate.go
Comment thread cmd/crossplane/composition/generate.go
Comment thread cmd/crossplane/dependency/dependency.go
Comment thread cmd/crossplane/project/push.go
Comment thread internal/project/functions/go.go
Comment thread internal/project/functions/go.go
Comment thread internal/schemas/generator/go.go
Comment thread internal/schemas/generator/go.go
Comment thread internal/schemas/generator/go.go
The central concept in our developer experience is the Project. Introduce the
Project API in preparation for adding the tooling.

Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (8)
internal/kcl/import_test.go (1)

28-31: ⚡ Quick win

Align test table shape with repo convention (args/want + reason).

Thanks for adding strong coverage here. Could we reshape each case to include args/want and a short reason field to match the test guideline pattern and make failure intent clearer?

Proposed refactor
-	tests := map[string]struct {
-		paths       []string
-		wantImports map[string]string
-	}{
+	tests := map[string]struct {
+		reason string
+		args   struct {
+			paths []string
+		}
+		want map[string]string
+	}{
 		"BasicPath": {
-			paths:       []string{"kcl/io.upbound.aws.ec2.v1beta1"},
-			wantImports: map[string]string{"ec2v1beta1": "models.io.upbound.aws.ec2.v1beta1"},
+			reason: "build alias from last two segments for a standard dotted path",
+			args: struct {
+				paths []string
+			}{paths: []string{"kcl/io.upbound.aws.ec2.v1beta1"}},
+			want: map[string]string{"ec2v1beta1": "models.io.upbound.aws.ec2.v1beta1"},
 		},
-			gotImports := FormatKclImportPaths(tc.paths)
-			if diff := cmp.Diff(tc.wantImports, gotImports); diff != "" {
+			gotImports := FormatKclImportPaths(tc.args.paths)
+			if diff := cmp.Diff(tc.want, gotImports); diff != "" {
 				t.Errorf("importPath mismatch (-want +got):\n%s", diff)
 			}

As per coding guidelines: "**/*_test.go: Enforce table-driven test structure ... args/want pattern ... proper test case naming and reason fields."

Also applies to: 32-63

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/kcl/import_test.go` around lines 28 - 31, The test table `tests` in
import_test.go should follow the repo convention by replacing the current shape
(paths []string, wantImports map[string]string) with the args/want/reason
pattern; change each case to: struct{ args struct{ paths []string }; want
map[string]string; reason string } and move existing `paths` into `args.paths`,
`wantImports` into `want`, and add a short `reason` for each case, then update
all references in the test loop that read `paths`/`wantImports` to use
`tc.args.paths` and `tc.want` respectively (look for the `tests` variable and
usages in the test function to modify).
internal/project/functions/build_test.go (1)

121-127: ⚡ Quick win

Use cmp-based error assertions instead of exact string matching.

Would you be open to switching this to an args/want-style table entry and asserting errors with cmp.Diff(..., cmpopts.EquateErrors())? It makes these tests less brittle as messages evolve.

As per coding guidelines: "**/*_test.go: ... use cmp.Diff with cmpopts.EquateErrors() for error testing."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/functions/build_test.go` around lines 121 - 127, Replace the
brittle exact string comparisons in the test's error assertions by adding an
args/want-style error entry to the test case table (e.g., extend each tc with a
wantErr error or nil) and then assert using cmp.Diff(err, tc.wantErr,
cmpopts.EquateErrors()) (instead of comparing err.Error() to a literal). Locate
the block using tc.expectError/err in build_test.go, remove the direct string
match ("no suitable builder found") and use cmp.Diff with cmpopts.EquateErrors()
to report mismatches; ensure imports include "github.com/google/go-cmp/cmp" and
"github.com/google/go-cmp/cmp/cmpopts".
internal/schemas/generator/python.go (6)

400-400: 💤 Low value

Curious about the depth computation against the full path.

depth := strings.Count(filePath, string(os.PathSeparator)) counts separators in whatever absolute-ish path the caller hands in, which today includes the sorted/... (or models/models/...) prefix. That means the number of leading dots produced by adjustLeadingDots depends on where in the in‑memory FS the file happens to live, not on its position within the generated Python package. It currently works because everything funnels through postTransformCRD with a stable targetDir, but it's a sharp edge if either entry point changes.

Would it be worth computing depth relative to the package root (e.g., from pythonAdoptModelsStructure / pythonPackageRoot) so the import math is anchored to the package layout rather than the FS layout? Happy to be told this is fine as-is if I'm missing context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/python.go` at line 400, The current depth
calculation uses strings.Count(filePath, string(os.PathSeparator)) which counts
separators in the full filesystem path and can vary with in-memory FS layout;
change depth to be measured relative to the Python package root (e.g.,
pythonPackageRoot / value from pythonAdoptModelsStructure or postTransformCRD)
by computing the path relative to that package root (use filepath.Rel or trim
the package root prefix) and then count separators in the relative path before
calling adjustLeadingDots; update the code that sets depth (the variable named
depth near filePath) to derive the relative path first and fall back to the
original behavior only if computing the relative path fails.

308-308: ⚡ Quick win

os.ModePerm (0o777) is awfully generous for .py source and __init__.py files.

Small nit, but every write here uses os.ModePerm which is 0o777 — world‑writable + executable for files that are just Python sources. finalizePythonSchemas already uses 0o644 for the seeded __init__.py / pyproject.toml, so we're a little inconsistent. Could we standardize on 0o644 for these writes? Same applies to the other call sites flagged in the line range.

Also applies to: 376-376, 385-385, 417-417, 555-555, 667-667, 673-673, 740-740

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/python.go` at line 308, The file
internal/schemas/generator/python.go uses os.ModePerm (0o777) for writing .py
and __init__.py files; update those WriteFile calls to use a restrictive file
mode (0644) to match finalizePythonSchemas and avoid world-writable/executable
source files—replace os.ModePerm with 0644 in the WriteFile invocations found in
this file (including the call at the shown site and the other flagged call
sites) so Python source files are created with correct permissions.

50-50: 💤 Low value

Hardcoded code-generator image / tag — any plan for overrides?

pythonImage = "docker.io/koxudaxi/datamodel-code-generator:0.31.2" is pinned here, which is great for reproducibility, but it's the only knob users have if they're behind an internal registry / mirror or want to bump versions ahead of a CLI release. Have you considered surfacing this via an env var or a flag on the dependency/schema commands (similar to how other Crossplane CLI tools allow overriding image references)? Not blocking — just flagging in case it's already on the roadmap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/python.go` at line 50, The pythonImage constant is
hardcoded which prevents overrides; make it configurable by reading an
environment variable (e.g., DATAGEN_PYTHON_IMAGE) or by wiring a CLI flag into
the same code path that uses pythonImage so callers can override it—keep the
existing value "docker.io/koxudaxi/datamodel-code-generator:0.31.2" as the
default when the env/flag is empty; update references to pythonImage (the
pythonImage variable and any functions that consume it) to use the resolved
value so users behind internal registries or wanting newer tags can override it.

602-602: 💤 Low value

Unused named return openapiFolder.

normalizeAndFilterPath declares openapiFolder as a named return, but every caller discards it with _. If it's not part of the intended API, we could drop it from the signature to keep the surface area honest; if it is, we should probably use it (or document why it's exposed). Tiny thing — feel free to ignore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/python.go` at line 602, The function
normalizeAndFilterPath currently declares an unused named return openapiFolder;
remove this from the signature and return only (normalizedParts []string,
include bool) to shrink the API surface, then update all callers (which
currently do _, normalizedParts, include := normalizeAndFilterPath(...)) to
capture the two returned values (e.g. normalizedParts, include :=
normalizeAndFilterPath(...)); alternatively, if openapiFolder was meant to be
used, populate and return it from normalizeAndFilterPath and update callers to
use it—pick one approach and make caller sites and the function signature
consistent.

267-268: 💤 Low value

--target-python-version 3.12 vs requires-python = ">=3.11,<3.14".

The generator is told to target 3.12, but the pyproject.toml we emit declares support for 3.11–3.13. In practice 3.12 output is usually backward-compatible to 3.11 and forward-compatible to 3.13, but if datamodel-code-generator ever starts emitting 3.12-only syntax (e.g., new type statement uses), 3.11 consumers would break. Would it make sense to either (a) target 3.11 here to match the floor of requires-python, or (b) tighten requires-python to >=3.12? Curious which direction matches the intended user base.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/python.go` around lines 267 - 268, The generator
currently passes "--target-python-version", "3.12" while the emitted
pyproject.toml sets requires-python = ">=3.11,<3.14", creating a mismatch;
update the generator to target "3.11" (replace the
"--target-python-version","3.12" literal in
internal/schemas/generator/python.go) so the datamodel-code-generator invocation
aligns with the pyproject's minimum Python version, or alternatively tighten the
emitted requires-python to ">=3.12" if you intend to require 3.12+—pick one
consistent change and apply it to the corresponding symbol(s)
("--target-python-version" value or the generated requires-python string) so
both agree.

46-49: 💤 Low value

pythonModelsFolder and pythonPackageRoot are both "models" — a brief comment would help future spelunkers.

It took me a second to realize that filepath.Join(pythonModelsFolder, pythonPackageRoot) deliberately produces models/models (outer dir holds pyproject.toml, inner dir is the importable Python package referenced by packages = ["models"] in the pyproject). Could we add a short comment on these constants explaining the two-layer layout? Would save the next reader a trip through finalizePythonSchemas.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/schemas/generator/python.go` around lines 46 - 49, Add a short
clarifying comment next to the constants pythonModelsFolder and
pythonPackageRoot explaining that the project uses a two-layer layout: the outer
folder (pythonModelsFolder) contains pyproject.toml and build metadata while the
inner folder (pythonPackageRoot) is the actual importable Python package (hence
filepath.Join(pythonModelsFolder, pythonPackageRoot) producing models/models),
and mention that finalizePythonSchemas relies on this structure; update the
comment near pythonModelsFolder, pythonPackageRoot (and optionally
pythonGeneratedFolder) to briefly state this intent so future readers understand
the models/models layout without tracing finalizePythonSchemas.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/crossplane/project/push.go`:
- Line 84: The code unguardedly asserts http.DefaultTransport.(*http.Transport)
into t which can panic if DefaultTransport is replaced; change it to a safe type
assertion (e.g., transport, ok := http.DefaultTransport.(*http.Transport)) and
handle the !ok case by returning a clear CLI error (or logging and exiting)
instead of allowing a panic; update the instance in
cmd/crossplane/project/push.go (the variable t) and make the same defensive
change at the matching site in cmd/crossplane/xpkg/push.go (around the transport
clone there) so both places validate the type before cloning.

In `@internal/project/functions/build_test.go`:
- Line 146: Tests that call NewStaticImageConfigStore (e.g., TestIdentify and
the two other tests flagged) are being run with t.Parallel(), causing races
around AddToScheme/static store initialization; fix by removing t.Parallel()
from those tests so they run serially, or alternatively make
NewStaticImageConfigStore/AddToScheme safe for concurrent use (e.g., protect
static initialization with sync.Once or a package mutex inside
NewStaticImageConfigStore) and update the tests accordingly.
- Around line 295-330: The reader returned by layer.Uncompressed() (rc) must be
closed and the return value from afero.Walk must be checked; after rc, call
rc.Close() via defer immediately after successful Uncompressed() (i.e., right
after rc, err := layer.Uncompressed()) to ensure the tar reader is closed, and
capture the error returned by afero.Walk(...) (replace the current ignored
return) and call t.Fatal(err) if non-nil so traversal failures are not
swallowed; reference layer.Uncompressed, rc, and afero.Walk in the change.

In `@internal/schemas/generator/python.go`:
- Around line 719-741: transformMetaImportsInFile currently drops scanner.Err()
and loses a trailing newline; extract a helper transformLines(fs, filePath,
transformFunc) that encapsulates bufio.Scanner buffer size/config, checks and
returns scanner.Err(), preserves the file's trailing newline when writing, and
returns an error; update transformMetaImportsInFile and adjustImportsInFile to
call transformLines passing a function that applies transformMetaImport and
adjustRelativeImportsForCoreMeta (or the existing import-adjust logic) so both
functions reuse consistent scanning, error handling, and newline-preservation
behavior.
- Around line 209-213: The current code silently falls back to the original
bytes when processedDoc.MarshalJSON() fails, losing the injected apiVersion/kind
from processOpenAPIContent without notifying callers; update the error handling
in the block around processedDoc.MarshalJSON() so that MarshalJSON errors are
not ignored—either return/propagate the error up to the caller (so callers know
enrichment failed) or at minimum log a warning including the error and the
filename/identifier before deciding to use bs; modify the logic in the function
that calls processOpenAPIContent (and the processedContent/bs handling) to
propagate the error path (or emit the logger call) rather than silently
continuing.
- Around line 399-418: The scanner-based rewrite in adjustImportsInFile (and the
similar transformMetaImportsInFile) must handle scanner errors and preserve the
original file's trailing newline: after scanning, check scanner.Err() and return
a wrapped error if non-nil (this prevents silent truncation on read errors or
bufio.MaxScanTokenSize hits), and when writing back, detect whether the original
fileContent ended with a newline and, if so, append a terminating "\n" to the
joined modifiedContent; alternatively replace the whole file body using a single
regexp on string(fileContent) (safer for long lines) instead of line-by-line
scanning — update functions adjustImportsInFile and transformMetaImportsInFile
accordingly and extract any shared logic into a small helper to avoid
duplication.

---

Nitpick comments:
In `@internal/kcl/import_test.go`:
- Around line 28-31: The test table `tests` in import_test.go should follow the
repo convention by replacing the current shape (paths []string, wantImports
map[string]string) with the args/want/reason pattern; change each case to:
struct{ args struct{ paths []string }; want map[string]string; reason string }
and move existing `paths` into `args.paths`, `wantImports` into `want`, and add
a short `reason` for each case, then update all references in the test loop that
read `paths`/`wantImports` to use `tc.args.paths` and `tc.want` respectively
(look for the `tests` variable and usages in the test function to modify).

In `@internal/project/functions/build_test.go`:
- Around line 121-127: Replace the brittle exact string comparisons in the
test's error assertions by adding an args/want-style error entry to the test
case table (e.g., extend each tc with a wantErr error or nil) and then assert
using cmp.Diff(err, tc.wantErr, cmpopts.EquateErrors()) (instead of comparing
err.Error() to a literal). Locate the block using tc.expectError/err in
build_test.go, remove the direct string match ("no suitable builder found") and
use cmp.Diff with cmpopts.EquateErrors() to report mismatches; ensure imports
include "github.com/google/go-cmp/cmp" and
"github.com/google/go-cmp/cmp/cmpopts".

In `@internal/schemas/generator/python.go`:
- Line 400: The current depth calculation uses strings.Count(filePath,
string(os.PathSeparator)) which counts separators in the full filesystem path
and can vary with in-memory FS layout; change depth to be measured relative to
the Python package root (e.g., pythonPackageRoot / value from
pythonAdoptModelsStructure or postTransformCRD) by computing the path relative
to that package root (use filepath.Rel or trim the package root prefix) and then
count separators in the relative path before calling adjustLeadingDots; update
the code that sets depth (the variable named depth near filePath) to derive the
relative path first and fall back to the original behavior only if computing the
relative path fails.
- Line 308: The file internal/schemas/generator/python.go uses os.ModePerm
(0o777) for writing .py and __init__.py files; update those WriteFile calls to
use a restrictive file mode (0644) to match finalizePythonSchemas and avoid
world-writable/executable source files—replace os.ModePerm with 0644 in the
WriteFile invocations found in this file (including the call at the shown site
and the other flagged call sites) so Python source files are created with
correct permissions.
- Line 50: The pythonImage constant is hardcoded which prevents overrides; make
it configurable by reading an environment variable (e.g., DATAGEN_PYTHON_IMAGE)
or by wiring a CLI flag into the same code path that uses pythonImage so callers
can override it—keep the existing value
"docker.io/koxudaxi/datamodel-code-generator:0.31.2" as the default when the
env/flag is empty; update references to pythonImage (the pythonImage variable
and any functions that consume it) to use the resolved value so users behind
internal registries or wanting newer tags can override it.
- Line 602: The function normalizeAndFilterPath currently declares an unused
named return openapiFolder; remove this from the signature and return only
(normalizedParts []string, include bool) to shrink the API surface, then update
all callers (which currently do _, normalizedParts, include :=
normalizeAndFilterPath(...)) to capture the two returned values (e.g.
normalizedParts, include := normalizeAndFilterPath(...)); alternatively, if
openapiFolder was meant to be used, populate and return it from
normalizeAndFilterPath and update callers to use it—pick one approach and make
caller sites and the function signature consistent.
- Around line 267-268: The generator currently passes "--target-python-version",
"3.12" while the emitted pyproject.toml sets requires-python = ">=3.11,<3.14",
creating a mismatch; update the generator to target "3.11" (replace the
"--target-python-version","3.12" literal in
internal/schemas/generator/python.go) so the datamodel-code-generator invocation
aligns with the pyproject's minimum Python version, or alternatively tighten the
emitted requires-python to ">=3.12" if you intend to require 3.12+—pick one
consistent change and apply it to the corresponding symbol(s)
("--target-python-version" value or the generated requires-python string) so
both agree.
- Around line 46-49: Add a short clarifying comment next to the constants
pythonModelsFolder and pythonPackageRoot explaining that the project uses a
two-layer layout: the outer folder (pythonModelsFolder) contains pyproject.toml
and build metadata while the inner folder (pythonPackageRoot) is the actual
importable Python package (hence filepath.Join(pythonModelsFolder,
pythonPackageRoot) producing models/models), and mention that
finalizePythonSchemas relies on this structure; update the comment near
pythonModelsFolder, pythonPackageRoot (and optionally pythonGeneratedFolder) to
briefly state this intent so future readers understand the models/models layout
without tracing finalizePythonSchemas.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5cb6ef39-0822-43ea-8456-fa21a0a6a271

📥 Commits

Reviewing files that changed from the base of the PR and between f4f09ed and 7108e55.

⛔ Files ignored due to path filters (31)
  • apis/dev/v1alpha1/zz_generated.deepcopy.go is excluded by !**/zz_generated*.go and included by **/*.go
  • cmd/crossplane/function/templates/go-templating/00-prelude.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go-templating/01-compose.yaml.gotmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/go.tar is excluded by !**/*.tar and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.lock is excluded by !**/*.lock and included by none
  • cmd/crossplane/function/templates/kcl/kcl.mod.tmpl is excluded by none and included by none
  • cmd/crossplane/function/templates/kcl/main.k is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__init__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/__version__.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/fn.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/function/main.py is excluded by none and included by none
  • cmd/crossplane/function/templates/python/pyproject.toml.tmpl is excluded by none and included by none
  • go.mod is excluded by none and included by none
  • go.sum is excluded by !**/*.sum and included by none
  • gomod2nix.toml is excluded by none and included by none
  • hack/boilerplate.go.txt is excluded by none and included by none
  • internal/crd/testdata/claimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/crd/testdata/unclaimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/project/functions/testdata/go-templating-function/resource1.yaml.gotmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/go-templating-function/resource2.yaml.tmpl is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod is excluded by !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/kcl.mod.lock is excluded by !**/*.lock, !**/testdata/** and included by none
  • internal/project/functions/testdata/kcl-function/main.k is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/account_scaffold_composition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/account_scaffold_definition.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/api__v1_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/api_openapi.json is excluded by !**/testdata/** and included by none
  • internal/schemas/generator/testdata/azure_linux_function_app.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/generator/testdata/configuration_crossplane.yaml is excluded by !**/testdata/** and included by **/*.yaml
  • internal/schemas/runner/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
📒 Files selected for processing (94)
  • apis/dev/v1alpha1/defaults.go
  • apis/dev/v1alpha1/doc.go
  • apis/dev/v1alpha1/project_types.go
  • apis/dev/v1alpha1/validate.go
  • apis/dev/v1alpha1/validate_test.go
  • cmd/crossplane/composition/composition.go
  • cmd/crossplane/composition/generate.go
  • cmd/crossplane/composition/generate_test.go
  • cmd/crossplane/dependency/add.go
  • cmd/crossplane/dependency/auth.go
  • cmd/crossplane/dependency/cache.go
  • cmd/crossplane/dependency/dependency.go
  • cmd/crossplane/function/function.go
  • cmd/crossplane/function/generate.go
  • cmd/crossplane/function/generate_test.go
  • cmd/crossplane/function/pipeline.go
  • cmd/crossplane/function/templates/python/README.md
  • cmd/crossplane/main.go
  • cmd/crossplane/project/build.go
  • cmd/crossplane/project/init.go
  • cmd/crossplane/project/project.go
  • cmd/crossplane/project/push.go
  • cmd/crossplane/project/run.go
  • cmd/crossplane/project/stop.go
  • cmd/crossplane/render/op/cmd.go
  • cmd/crossplane/render/op/cmd_test.go
  • cmd/crossplane/render/xr/cmd.go
  • cmd/crossplane/render/xr/cmd_test.go
  • cmd/crossplane/xrd/generate.go
  • cmd/crossplane/xrd/generate_test.go
  • cmd/crossplane/xrd/xrd.go
  • generate.go
  • internal/async/event.go
  • internal/crd/convert.go
  • internal/crd/convert_test.go
  • internal/crd/generator.go
  • internal/crd/generator_test.go
  • internal/dependency/cache_dir.go
  • internal/dependency/manager.go
  • internal/dependency/manager_test.go
  • internal/docker/docker.go
  • internal/filesystem/filesystem.go
  • internal/filesystem/filesystem_test.go
  • internal/git/git.go
  • internal/git/git_test.go
  • internal/kcl/import.go
  • internal/kcl/import_test.go
  • internal/project/build.go
  • internal/project/build_test.go
  • internal/project/certs/cert_generator.go
  • internal/project/certs/tls.go
  • internal/project/controlplane/controlplane.go
  • internal/project/functions/build.go
  • internal/project/functions/build_test.go
  • internal/project/functions/go.go
  • internal/project/functions/go_templating.go
  • internal/project/functions/kcl.go
  • internal/project/functions/python.go
  • internal/project/helm/helm.go
  • internal/project/helm/restclientgetter.go
  • internal/project/image.go
  • internal/project/install.go
  • internal/project/install_test.go
  • internal/project/projectfile/projectfile.go
  • internal/project/projectfile/projectfile_test.go
  • internal/project/push.go
  • internal/project/push_test.go
  • internal/project/render.go
  • internal/project/sort.go
  • internal/schemas/generator/go.go
  • internal/schemas/generator/go_test.go
  • internal/schemas/generator/interface.go
  • internal/schemas/generator/json.go
  • internal/schemas/generator/json_test.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/kcl_test.go
  • internal/schemas/generator/python.go
  • internal/schemas/generator/python_test.go
  • internal/schemas/manager/lock.go
  • internal/schemas/manager/manager.go
  • internal/schemas/manager/manager_test.go
  • internal/schemas/manager/source.go
  • internal/schemas/manager/source_test.go
  • internal/schemas/runner/run.go
  • internal/schemas/runner/run_test.go
  • internal/terminal/spinner.go
  • internal/xpkg/client.go
  • internal/xpkg/doc.go
  • internal/xpkg/fetcher.go
  • internal/xpkg/metadata.go
  • internal/xpkg/resolver.go
  • internal/xpkg/resolver_test.go
  • internal/xrd/infer.go
  • internal/xrd/infer_test.go
✅ Files skipped from review due to trivial changes (1)
  • cmd/crossplane/function/templates/python/README.md
🚧 Files skipped from review as they are similar to previous changes (69)
  • cmd/crossplane/function/function.go
  • generate.go
  • cmd/crossplane/xrd/xrd.go
  • internal/schemas/generator/kcl_test.go
  • internal/crd/generator_test.go
  • internal/dependency/cache_dir.go
  • cmd/crossplane/dependency/auth.go
  • internal/project/sort.go
  • internal/schemas/generator/go_test.go
  • cmd/crossplane/project/project.go
  • internal/project/build_test.go
  • apis/dev/v1alpha1/doc.go
  • cmd/crossplane/composition/composition.go
  • cmd/crossplane/main.go
  • internal/schemas/generator/json_test.go
  • cmd/crossplane/function/pipeline.go
  • internal/async/event.go
  • cmd/crossplane/function/generate_test.go
  • internal/project/render.go
  • internal/docker/docker.go
  • internal/project/projectfile/projectfile.go
  • internal/project/functions/build.go
  • cmd/crossplane/dependency/dependency.go
  • cmd/crossplane/render/op/cmd_test.go
  • internal/crd/convert_test.go
  • internal/crd/convert.go
  • internal/project/functions/go.go
  • cmd/crossplane/dependency/cache.go
  • internal/project/certs/tls.go
  • internal/project/push_test.go
  • internal/schemas/generator/json.go
  • apis/dev/v1alpha1/validate.go
  • internal/project/projectfile/projectfile_test.go
  • apis/dev/v1alpha1/validate_test.go
  • internal/project/functions/kcl.go
  • internal/project/functions/go_templating.go
  • cmd/crossplane/project/stop.go
  • internal/git/git.go
  • apis/dev/v1alpha1/defaults.go
  • cmd/crossplane/xrd/generate_test.go
  • cmd/crossplane/dependency/add.go
  • apis/dev/v1alpha1/project_types.go
  • internal/project/functions/python.go
  • cmd/crossplane/function/generate.go
  • internal/project/install_test.go
  • internal/project/certs/cert_generator.go
  • internal/dependency/manager_test.go
  • internal/project/image.go
  • internal/project/helm/restclientgetter.go
  • cmd/crossplane/composition/generate.go
  • cmd/crossplane/render/op/cmd.go
  • cmd/crossplane/xrd/generate.go
  • internal/project/push.go
  • internal/schemas/generator/kcl.go
  • internal/schemas/generator/interface.go
  • internal/crd/generator.go
  • cmd/crossplane/project/build.go
  • internal/git/git_test.go
  • internal/filesystem/filesystem.go
  • internal/project/controlplane/controlplane.go
  • internal/project/build.go
  • internal/project/helm/helm.go
  • internal/dependency/manager.go
  • internal/project/install.go
  • internal/filesystem/filesystem_test.go
  • internal/schemas/generator/go.go
  • cmd/crossplane/composition/generate_test.go
  • cmd/crossplane/render/xr/cmd.go
  • cmd/crossplane/project/run.go

Comment thread cmd/crossplane/project/push.go
Comment thread internal/project/functions/build_test.go
Comment thread internal/project/functions/build_test.go
Comment thread internal/schemas/generator/python.go
Comment thread internal/schemas/generator/python.go
Comment thread internal/schemas/generator/python.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (5)
cmd/crossplane/project/push.go (4)

61-64: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add user-friendly context to the error message.

If filepath.Abs fails, the user won't know which path caused the issue or why. Consider wrapping with context about what the command was attempting to do.

🛡️ Proposed fix to add error context
 	projFilePath, err := filepath.Abs(c.ProjectFile)
 	if err != nil {
-		return err
+		return errors.Wrapf(err, "failed to resolve project file path %q", c.ProjectFile)
 	}

As per coding guidelines: "CRITICAL: Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/push.go` around lines 61 - 64, The call to
filepath.Abs(c.ProjectFile) returns a raw error with no user-friendly context;
update the error handling around that call (the projFilePath, err :=
filepath.Abs(c.ProjectFile) block) to wrap the error with a clear message that
includes the attempted path (c.ProjectFile) and a brief next step, e.g., using
fmt.Errorf or errors.Wrapf to produce: "failed to resolve absolute path for
project file '<path>': <err>; please check the path and permissions." Ensure the
wrapped error is returned in place of the raw err.

99-105: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the error message more user-friendly.

The error "failed to parse repository" uses technical jargon and doesn't guide the user. When a user provides an invalid --repository value, they need to know what format is expected.

💬 Proposed fix for clearer error message
 	if c.Repository != "" {
 		ref, err := name.NewRepository(c.Repository)
 		if err != nil {
-			return errors.Wrap(err, "failed to parse repository")
+			return errors.Wrapf(err, "invalid repository %q: must be a valid OCI repository reference (e.g., ghcr.io/crossplane/example)", c.Repository)
 		}
 		c.proj.Spec.Repository = ref.String()
 	}

As per coding guidelines: "CRITICAL: Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible. CLI error messages must be especially user-friendly."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/push.go` around lines 99 - 105, The error returned
when parsing c.Repository via name.NewRepository is too technical; update the
error handling in the block that calls name.NewRepository (where c.Repository is
parsed and assigned to c.proj.Spec.Repository) to return a user-friendly message
that explains the expected repository format (e.g.,
"registry.example.com/my-repo[:tag]" or "host/namespace/repo"), includes the
invalid value provided, and suggests how to fix it (for example "provide a valid
container registry repository like 'ghcr.io/org/repo'"). Keep the original
wrapped error for debugging but present the clearer message to the end user.

148-177: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Improve error messages for better user experience.

The error messages in this function use technical jargon ("manifest", "parse") and don't help users understand what went wrong or what to check. When a user encounters these errors, they need actionable guidance.

💬 Proposed fix for clearer error messages
 	mfst, err := tarball.LoadManifest(opener)
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to read package file manifest")
+		return nil, errors.Wrapf(err, "failed to read package file %q: ensure it is a valid .xpkg file created by 'crossplane project build'", c.PackageFile)
 	}
 
 	imgMap := make(project.ImageTagMap)
 	for _, desc := range mfst {
 		if len(desc.RepoTags) == 0 {
 			// Ignore images with no tags; we shouldn't find these in xpkg
 			// files, but best not to panic if it happens.
 			continue
 		}
 
 		tag, err := name.NewTag(desc.RepoTags[0])
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to parse image tag %q", desc.RepoTags[0])
+			return nil, errors.Wrapf(err, "invalid image tag %q in package file: the package may be corrupted", desc.RepoTags[0])
 		}
 		image, err := tarball.Image(opener, &tag)
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to load image %q from package", tag)
+			return nil, errors.Wrapf(err, "failed to load image %q from package file: the package may be corrupted or incomplete", tag)
 		}
 		imgMap[tag] = image
 	}

As per coding guidelines: "CRITICAL: Ensure all error messages are meaningful to end users, not just developers - avoid technical jargon, include context about what the user was trying to do, and suggest next steps when possible. CLI error messages must be especially user-friendly."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/push.go` around lines 148 - 177, The error messages in
loadPackages should be rewritten to be user-facing and actionable: when
tarball.LoadManifest(opener) fails (currently wrapped with "failed to read
package file manifest"), change it to a clear message like "cannot open package
file '<PackageFile>' — ensure the file exists and is a valid package archive"
and include the original error; when name.NewTag(desc.RepoTags[0]) fails
(currently "failed to parse image tag"), replace with "invalid image tag '<tag>'
in package — check image name format (e.g. repo:tag) in the package" and include
the underlying error; when tarball.Image(opener, &tag) fails (currently "failed
to load image ... from package"), replace with "cannot load image '<tag>' from
package — package may be corrupted or missing image layers" and include the
original error; update the errors.Wrapf calls around these sites (loadPackages,
tarball.LoadManifest, name.NewTag, tarball.Image) to use these friendlier
messages while still wrapping the original error for diagnostics.

84-84: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard the http.DefaultTransport type assertion to prevent a panic if the transport is ever replaced.

While http.DefaultTransport is always *http.Transport in Go's standard library, a defensive guard here is good practice—especially since this is public CLI code. Note that the same pattern appears in cmd/crossplane/xpkg/push.go:126 and should be addressed consistently for better resilience.

🛡️ Suggested fix
-	t := http.DefaultTransport.(*http.Transport).Clone() //nolint:forcetypeassert // http.DefaultTransport is always *http.Transport
+	baseTransport, ok := http.DefaultTransport.(*http.Transport)
+	if !ok {
+		return errors.New("failed to prepare HTTP transport: unexpected default transport type")
+	}
+	t := baseTransport.Clone()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/push.go` at line 84, The code unguardedly type-asserts
http.DefaultTransport to *http.Transport when creating t (t :=
http.DefaultTransport.(*http.Transport).Clone()), which can panic if
DefaultTransport is replaced; update this to perform a safe type assertion (tr,
ok := http.DefaultTransport.(*http.Transport)) and if ok set t = tr.Clone() else
create a sensible fallback transport (e.g., t = http.DefaultTransport or
&http.Transport{} configured as needed) to avoid panics; apply the same guarded
pattern to the analogous occurrence in xpkg/push.go (the type assertion at push
setup) so both places safely handle non-*http.Transport DefaultTransport values.
internal/docker/docker.go (1)

509-512: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Zip-Slip guard appears to be a no-op — filepath.SplitList splits PATH-style lists, not path components.

Thanks for adding an explicit traversal check here! Could you double-check the choice of filepath.SplitList though? It splits strings using the OS PATH list separator (: on Unix, ; on Windows), not the path separator — so for a malicious tar entry like ../etc/passwd, after filepath.Clean you get "../etc/passwd", and filepath.SplitList("../etc/passwd") returns a single element ["../etc/passwd"], which doesn't contain a bare "..". The guard then passes and the file is happily written outside the intended destination. This also lines up with the CodeQL "Zip Slip" finding flagged on line 497.

A few thoughts on how to make this watertight — happy to iterate on the approach you prefer:

  • Use filepath.IsLocal (Go 1.20+) on the cleaned, slash-form entry name, which explicitly rejects paths that escape, are absolute, or reference reserved names.
  • Or resolve the intended absolute destination and confirm the final cleaned path stays within it (e.g. strings.HasPrefix(absChild, absRoot+string(os.PathSeparator))).

Also note that tar entries from Docker use / as the separator regardless of host OS, so mixing filepath.Clean / filepath.Base with tar-relative names can subtly misbehave on Windows; using the path package for the tar-side normalization and filepath only when writing to afero.Fs is usually safer.

🛡️ One possible fix using path for tar-side handling + filepath.IsLocal
-		cleanedPath := filepath.Clean(strings.TrimPrefix(header.Name, filepath.Base(path)+"/"))
-		if slices.Contains(filepath.SplitList(cleanedPath), "..") {
-			return errors.Errorf("file %s lives outside path %s - refusing to copy", cleanedPath, path)
-		}
+		// Tar entries always use forward slashes; normalize on the tar side.
+		rel := stdpath.Clean(strings.TrimPrefix(header.Name, stdpath.Base(path)+"/"))
+		if rel == "." || rel == "" {
+			continue
+		}
+		if !filepath.IsLocal(rel) {
+			return errors.Errorf("tar entry %q escapes destination %q - refusing to copy", header.Name, path)
+		}
+		cleanedPath := filepath.FromSlash(rel)

You'll also want stdpath "path" in the import block alongside "path/filepath".

Could you confirm the intent and run a quick check that no other call site relies on the current (broken) shape of the guard?

#!/bin/bash
# Look for other places that mirror the same SplitList-based traversal check.
rg -nP -C3 'filepath\.SplitList\([^)]+\)' 
# Look for existing usage of IsLocal so we can stay consistent if it's already in use.
rg -nP -C2 '\bfilepath\.IsLocal\b'
# Confirm CopyFromContainer callers to understand the blast radius if a malicious tar slips through.
rg -nP -C3 '\bCopyFromContainer\s*\(' --type=go

As per coding guidelines: "Use crossplane-runtime/pkg/errors for wrapping ... CRITICAL: Ensure all error messages are meaningful to end users, not just developers ... include context about what the user was trying to do, and suggest next steps when possible."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/docker/docker.go` around lines 509 - 512, The current zip-slip guard
using filepath.SplitList on cleanedPath is ineffective (it splits PATH lists,
not path components); update the check in the extraction logic that computes
cleanedPath (derived from header.Name and filepath.Base(path)) to validate
escapes correctly: normalize the tar entry using the path package (e.g.,
stdpath.Clean on header.Name), then either use filepath.IsLocal on the
converted-local path (Go 1.20+) or resolve the destination absolute path and the
would-be extraction absolute path and ensure the child path has the root as a
prefix (e.g., absChild starts with absRoot+os.PathSeparator); adjust imports to
include "path" (as stdpath) and ensure the guard references cleaned/normalized
tar-side name and rejects absolute or `..` escapes before writing, and run the
suggested ripgrep checks (CopyFromContainer, filepath.IsLocal,
filepath.SplitList) to ensure no other call sites rely on the broken check.
🧹 Nitpick comments (5)
internal/project/functions/go.go (1)

132-138: 💤 Low value

Consider documenting the pinned base image tag for easier future bumps.

Pinning by digest is great for reproducibility — thanks for that! One small nit: future maintainers (or a Renovate/Dependabot config) will have a hard time knowing which distroless tag this digest currently corresponds to when the time comes to bump it. Would you be open to a short comment noting the resolved tag (and maybe a link to the upstream image), or wiring this into your dependency-update tooling?

📝 Example annotation
 func newGoBuilder(imageConfigs []pkgv1beta1.ImageConfig) *goBuilder {
 	return &goBuilder{
+		// gcr.io/distroless/static-debian12:nonroot as of <date>. Bump the digest
+		// when a new distroless release is published.
 		baseImage:   "gcr.io/distroless/static-debian12@sha256:a9329520abc449e3b14d5bc3a6ffae065bdde0f02667fa10880c49b35c109fd1",

Totally optional — feel free to ignore if you already have tooling that tracks this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/functions/go.go` around lines 132 - 138, The pinned
distroless base image in newGoBuilder (goBuilder.baseImage) should be annotated
so future maintainers can see the resolved tag and source; add a short comment
above the baseImage assignment that states the human-readable tag (e.g.,
"distroless/static-debian12:TAG") and a link to the upstream image or release
page, and optionally note the digest used, so dependency-update tooling or
reviewers can easily map the digest to the tag when bumping.
internal/project/install_test.go (2)

133-138: ⚡ Quick win

Please standardize table case shape to args/want and include a reason field.

These tables are clear already—thank you. A small structure tweak to the canonical args/want form (plus reason) would make them fully consistent with other tests and review expectations.

As per coding guidelines "**/*_test.go: Enforce table-driven test structure: ... args/want pattern ... Ensure proper test case naming and reason fields."

Also applies to: 184-187, 210-213, 237-240

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/install_test.go` around lines 133 - 138, The table-driven
test cases in install_test.go use top-level keys
(source/constraint/wantName/wantFound); change the map values to the canonical
args/want shape and add a reason string: replace the anonymous struct with
struct { args struct { source string; constraint string }; want struct { name
string; found bool }; reason string } and update all test case entries
(including the other tables noted) to populate args.source/args.constraint,
want.name/want.found and reason; then update test logic that references tcs[...]
fields to use tc.args.source, tc.args.constraint, tc.want.name and tc.want.found
(and include tc.reason in the test name or t.Run call).

49-115: ⚡ Quick win

Could we align TestApplyResources with the repo’s table-driven test pattern?

Thanks for adding strong coverage here. Would you be open to converting this block into a single table (args/want + per-case reason) so it matches the project’s standard and is easier to extend with more apply edge cases?

As per coding guidelines "**/*_test.go: Enforce table-driven test structure: ... args/want pattern ... Ensure proper test case naming and reason fields."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/project/install_test.go` around lines 49 - 115, TestApplyResources
uses three separate t.Run blocks instead of the repo's required table-driven
pattern; refactor it into one table-driven test that iterates over cases with
fields like name, args (including resources and client init), want (error
expectation) and reason. Consolidate the current subtests ("AppliesConfigMaps",
"IdempotentOnExisting", "EmptyRawErrors") into entries in the table, reuse
helpers such as configMapJSON and newFakeClient, and call
ApplyResources(t.Context(), cl, resources) inside the loop while using
t.Run(tc.name, func(t *testing.T){...}) for each case; ensure each case asserts
expected success/failure (checking created ConfigMaps or error presence) and
includes a human-readable reason field.
cmd/crossplane/project/push.go (1)

86-88: ⚡ Quick win

Consider setting a minimum TLS version for defense-in-depth.

While InsecureSkipVerify disables certificate validation when enabled, setting MinVersion: tls.VersionTLS13 in the TLS config would ensure that when certificate verification is enabled (the default case), only modern TLS versions are accepted. This follows security best practices even though it doesn't affect the insecure mode.

♻️ Suggested enhancement
 	if c.InsecureSkipTLSVerify {
 		t.TLSClientConfig = &tls.Config{
 			InsecureSkipVerify: true, //nolint:gosec // we need to support insecure connections if requested
+			MinVersion:         tls.VersionTLS13,
 		}
+	} else {
+		t.TLSClientConfig = &tls.Config{
+			MinVersion: tls.VersionTLS13,
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/crossplane/project/push.go` around lines 86 - 88, The TLS config created
in t.TLSClientConfig currently only sets InsecureSkipVerify, so add a minimum
TLS version for defense-in-depth by setting MinVersion: tls.VersionTLS13 on the
tls.Config; update the tls.Config construction used for t.TLSClientConfig (the
tls.Config literal where InsecureSkipVerify is set) to include MinVersion:
tls.VersionTLS13 so that when certificate verification is enabled modern TLS is
required.
internal/docker/docker.go (1)

516-533: ⚡ Quick win

Wrap filesystem errors with user-facing context.

These error returns currently propagate raw afero errors (MkdirAll, Create, io.Copy) without any indication of what the user was doing or which file was involved — a bit of a debugging cliff for end users. Could we wrap them, similar to the rest of the file? Something like:

♻️ Suggested wrapping
 		case tar.TypeDir:
-			if err := fs.MkdirAll(cleanedPath, 0o755); err != nil {
-				return err
-			}
+			if err := fs.MkdirAll(cleanedPath, 0o755); err != nil {
+				return errors.Wrapf(err, "failed to create directory %q while extracting from container", cleanedPath)
+			}
 		case tar.TypeReg:
-			outFile, err := fs.Create(cleanedPath)
-			if err != nil {
-				return err
-			}
+			outFile, err := fs.Create(cleanedPath)
+			if err != nil {
+				return errors.Wrapf(err, "failed to create file %q while extracting from container", cleanedPath)
+			}

-			if _, err := io.Copy(outFile, tarReader); err != nil { //nolint:gosec // File size is checked above.
+			if _, err := io.Copy(outFile, tarReader); err != nil { //nolint:gosec // File size is checked above.
 				if cerr := outFile.Close(); cerr != nil {
 					err = errors.Wrap(cerr, "error while closing file")
 				}
-				return err
+				return errors.Wrapf(err, "failed to write file %q from container tarball", cleanedPath)
 			}

As per coding guidelines: "Use crossplane-runtime/pkg/errors for wrapping ... Ensure all error messages are meaningful to end users, not just developers — avoid technical jargon, include context about what the user was trying to do."

Push a commit to this branch (recommended)
  • Create a new PR with the fixes

  • ℹ️ Review info
    ⚙️ Run configuration

    Configuration used: Path: .coderabbit.yaml

    Review profile: CHILL

    Plan: Pro

    Run ID: 84d4c9ba-a012-4bf3-a733-64afc62c03cf

    📥 Commits

    Reviewing files that changed from the base of the PR and between 7108e55 and 66a8bbd.

    ⛔ Files ignored due to path filters (29)
    • cmd/crossplane/function/templates/go-templating/00-prelude.yaml.gotmpl is excluded by none and included by none
    • cmd/crossplane/function/templates/go-templating/01-compose.yaml.gotmpl is excluded by none and included by none
    • cmd/crossplane/function/templates/go.tar is excluded by !**/*.tar and included by none
    • cmd/crossplane/function/templates/kcl/kcl.mod.lock is excluded by !**/*.lock and included by none
    • cmd/crossplane/function/templates/kcl/kcl.mod.tmpl is excluded by none and included by none
    • cmd/crossplane/function/templates/kcl/main.k is excluded by none and included by none
    • cmd/crossplane/function/templates/python/function/__init__.py is excluded by none and included by none
    • cmd/crossplane/function/templates/python/function/__version__.py is excluded by none and included by none
    • cmd/crossplane/function/templates/python/function/fn.py is excluded by none and included by none
    • cmd/crossplane/function/templates/python/function/main.py is excluded by none and included by none
    • cmd/crossplane/function/templates/python/pyproject.toml.tmpl is excluded by none and included by none
    • go.mod is excluded by none and included by none
    • go.sum is excluded by !**/*.sum and included by none
    • gomod2nix.toml is excluded by none and included by none
    • internal/crd/testdata/claimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
    • internal/crd/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
    • internal/crd/testdata/unclaimable-xrd.yaml is excluded by !**/testdata/** and included by **/*.yaml
    • internal/project/functions/testdata/go-templating-function/resource1.yaml.gotmpl is excluded by !**/testdata/** and included by none
    • internal/project/functions/testdata/go-templating-function/resource2.yaml.tmpl is excluded by !**/testdata/** and included by none
    • internal/project/functions/testdata/kcl-function/kcl.mod is excluded by !**/testdata/** and included by none
    • internal/project/functions/testdata/kcl-function/kcl.mod.lock is excluded by !**/*.lock, !**/testdata/** and included by none
    • internal/project/functions/testdata/kcl-function/main.k is excluded by !**/testdata/** and included by none
    • internal/schemas/generator/testdata/account_scaffold_composition.yaml is excluded by !**/testdata/** and included by **/*.yaml
    • internal/schemas/generator/testdata/account_scaffold_definition.yaml is excluded by !**/testdata/** and included by **/*.yaml
    • internal/schemas/generator/testdata/api__v1_openapi.json is excluded by !**/testdata/** and included by none
    • internal/schemas/generator/testdata/api_openapi.json is excluded by !**/testdata/** and included by none
    • internal/schemas/generator/testdata/azure_linux_function_app.yaml is excluded by !**/testdata/** and included by **/*.yaml
    • internal/schemas/generator/testdata/configuration_crossplane.yaml is excluded by !**/testdata/** and included by **/*.yaml
    • internal/schemas/runner/testdata/template.fn.crossplane.io_kclinputs.yaml is excluded by !**/testdata/** and included by **/*.yaml
    📒 Files selected for processing (88)
    • cmd/crossplane/composition/composition.go
    • cmd/crossplane/composition/generate.go
    • cmd/crossplane/composition/generate_test.go
    • cmd/crossplane/dependency/add.go
    • cmd/crossplane/dependency/auth.go
    • cmd/crossplane/dependency/cache.go
    • cmd/crossplane/dependency/dependency.go
    • cmd/crossplane/function/function.go
    • cmd/crossplane/function/generate.go
    • cmd/crossplane/function/generate_test.go
    • cmd/crossplane/function/pipeline.go
    • cmd/crossplane/function/templates/python/README.md
    • cmd/crossplane/main.go
    • cmd/crossplane/project/build.go
    • cmd/crossplane/project/init.go
    • cmd/crossplane/project/project.go
    • cmd/crossplane/project/push.go
    • cmd/crossplane/project/run.go
    • cmd/crossplane/project/stop.go
    • cmd/crossplane/render/op/cmd.go
    • cmd/crossplane/render/op/cmd_test.go
    • cmd/crossplane/render/xr/cmd.go
    • cmd/crossplane/render/xr/cmd_test.go
    • cmd/crossplane/xrd/generate.go
    • cmd/crossplane/xrd/generate_test.go
    • cmd/crossplane/xrd/xrd.go
    • internal/async/event.go
    • internal/crd/convert.go
    • internal/crd/convert_test.go
    • internal/crd/generator.go
    • internal/crd/generator_test.go
    • internal/dependency/cache_dir.go
    • internal/dependency/manager.go
    • internal/dependency/manager_test.go
    • internal/docker/docker.go
    • internal/filesystem/filesystem.go
    • internal/filesystem/filesystem_test.go
    • internal/git/git.go
    • internal/git/git_test.go
    • internal/kcl/import.go
    • internal/kcl/import_test.go
    • internal/project/build.go
    • internal/project/build_test.go
    • internal/project/certs/cert_generator.go
    • internal/project/certs/tls.go
    • internal/project/controlplane/controlplane.go
    • internal/project/functions/build.go
    • internal/project/functions/build_test.go
    • internal/project/functions/go.go
    • internal/project/functions/go_templating.go
    • internal/project/functions/kcl.go
    • internal/project/functions/python.go
    • internal/project/helm/helm.go
    • internal/project/helm/restclientgetter.go
    • internal/project/image.go
    • internal/project/install.go
    • internal/project/install_test.go
    • internal/project/projectfile/projectfile.go
    • internal/project/projectfile/projectfile_test.go
    • internal/project/push.go
    • internal/project/push_test.go
    • internal/project/render.go
    • internal/project/sort.go
    • internal/schemas/generator/go.go
    • internal/schemas/generator/go_test.go
    • internal/schemas/generator/interface.go
    • internal/schemas/generator/json.go
    • internal/schemas/generator/json_test.go
    • internal/schemas/generator/kcl.go
    • internal/schemas/generator/kcl_test.go
    • internal/schemas/generator/python.go
    • internal/schemas/generator/python_test.go
    • internal/schemas/manager/lock.go
    • internal/schemas/manager/manager.go
    • internal/schemas/manager/manager_test.go
    • internal/schemas/manager/source.go
    • internal/schemas/manager/source_test.go
    • internal/schemas/runner/run.go
    • internal/schemas/runner/run_test.go
    • internal/terminal/spinner.go
    • internal/xpkg/client.go
    • internal/xpkg/doc.go
    • internal/xpkg/fetcher.go
    • internal/xpkg/metadata.go
    • internal/xpkg/resolver.go
    • internal/xpkg/resolver_test.go
    • internal/xrd/infer.go
    • internal/xrd/infer_test.go
    ✅ Files skipped from review due to trivial changes (1)
    • internal/dependency/cache_dir.go
    🚧 Files skipped from review as they are similar to previous changes (64)
    • cmd/crossplane/function/templates/python/README.md
    • internal/schemas/generator/json_test.go
    • internal/schemas/generator/kcl_test.go
    • cmd/crossplane/xrd/xrd.go
    • cmd/crossplane/function/function.go
    • cmd/crossplane/dependency/auth.go
    • internal/project/push_test.go
    • cmd/crossplane/composition/composition.go
    • cmd/crossplane/project/stop.go
    • internal/schemas/generator/go_test.go
    • cmd/crossplane/render/op/cmd_test.go
    • cmd/crossplane/dependency/dependency.go
    • internal/crd/generator.go
    • internal/schemas/generator/interface.go
    • cmd/crossplane/composition/generate_test.go
    • internal/kcl/import_test.go
    • cmd/crossplane/main.go
    • cmd/crossplane/project/project.go
    • internal/project/projectfile/projectfile_test.go
    • internal/crd/generator_test.go
    • cmd/crossplane/function/pipeline.go
    • cmd/crossplane/xrd/generate_test.go
    • cmd/crossplane/project/init.go
    • internal/git/git_test.go
    • cmd/crossplane/function/generate.go
    • cmd/crossplane/dependency/cache.go
    • internal/project/image.go
    • cmd/crossplane/render/xr/cmd_test.go
    • cmd/crossplane/project/run.go
    • internal/async/event.go
    • internal/project/functions/python.go
    • internal/project/push.go
    • cmd/crossplane/dependency/add.go
    • internal/project/functions/build_test.go
    • cmd/crossplane/render/op/cmd.go
    • internal/project/projectfile/projectfile.go
    • internal/project/helm/restclientgetter.go
    • internal/schemas/generator/json.go
    • internal/project/certs/tls.go
    • internal/crd/convert_test.go
    • internal/kcl/import.go
    • cmd/crossplane/function/generate_test.go
    • internal/project/build.go
    • internal/project/helm/helm.go
    • internal/project/functions/build.go
    • internal/project/render.go
    • internal/project/install.go
    • internal/dependency/manager_test.go
    • internal/schemas/generator/python.go
    • internal/project/functions/go_templating.go
    • internal/crd/convert.go
    • internal/project/build_test.go
    • internal/project/functions/kcl.go
    • internal/dependency/manager.go
    • internal/project/controlplane/controlplane.go
    • internal/project/certs/cert_generator.go
    • cmd/crossplane/project/build.go
    • cmd/crossplane/composition/generate.go
    • internal/filesystem/filesystem.go
    • internal/git/git.go
    • cmd/crossplane/xrd/generate.go
    • internal/filesystem/filesystem_test.go
    • internal/schemas/generator/kcl.go
    • internal/schemas/generator/go.go

    Comment thread cmd/crossplane/render/xr/cmd.go Outdated
    Comment on lines +196 to +198
    func (c *Cmd) Run(k *kong.Context, log logging.Logger, sp terminal.SpinnerPrinter) error { //nolint:gocognit // Orchestration is inherently complex.
    ctx, cancel := context.WithTimeout(context.Background(), c.Timeout)
    defer cancel()
    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

    Consider starting the timeout after project bootstrap.

    Thanks for consolidating the context handling. The new placement means the default 1m budget now starts before loadFunctions(), so project-mode renders spend that same timeout on dependency resolution, schema generation, and embedded-function builds before the render engine even starts. On a cold cache or first build, that makes deadline exceeded a pretty likely first-run failure. Could we either start the timeout after function loading or split build and render budgets? As per coding guidelines **/cmd/**: "Ask about backward compatibility and user experience."

    Also applies to: 242-243, 506-530

    🤖 Prompt for AI Agents
    Verify each finding against current code. Fix only still-valid issues, skip the
    rest with a brief reason, keep changes minimal, and validate.
    
    In `@cmd/crossplane/render/xr/cmd.go` around lines 196 - 198, The timeout is being
    started at the top of Cmd.Run which lets the default c.Timeout expire during
    pre-render work (loadFunctions, dependency resolution, schema/gen, builds);
    change Run so the initial phase runs with a non-deadlined context
    (context.Background or ctx from kong) and only create context.WithTimeout for
    the actual render phase (after loadFunctions() completes) or else introduce two
    distinct timeouts (e.g., buildTimeout for loadFunctions/builds and renderTimeout
    for the render engine) and wire them where used; update references around
    Cmd.Run (the Run method), and the call sites that use the ctx for loadFunctions,
    dependency resolution, and render so that build steps use the pre-timeout
    context and only rendering uses the timed ctx.
    

    Comment thread internal/docker/docker.go
    Comment thread internal/docker/docker.go
    Comment thread internal/project/sort.go
    This commit introduces the initial set of DevEx tools based around the concept
    of control plane Projects. Specifically, it introduces tools for:
    
    - Scaffolding a new, empty project.
    - Generating XRDs from example XRs or simpleschema documents.
    - Scaffolding compositions.
    - Scaffolding functions in go, go-templating, kcl, and python and adding them to
      composition pipelines.
    - Managing both runtime and build-time dependencies for a project, including
      generating language bindings (schemas) for added dependencies.
    - Building a project into a set of xpkgs.
    - Pushing xpkgs built from a project to an OCI registry.
    - Installing a project into a local control plane (created using kind) for
      testing.
    
    Fixes crossplane/crossplane#6840
    
    This work is based on (and imports a large amount of code from) the developer
    experience tooling originally built as part of the proprietary `up` CLI at
    Upbound. The following people contributed to that work:
    
    Co-authored-by: Christopher Haar <christopher.haar@upbound.io>
    Co-authored-by: Hasan Turken <turkenh@gmail.com>
    Co-authored-by: Jason Tang <jason@upbound.io>
    Co-authored-by: Murph <murph@opusdavei.org>
    Co-authored-by: Nic Cope <nicc@rk0n.org>
    Co-authored-by: Nicholas Thomson <RedbackThomson@users.noreply.github.com>
    Co-authored-by: Philippe Scorsolini <p.scorsolini@gmail.com>
    Co-authored-by: Tobias Kässer <tobias.kasser@upbound.io>
    Co-authored-by: nullable-eth <2248325+nullable-eth@users.noreply.github.com>
    Co-authored-by: tnthornton <2375126+tnthornton@users.noreply.github.com>
    Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
    When running render in a project directory, allow the functions argument to be
    omitted, resolving external functions based on the project's dependencies and
    building embedded functions.
    
    Signed-off-by: Adam Wolfe Gordon <awg@upbound.io>
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    Proposal: Contribute the Upbound developer experience tooling to Crossplane

    3 participants