Skip to content

Conversation

@sergenyalcin
Copy link
Member

@sergenyalcin sergenyalcin commented Jan 12, 2026

Description of your changes

This PR adds infrastructure for managing the lifecycle of Managed Resource API versions, in accordance with Kubernetes deprecation policies.

  1. New configurations:
  • ServedVersions: Controls which API versions the API server exposes
  • DeprecatedVersions: Maps versions to deprecation metadata (warning messages, release info)
  • GetServedVersions(): Provides safe access with proper defaulting behavior
  1. CRD generation enhancements:
  • Generates +kubebuilder:deprecatedversion:warning markers for deprecated versions
  • Generates +kubebuilder:unservedversion markers for removed versions
  • Adds deprecation notices to CRD descriptions
  1. Post-generation hooks:
  • NewVersionMarkerUpdateHook(): Updates lifecycle markers in previous version files
  • NewStorageVersionMarkerUpdateHook(): Ensures only the configured storage version has the marker
  • Required because previous version files aren't regenerated during normal pipeline execution

I have:

  • Read and followed Upjet's contribution process.
  • Run make reviewable to ensure this PR is ready for review.
  • Added backport release-x.y labels to auto-backport this PR if necessary.

How has this code been tested

Tested in the provider-upjet azuread and provider-upjet-gcp. Observed successful generations. Deprecation and serving version markers were observed. There are no unintended changes.

Please see the provider branches that consume this change:

Summary by CodeRabbit

  • New Features
    • Added API version deprecation support with customizable warnings and planned removal dates.
    • Introduced explicit control over served API versions with validation.
    • Enhanced CRD generation with automatic deprecation notices, warnings, and unserved version markers for clearer API lifecycle management.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
@sergenyalcin sergenyalcin marked this pull request as ready for review January 12, 2026 05:33
@sergenyalcin sergenyalcin changed the title Introduce new API version management configuration APIs for MRs Add new configuration APIs/mechanisms for starting to introduce new MR API version policy Jan 12, 2026
Copy link
Collaborator

@ulucinar ulucinar left a comment

Choose a reason for hiding this comment

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

Thank you @sergenyalcin, leaving some comments for us to discuss.

// buildEnhancedDeprecationWarning creates an enhanced deprecation warning message
// by appending release information to the base warning message if available.
func buildEnhancedDeprecationWarning(deprecation config.VersionDeprecation) string {
warning := deprecation.Warning
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
warning := deprecation.Warning
warning := strings.TrimSpace(deprecation.Warning)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

// is planned to be removed. According to the versioning policy, a version must
// remain deprecated for at least one release before removal.
// Format: "v1.3.0"
RemovalPlannedRelease string
Copy link
Collaborator

Choose a reason for hiding this comment

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

Because we've a preposition in the name DeprecatedInRelease, for consistency:

Suggested change
RemovalPlannedRelease string
RemovalPlannedForRelease string

Or something like DeprecationRelease & PlannedRemovalRelease (or just RemovalRelease concisely). I think these forms are more suitable for a declarative API btw.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

Comment on lines 450 to 456
DeprecatedInRelease string

// RemovalPlannedRelease indicates the provider release in which this version
// is planned to be removed. According to the versioning policy, a version must
// remain deprecated for at least one release before removal.
// Format: "v1.3.0"
RemovalPlannedRelease string
Copy link
Collaborator

Choose a reason for hiding this comment

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

We are coupling the specific release versions with the resource configurations I believe for the first time, optionally. I think this should be fine because:

  • It's optional. Provider authors can opt out if they don't want such coupling.
  • We are planning to drive the deprecation policy enforcement as autonomously as possible, i.e., there will be automatic configurators which utilize the information from VersionDeprecation to drive the policy as we make new releases.
  • If the provider code lives in multiple repositories (has forks), most probably they will be following the same versioning policy and have the same versions. But if there are forks, this may also not be true and if the provider author does not want to maintain this configuration (even via some sort of automatic configuration), she can still opt out using this optional configuration.

So, I think this is perfectly fine. But because we are introducing a coupling with the resource configurations and the release versions of the providers, I believe it's worth surfacing this.

We could hold this information outside of the resource configuration and specific for a provider's implementation. The current approach makes it universally available for all upjet-based providers rather than being part of a repo specific API versioning policy driver. So I believe it's worth the coupling.


// DeprecatedInRelease indicates which provider release deprecated this version.
// Format: "v1.2.3"
DeprecatedInRelease string
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
DeprecatedInRelease string
DeprecationRelease string

Please see the comment below also.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

// Version: "v1beta2"
// PreviousVersions: []string{} // v1beta1 removed
// ServedVersions: []string{"v1beta2"} // Only v1beta2 served
ServedVersions []string
Copy link
Collaborator

Choose a reason for hiding this comment

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

We must ensure that ServedVersions contains Version. It may not be a superset of the set PreviousVersions, i.e., there might be some previous versions that are no longer served. Another constraint could be if a version is not the Version or is not in PreviousVersions, then it cannot be in ServedVersions, i.e., a removed version should not exist in ServedVersions. We will also likely be serving some deprecated versions but not all of them.

We could enforce these rules with a validation, or maybe unexporting ServedVersions and implement the enforcement in accessors. That would also enforce the use of {Set,Get}ServedVersions at the API level.

Copy link
Member Author

Choose a reason for hiding this comment

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

ServedVersions unexported, added accessors and some validations.

Comment on lines 514 to 515
// Deprecated versions should still be listed in ServedVersions until they are
// completely removed from the API.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this comment correct? I think we can have some deprecated versions that are no longer served, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for catching this. Yes this comment isn't accurate. Changed it.

// - If ServedVersions is explicitly set, it's used as-is
// - If ServedVersions is empty/nil, all versions are served by default
// - The returned slice is a new slice to prevent external modifications
func (r *Resource) GetServedVersions() []string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The default implementations looks good for backward compatibility so that if a provider author does not start using the new APIs, we assume all previously configured API versions via the existing Version and PreviousVersions APIs are considered as being served.

One case we need to be careful for backward compatibility is what would happen if a provider author had multiple versions but did not bother to set the PreviousVersions? Is this possible? How do we use PreviousVersions before these changes? Can you check how we previously make use of the PreviousVersions API and leave a comment here for documentation purposes?

If some scenario like this is actually possible, i.e., the PreviousVersions API was optional even with multi-version CRDs, then we may also consider not using ServedVersions if it's not explicitly configured or making this visible in the release notes.

Copy link
Member Author

Choose a reason for hiding this comment

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

About PreviousVersions usage, I added comments to a few places. Also, I will add some statements to the release notes.

In the current situation, the PreviousVersions API is optional and managed by providers. However, if the provider wants to work everything in place (I mean, without the new configuration), they must be set. Because in the current code generation pipeline, we rely on this field being filled.

So at the API level, we don't enforce this, even if the provider has multiversion CRDs. However, in the current code-generation pipeline, we assume this is properly configured.

if deprecation.RemovalPlannedRelease != "" {
notice.WriteString(fmt.Sprintf(" and is planned for removal in release %s.", deprecation.RemovalPlannedRelease))
} else {
notice.WriteString(".")
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: If both deprecation and removal releases are empty strings, then the comment is not terminated with a ".".

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

}

// Add deprecation information if this version is deprecated
if deprecation, isDeprecated := cfg.DeprecatedVersions[cfg.Version]; isDeprecated {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we don't expect the actively being generated API version to be deprecated, right? This code path still generates the types of only the current API version, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

One use case must be this for deprecating the last (generated) API version:

The underlying TF provider deprecates some resources in minor versions and removes them in the next major version.

Until today, because we didn't have the resource-level deprecation configuration, we couldn't deprecate them on our side and directly remove resources when we bump the providers in major versions.

As you pointed, in the context of this PR, I mean in the context of multiversion lifecyle of CRDs of resources, this is not expected. However, deprecating a generated version (the latest version) is a legitimate scenario for this type of situation. So I preferred to add this.

// Check if this version should be marked as unserved
// Only mark as unserved if ServedVersions is explicitly configured
// and the current version is NOT in that list
if len(cfg.ServedVersions) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Misconfiguration like forgetting to put the current API version into the served versions would result in the current version not being served. Please also see the discussions above. I believe we had better prevent such invalid configuration cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

Added validation.

})
}

type storageVersionMarkerUpdater struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's break the implementations of these two hooks into their own files.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

})
}

type markerUpdater struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's break the implementations of these two hooks into their own files.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

})
}

type markerUpdater struct {
Copy link
Collaborator

@ulucinar ulucinar Jan 19, 2026

Choose a reason for hiding this comment

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

Because we already have a struct type declaration (markerUpdater), the type can directly implement the PostGenerationHook interface. Or, you can convert updateMarkers into a PostGenerationHookFn but I think having the type is better for breaking its implementation into multiple methods (we can make both work I think).

Please also consider doing this for storageVersionMarkerUpdater.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

})
}

type markerUpdater struct {
Copy link
Collaborator

@ulucinar ulucinar Jan 19, 2026

Choose a reason for hiding this comment

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

Would it be possible to use higher-level string functions (search/replace/regex, etc.) to simplify this processor instead of using go/{ast,parser,token} maybe with some assumptions on the shape of the generated Go files? Can you give this a try?

We can also consider this for storageVersionMarkerUpdater.

Copy link
Member Author

Choose a reason for hiding this comment

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

Now we don't use the ast, parser, etc. libraries. Because we don't change any Go code, even if these libraries are just used to ensure more robustness, as you pointed out, these usages are a bit complex.

Now I just did some string manipulations by using the strings library. So, the current one is also working on the same logic.

For removal, I used the Replace function directly. However, for additions, some processing is necessary and applied.

Scope: tjtypes.CRDScopeCluster,
// Register built-in post-generation hooks
PostGenerationHooks: []PostGenerationHook{
NewStorageVersionMarkerUpdateHook(),
Copy link
Collaborator

@ulucinar ulucinar Jan 19, 2026

Choose a reason for hiding this comment

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

A high-level comment: (That we also discussed offline) We would like to break the current monolithic pipeline into smaller, modular steps and be able to selectively generate a single version of a single resource, for example. Once we achieve this, it will become much more feasible for us to generate the selected older API versions of selected resources when we need to do so, like there's an API change in the "preferred" version and we need to update the old versions.

This is currently not practical and we are relying on complex post-processors to work around this. And this also means we are building technical debt when we need updates to the older versions (or simply we rely on manual code updates on previously generated code).

I'm a little bit hesitant with the hooks API we are introducing here, although hooks can be utilized for other purposes than what we are doing here with them (updating the already generated versions).

Furthermore, we also setup a higher level code generation pipeline using the //go:generate markers on Go source, like here. So, the hook API here is a lower-level alternative to that with some convenience of itself being able to programmatically access the upjet resource configuration API.

Because of these reasons, I feel like it's better for us at least not to export this API, even if you'd like to utilize the convenience of accessing the resource configuration (which the new API-related configuration becomes part of).

We could also choose to move the new configuration (ServedVersions, etc.) out of upjet's resource configuration so that the implementations of these two post processors can completely be moved outside of upjet's code generation pipeline, and possibly added to the higher-level go:generate-based pipeline above.

But I still think the current resource configuration is better because:

  • Resource configuration determines/affects the shapes of the CRDs generated. Other aspects already live in upjet resource configurations. Why would we then move served version or deprecated version configurations out of upjet's resource configuration?
  • Once we achieve a modular upjet code generation pipeline, we will need the resource configuration when re-generating the old versions according to the new version. In my opinion, this is a much cleaner, easier to maintain approach, then trying to post-process the already generated (but no longer auto-generated) code.

Another approach could be the move the new configuration and the post processors both out of upjet's code generation pipeline. But I also think why should we not enjoy the convenience as long as long we don't make this part of the API contract, till we do the refactoring on the code generation pipelines?

TLDR; how about unexporting the hooks APIs? If some unrelated use case pops up (some other use case than modifying the auto-generated code), then we can reconsider exporting them. We already know the drawbacks of updating auto-generated code in post processors so maybe we would like to prioritize the pipeline refactoring over adding new features that will require us to update the older versions. So we may have a change to block further technical debt.

When we export this, with a small probability, other provider maintainers may start relying on these APIs to update the older API versions like we do here. And according to our discussions, I believe we've consensus on preventing this as we this as technical debt. So let's be proactive and block the way to technical debt. Maybe some other use case pops up and then we reconsider.

Copy link
Member Author

Choose a reason for hiding this comment

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

As we talked about offline, I agree on the unexporting them. Unexported this hook mechanism.

…em, unexport post generation hook mechanism, in hook implementation remove ast usage and just do with string manipulation, break hook implementations to their own files, apply some nit comments

Signed-off-by: Sergen Yalçın <yalcinsergen97@gmail.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

This PR adds API version deprecation and served versions support to the Kubernetes API code generator. It introduces configuration types and methods for managing deprecation metadata, implements post-generation hooks to update kubebuilder markers in generated files, and extends CRD templates to conditionally include deprecation and unserved markers.

Changes

Cohort / File(s) Summary
Core Config API Enhancement
pkg/config/resource.go
Introduces VersionDeprecation struct with Warning, DeprecationRelease, and PlannedRemovalRelease fields. Adds six new public methods: SetServedVersions(), HasExplicitServedVersions(), GetServedVersions(), SetDeprecatedVersion(), GetDeprecatedVersions(), and IsVersionDeprecated() with validation logic. Adds internal fields servedVersions and deprecatedVersions to track configuration state.
CRD Generation Updates
pkg/pipeline/crd.go, pkg/pipeline/templates/crd_types.go.tmpl
Updates CRD generation to consolidate metadata into a local crdMap structure. Adds conditional deprecation support by checking version status and setting DeprecationWarning, DeprecationNotice, and MarkUnserved fields. Introduces helper functions buildEnhancedDeprecationWarning() and buildDeprecationNotice(). Template updated to conditionally insert +kubebuilder:unservedversion and +kubebuilder:deprecatedversion:warning markers with optional deprecation notice in descriptions.
Post-Generation Hook Framework
pkg/pipeline/hooks.go
Defines postGenerationHook interface with Run method for post-generation processing. Introduces validateFilePath() helper that validates file paths are within a base directory via path normalization and relative path computation.
Lifecycle Marker Updates
pkg/pipeline/hooks_deprecated_version.go, pkg/pipeline/hooks_storage_version.go
Implements lifecycleMarkerUpdateHook and storageVersionMarkerUpdateHook to update kubebuilder markers in zz_*_types.go files. Adds complex marker insertion/removal logic that locates type declarations, finds insertion points, handles marker ordering (storageversion before unservedversion before deprecatedversion), and manages deprecation notices in type descriptions. File path construction, validation, and read/write operations included with error handling.
Pipeline Runner Integration
pkg/pipeline/run.go
Adds postGenerationHooks field to PipelineRunner and registers both marker update hooks during initialization. Extends Run flow to execute hooks after resource generation, iterating through hooks and panicking on failure.
Marker Handling Tests
pkg/pipeline/hooks_test.go
Comprehensive test suite (863 lines) validating marker placement, ordering, and deprecation logic. Tests cover storageversion, unservedversion, and deprecatedversion marker insertion/removal, round-trip validation, edge cases with missing types, marker interactions, and deprecation notice generation.

Sequence Diagram

sequenceDiagram
    participant Config as Provider Config
    participant Resource as Resource API
    participant CRDGen as CRD Generator
    participant PostHook as Post-Gen Hooks
    participant FileSystem as File System

    Config->>Resource: SetServedVersions(versions)
    Config->>Resource: SetDeprecatedVersion(version, metadata)
    Resource->>Resource: Validate & Store
    
    CRDGen->>Resource: GetServedVersions()
    CRDGen->>Resource: IsVersionDeprecated(version)
    Resource-->>CRDGen: Served list & deprecation metadata
    CRDGen->>CRDGen: Build CRD with markers
    CRDGen-->>PostHook: Generated files
    
    PostHook->>PostHook: lifecycleMarkerUpdateHook
    PostHook->>FileSystem: Read type files
    FileSystem-->>PostHook: File content
    PostHook->>PostHook: Update unservedversion markers
    PostHook->>PostHook: Update deprecatedversion markers
    PostHook->>FileSystem: Write updated files
    
    PostHook->>PostHook: storageVersionMarkerUpdateHook
    PostHook->>FileSystem: Read previous version files
    FileSystem-->>PostHook: File content
    PostHook->>PostHook: Add/remove storageversion markers
    PostHook->>FileSystem: Write updated files
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 With marker wisdom, we now know
Which versions served, which deprecated flow
Kubebuilder signs mark the way,
API lifecycle, here to stay!
Types updated, hooks compile,
Version management with style!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing new configuration APIs and mechanisms for managing MR API version lifecycle policies.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 2

🤖 Fix all issues with AI agents
In `@pkg/config/resource.go`:
- Around line 1021-1028: The code currently uses lexicographic string comparison
to validate that deprecation.PlannedRemovalRelease is after
deprecation.DeprecationRelease, which fails for multi-digit semver components;
replace the string comparison with a proper semantic version comparison using
golang.org/x/mod/semver (import it) and call
semver.Compare(deprecation.PlannedRemovalRelease,
deprecation.DeprecationRelease) to ensure the planned removal is strictly
greater; keep the existing error message when the comparison indicates the
planned removal is not after the deprecation.

In `@pkg/pipeline/hooks_deprecated_version.go`:
- Around line 251-281: The current findDescriptionBlock function uses a fixed
look-back limit (typeLineIndex-10) which can skip doc lines when many
kubebuilder markers exist; remove the hard cap and iterate i from
typeLineIndex-1 down to 0, stopping only when you hit the first
non-comment/non-kubebuilder line (but still treating "//" as description lines
and ignoring "// +kubebuilder:" markers), and update
descriptionStartIndex/descriptionEndIndex accordingly; apply the identical
change to the analogous function around lines 337-349 so both scanning routines
no longer use the 10/20-line limits.
🧹 Nitpick comments (4)
pkg/pipeline/crd.go (1)

109-119: Consider simplifying the served version check with slices.Contains.

The manual loop to check if the current version is served could be simplified using Go's slices.Contains (available since Go 1.21). This improves readability and reduces boilerplate.

♻️ Suggested refactor
+import "slices"
+
 // Check if this version should be marked as unserved
 // With SetServedVersions() validation, the current version MUST be included
 // if served versions are explicitly configured. This check is defensive but should
 // not trigger under normal circumstances with validated configuration.
 servedVersions := cfg.GetServedVersions()
-isServed := false
-for _, v := range servedVersions {
-	if v == cfg.Version {
-		isServed = true
-		break
-	}
-}
-if !isServed {
+if !slices.Contains(servedVersions, cfg.Version) {
 	crdMap["MarkUnserved"] = "true"
 }
pkg/pipeline/hooks_test.go (1)

245-269: Consider adding test for warning messages with special characters.

The test cases use clean warning strings. Consider adding a test case with a warning containing quotes or other special characters to ensure proper escaping in the generated marker.

Example test case to add:

"DeprecatedWithSpecialChars": {
    reason: "Should handle warning with special characters.",
    args: args{
        content:      baseContent,
        typeName:     "Member",
        version:      "v1beta1",
        isServed:     true,
        isDeprecated: true,
        deprecation: config.VersionDeprecation{
            Warning: `Use "v1beta2" instead`,
        },
    },
    want: want{
        // Verify escaping behavior
    },
},
pkg/pipeline/hooks_deprecated_version.go (1)

7-15: Use github.com/pkg/errors for wrapping.

The import uses github.com/crossplane/crossplane-runtime/v2/pkg/errors, but the guidelines require github.com/pkg/errors for wrapping/context. Please switch to align with the repo standard.

🔧 Suggested import change
-	"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
+	"github.com/pkg/errors"

As per coding guidelines, please use github.com/pkg/errors.

pkg/pipeline/hooks_storage_version.go (1)

7-16: Use github.com/pkg/errors for wrapping.

This file also imports github.com/crossplane/crossplane-runtime/v2/pkg/errors. Guidelines require github.com/pkg/errors for wrapping/context.

🔧 Suggested import change
-	"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
+	"github.com/pkg/errors"

As per coding guidelines, please use github.com/pkg/errors.

Comment on lines +1021 to +1028
// Validate release ordering if both are provided
if deprecation.DeprecationRelease != "" && deprecation.PlannedRemovalRelease != "" {
// Simple string comparison works for semver format (v1.5.0 < v1.7.0)
if deprecation.PlannedRemovalRelease <= deprecation.DeprecationRelease {
return errors.Errorf("PlannedRemovalRelease %q must be after DeprecationRelease %q",
deprecation.PlannedRemovalRelease, deprecation.DeprecationRelease)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the specific lines mentioned
sed -n '1015,1035p' pkg/config/resource.go

Repository: crossplane/upjet

Length of output: 857


🏁 Script executed:

# Check what version patterns are actually used in the codebase
rg -i 'v[0-9]+\.[0-9]+\.[0-9]+' --max-count=20

Repository: crossplane/upjet

Length of output: 5749


🏁 Script executed:

# Search for any existing semver libraries imported
rg 'import.*semver|Masterminds|coreos/go-semver' pkg/

Repository: crossplane/upjet

Length of output: 42


🌐 Web query:

Go semver version comparison library recommended best practices 2024 2025

💡 Result:

Short summary — recommended Go semver libs (2024–2025) and best practices

Recommended libraries

  • golang.org/x/mod/semver — official x/mod package for simple/accurate comparisons (requires leading "v", returns canonical form). [1]
  • github.com/Masterminds/semver — feature-rich (parsing, ranges/constraints, v3+ for Go modules). [2]
  • github.com/blang/semver (v4) — fast, spec‑compliant, zero‑stdlib approach (value types). [3]
  • github.com/gomodules/semver — modern, constraint and sort support, optional "v" handling. [4]
  • github.com/aquasecurity/go-version — parses semver-like versions, supports extra components and constraints (useful if you need non‑standard variants). [5]

Practical best practices

  • Prefer a well‑maintained library rather than writing custom parsing/comparison. Use golang.org/x/mod/semver for straightforward precedence checks; use Masterminds or blang/gomodules when you need constraints, sorting, or richer parsing. [1][2][3][4]
  • Always use Go modules (go.mod) and pin exact versions (avoid @latest/main) for reproducible builds; vendor if you require hermetic CI. Use go mod tidy and run dependency audits. [6]
  • Normalize/canonicalize versions before comparing (use library canonical functions) and be explicit about accepting or requiring a leading "v". [1][2]
  • Handle pre-release and build metadata according to SemVer 2.0.0 rules (pre-release < release; build metadata ignored for precedence). Use libraries that implement the spec rather than ad hoc rules. [1][2][3]
  • Use semantic‑aware constraint checking (e.g., range expressions) from a library when deciding allowed upgrades; rely on MVS behavior but assert constraints in CI. [2][4]
  • Keep dependencies updated and monitored (Dependabot or CI checks), add tests for version logic, and document version expectations in your project README. [6]

If you want, I can: (a) show short code examples for Compare/IsValid using x/mod or Masterminds, or (b) recommend one library tailored to your exact needs (requirement: simple compare vs. range/constraint support).


Use a proper semver comparison library instead of string comparison.

String comparison fails for semantic versions with multi-digit components. For example, "v1.10.0" < "v1.9.0" evaluates to true lexicographically, but semantically v1.10.0 comes after v1.9.0. Use golang.org/x/mod/semver (standard library) or another dedicated semver library for correct ordering validation.

Example fix
import "golang.org/x/mod/semver"

// Replace the string comparison with semver comparison
if semver.Compare(deprecation.PlannedRemovalRelease, deprecation.DeprecationRelease) <= 0 {
    return errors.Errorf("PlannedRemovalRelease %q must be after DeprecationRelease %q",
        deprecation.PlannedRemovalRelease, deprecation.DeprecationRelease)
}
🤖 Prompt for AI Agents
In `@pkg/config/resource.go` around lines 1021 - 1028, The code currently uses
lexicographic string comparison to validate that
deprecation.PlannedRemovalRelease is after deprecation.DeprecationRelease, which
fails for multi-digit semver components; replace the string comparison with a
proper semantic version comparison using golang.org/x/mod/semver (import it) and
call semver.Compare(deprecation.PlannedRemovalRelease,
deprecation.DeprecationRelease) to ensure the planned removal is strictly
greater; keep the existing error message when the comparison indicates the
planned removal is not after the deprecation.

Comment on lines +251 to +281
// findDescriptionBlock finds the description comment block before the type
func findDescriptionBlock(lines []string, typeLineIndex int) (int, int) {
descriptionStartIndex := -1
descriptionEndIndex := -1

for i := typeLineIndex - 1; i >= 0 && i >= typeLineIndex-10; i-- {
line := strings.TrimSpace(lines[i])
if line == "" {
if descriptionEndIndex != -1 {
break
}
continue
}
if strings.HasPrefix(line, "//") && !strings.HasPrefix(line, "// +kubebuilder:") {
if descriptionEndIndex == -1 {
descriptionEndIndex = i
}
descriptionStartIndex = i
} else {
if descriptionEndIndex != -1 {
break
}
if strings.HasPrefix(line, "// +kubebuilder:") {
continue
}
break
}
}

return descriptionStartIndex, descriptionEndIndex
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove fixed look‑back limits to avoid missing doc blocks.

The 10/20-line caps can skip the description line when many kubebuilder markers exist, leaving stale or missing deprecation notices. Scanning back to the first non-comment line is safer.

🛠️ Proposed fix
-	for i := typeLineIndex - 1; i >= 0 && i >= typeLineIndex-10; i-- {
+	for i := typeLineIndex - 1; i >= 0; i-- {
@@
-	for i := typeLineIndex - 1; i >= 0 && i >= typeLineIndex-20; i-- {
+	for i := typeLineIndex - 1; i >= 0; i-- {

Also applies to: 337-349

🤖 Prompt for AI Agents
In `@pkg/pipeline/hooks_deprecated_version.go` around lines 251 - 281, The current
findDescriptionBlock function uses a fixed look-back limit (typeLineIndex-10)
which can skip doc lines when many kubebuilder markers exist; remove the hard
cap and iterate i from typeLineIndex-1 down to 0, stopping only when you hit the
first non-comment/non-kubebuilder line (but still treating "//" as description
lines and ignoring "// +kubebuilder:" markers), and update
descriptionStartIndex/descriptionEndIndex accordingly; apply the identical
change to the analogous function around lines 337-349 so both scanning routines
no longer use the 10/20-line limits.

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.

2 participants